Android 之 自定义控件

1、View 与 ViewGroup

View 是Android 所有控件类的基类,我们平常使用的TextView 、ImageView都是继承的View 

ViewGroup 是Android 所有能拥有子类(其实就是布局控件:如LinearLayout)的基类,我们可以这样理解ViewGroup是View的一个集合、组合类,里面包含很多的View ,当然也可以包含ViewGroup(ViewGroup的基类也是View)

以此类推,即可形成一个树,如下图

2、坐标系

Android 系统中有两种坐标系,分别为Android 坐标系和View坐标系。了解这两种坐标系可以帮助我们实现View 的各种操作。

2.1、Android 坐标系

在Android 中,将屏幕左上角的顶点作为Android 坐标系的原点,这个原点向右是X轴的正方向,向下是Y轴的正方向,如下图,另外在触控事件中,使用getRawX()  和getRawY() 方法获取的坐标也是Android 坐标系的坐标

Android 坐标系

2.2、View 坐标系

View坐标系与Android坐标系是共存的,并不冲突,如下图

View坐标系

2.2.1、View获取自身的宽和高

int width = getRight() - getLeft();
int height = getBottom() - getTop();

当然这样获取View的宽度和高度是有点麻烦的,因为可以可以直接通过getWidth()方法来获取宽度,如下是View源码,

  /**
     * Return the scrolled top position of this view. This is the top edge of
     * the displayed part of your view. You do not need to draw any pixels above
     * it, since those are outside of the frame of your view on screen.
     *
     * @return The top edge of the displayed part of your view, in pixels.
     */
    @InspectableProperty
    public final int getScrollY() {
        return mScrollY;
    }

    /**
     * Return the width of your view.
     *
     * @return The width of your view, in pixels.
     */
    @ViewDebug.ExportedProperty(category = "layout")
    public final int getWidth() {
        return mRight - mLeft;
    }

而这里面的变量mRight又是通过方法getRight()返回的,如下源码

    /**
     * Right position of this view relative to its parent.
     *
     * @return The right edge of this view, in pixels.
     */
    @ViewDebug.CapturedViewProperty
    public final int getRight() {
        return mRight;
    }

同理mLeft也一样,由此可知getWidth()是由getRight() - getLeft()方法得到的和直接通过方法getWidth()获取是一样的

2.2.2、View自身的坐标

通过如下方法可以获得View到其父控件(ViewGroup)的距离

  • getTop() : 获取View自身顶边到父布局顶边的距离
  • getLeft() : 获取View自身左边到父布局左边的距离
  • getRight() : 获取View自身右边到父布局左边的距离
  • getBottom() : 获取View自身底边到父布局顶边的距离

2.2.3、MotionEvent提供的方法

View坐标系

如上图中间红色的那个圆点,假设就是我们触摸的点,我们知道无论是View还是ViewGroup,最终的点击事件都会由onTouchEvent(MotionEvent event)方法来处理。MotionEvent在用户交互中作用重大,其内部提供很多事件常量,比如我们常用的ACTION_DOWN、ACTION_UP和ACTION_MOVE。此外MotionEvent也提供了获取焦点坐标的各种方法

  • getX() 获取点击事件距离控件左边的距离,即视图坐标
  • getY() 获取点击事件距离控件顶边的距离,即视图坐标
  • getRawX() 获取点击事件距离屏幕左边的距离,即绝对坐标
  • getRawY() 获取点击事件距离屏幕顶边的距离,即绝对左边

2.3 View的滑动

View的滑动时Android是实现自定义控件的基础,同时在开发中我们也难免会遇到View滑动处理,其实不管是那种滑动方式,其基本思想都是类似的,当点击事件传到View时,系统记下触摸点的坐标,手指移动时系统记下移动后的触摸的坐标并计算出偏移量,并通过偏移量来修改View的坐标,实现View滑动有多种方法,在这里主要用到的6种滑动方法,分别是layout()、offsetLeftAndRight()、offsetTopAndBottom()、LayoutParams、动画、scollTo、scollBy和Scroller

2.3.1、layout()方法

View进行绘制的时候会调用onLayout()方法来设置显示位置,因此我们同样也可以通过修改View的left、top、right、bottom这4种属性来控制View的坐标。首先我们需要自定义一个View--TextCsutomView继承AppCompatTextView ,在onTouchEvent()方法种获取触摸点的坐标 如下是实现的代码

// 可以移动的TextView
public class MoveTextView extends AppCompatTextView {

    private int lastX = 0;
    private int lastY = 0;

    public MoveTextView(Context context) {
        super(context);
    }

    public MoveTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MoveTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr){
        super(context, attrs, defStyleAttr);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
         // 获取手指触摸点的横坐标和纵坐标
        int x = (int) event.getX(); 
        int y = (int) event.getY(); 
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;

            case MotionEvent.ACTION_MOVE:
                // 计算移动的距离
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                // 调用layout来重新设置位置
                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
                break;
        }
        return true;
    }
}

而这个过程是如何计算的看如下图

移动计算原理图

2.3.2 offsetLeftAndRight() 与 offsetTopAndBottom()

这两个方法和layout() 方法的效果是差不多的,其实方法也差不多,我们将ACTION_MOVE中的替代代码设置如下

case MotionEvent.ACTION_MOVE:
     // 计算移动的距离
     int offsetX = x - lastX;
     int offsetY = y - lastY;
     offsetLeftAndRight(offsetX);
     offsetTopAndBottom(offsetY);
     break;

2.3.3 LayoutParams(改变布局参数)

LayoutParams 主要保存了一个View的布局参数,因此我们可以通过LayoutParams来改变View的布局参数从而达到改变View位置的效果。同理,我们将ACTION_MOVE中的代码替换成如下代码

    case MotionEvent.ACTION_MOVE:
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
                params.leftMargin = getLeft() + offsetX;
                params.topMargin = getTop() + offsetY;
                setLayoutParams(params);
                break;

其实如果确定父控件是LinearLayout,我们就可以使用LinearLayout.LayoutParams,父控件是RelativeLayout就使用RelativeLayout.LayoutParams

2.3.4 动画

可以使用View动画来移动,在res目录下创建anim文件夹,并创建translate.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="1000"
        android:fromXDelta="0"
        android:toXDelta="300" />
</set>

然后再通过mTextView.setAnimation(AnimationUtils.loadAnimation(this,R.anim.translate));执行动画,但是这个会水平平移300像素之后回到原来得位置,如果要解决这个问题需要新增一个属性值fillAfter = "true"

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true">
    <translate
        android:duration="1000"
        android:fromXDelta="0"
        android:toXDelta="300" />
</set>

但是这种动画得移动并没有改变View得位置参数,如果我们对一个button进行平移动画操作,当动画执行完成之后我们在当前位置去点击按钮是无法触发点击事件得,在原来得位置点击却能触发点击事件。不过这个问题在Android 3.0 时出现得属性动画解决了

2.3.5 scrollTo 与 scollBy

scrollTo(x,y)表示移动到一个具体得坐标点,而scrollBy(dx,dy)则表示移动得增量为dx,dy其中scrollBy最终也要调用scrollTo的 如下是源码

// scrollTo 源码
  public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
  }
// scrollBy
 public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
  }

scrollTo、scrollBy移动的是View的内容,如果在ViewGroup中使用,则是移动其所有的子View,我们将ACTION_MOVE中的代码替换成如下内容:

((View)getParent()).scrollBy(-offsetX,-offsetY)

2.3.6 Scroller

我们在使用scrollTo/scrollBy方法进行滑动时,这个过程是瞬间完成的,所有用户体验不好,这里我们可以使用Scroller来实现有过渡效果的滑动,这个过程不是瞬间完成的,而是在一定的时间间隔内完成的。Scroller本身是不能实现View的滑动的,他需要与View的computeScroll()方法配合才能实现弹性滑动的效果。在这里我们实现TextCustomView平滑的向右滑动,首先我们需要初始化Scroller 如下代码

   public TextCustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    public void init(Context context) {
        mScroller = new Scroller(context);
    }

接下来重写computeScroll()方法,系统会在绘制View的时候在draw()方法中调用该方法,在这个方法中,我们调用父类的scrollTo()方法并通过Scroller来不断获取当前的滚动值,每滚动一小段距离我们就调用invalidate()方法不断进行重绘,重绘就会调用computeScroll()方法,这样我们通过不断移动一小段距离并连贯起来就实现了平滑移动的效果。

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            ((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

我们在TextCustomView中编写一个方法smoothScrollTo()方法,调用Scroller的startScroll()方法,在2000ms内沿X轴平移delta像素,代码如下所示

 public void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();
        int delta = destX - scrollX;
        mScroller.startScroll(scrollX, 0, delta, 0, 2000);
        invalidate();
    }

最后在代码中调用即可

  mBinding.tvCustom.smoothScrollTo(-400, 0)

完整代码为


public class TextCustomView extends AppCompatTextView {

    private Scroller mScroller;

    public TextCustomView(Context context) {
        this(context, null);
    }

    public TextCustomView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TextCustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    public void init(Context context) {
        mScroller = new Scroller(context);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            ((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

    public void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();
        int scrollY = getScrollY();
        int deltaY = destY - scrollY;
        int deltaX = destX - scrollX;
        mScroller.startScroll(scrollX, scrollY, deltaX, deltaY, 2000);
        invalidate();
    }

}

2.4 属性动画

在属性动画出来之前Android系统提供的动画只有帧动画和View动画,View动画提供了AlphaAnimation、RotateAnimation、TranslateAnimation、ScaleAnimation这4种动画,并提供AnimationSet动画集来组合使用多种动画,但是由于这个View动画存在致命缺陷,就是不具备交互性,当某个元素发生View动画后,其响应事件仍在原来的位置,所有View动画只能做动画效果,涉及到交互就不能使用了。

在属性动画(Animator)中使用最多的就是AnimatorSet和ObjectAnimator配合,而ObjectAnimator能进行更精细化的控制,

2.4.1 ObjectAnimator

ObjectAnimator是属性动画中最重要的类,创建一个ObjectAnimator只需要通过静态工厂类直接返还一个ObjectAnimator对象,参数包括一个对象和对象的属性名称,但这个属性必须有get和set方法,其内部回通过Java反射机制来调用set方法修改对象属性值,如下实现平移动画,其代码如下

mBinding.btnTest.setOnClickListener {
   val mObjectAnimator =
    ObjectAnimator.ofFloat(mBinding.tvCustom, "translationX", 400F)
    mObjectAnimator.duration = 2000
   mObjectAnimator.start()
}

通过ObjectAnimator的静态方法,创建一个ObjectAnimator对象,查看ObjectAnimator的静态方法ofFloat(),源码如下

  public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) {
        ObjectAnimator anim = new ObjectAnimator(target, propertyName);
        anim.setFloatValues(values);
        return anim;
    }

未完待续...

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逍遥Y

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值