View体系

  **

坐标系

  Android系统中有两种坐标系,分别为Android坐标系和View坐标系。

Android坐标系

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

View坐标系

  View坐标系和Android坐标系不冲突,也是从原点往右为X轴的正方向,从原点往下是Y轴的正方向,但是原点不再是屏幕的左上角顶点,而是父视图的左上角顶点坐标。两者是共同存在的,它们一起帮助开发者更好地控制View。在触控事件中,使用getX()和getY()获取当前触摸点相对于视图坐标系的坐标。

  • View获取自身的宽和高
      根据上图,我们可以算出View的宽和高:
int width = getRight() - getLeft();
int height = getBottom() - getTop();

  当然,这样做有些麻烦,因为系统已经为我们提供了获取View宽和高的方法。getHeight()用来获取View自身的高度,getWidth()用来获取View自身的宽度。从源码中看,这两个方法和是和图中计算宽高的算法是一致的。

public final int getWidth() {
    return mRight - mLeft;
}

public final int getHeight() {
    return mBottom - mTop;
}
  • View自身的坐标
      通过如下的方法可以获得View到其父控件的距离:

    1. getTop():获取View自身顶边到其父布局顶边的距离;
    2. getLeft():获取View自身左边到其父布局左边的距离;
    3. getRight():获取View自身右边到其父布局右边的距离;
    4. getBottom():获取View自身底边到其父布局底边的距离;
  • MotionEvent提供的方法
      无论是在View还是在ViewGroup中,最终的点击事件都会由onTouchEvent(MotionEvent event)方法来处理。MotionEvent在用户交互中作用重大,其内部提供了很多事件常量,比如我们常用的ACTION_DOWN、ACTION_UP、ACTION_MOVE等。此外,MotionEvent也提供了获取焦点坐标的各种方法。

    1. getX():获取点击事件距离控件左边的距离,即视图坐标;
    2. getY():获取点击事件距离控件顶边的距离,即视图坐标;
    3. getRawX():获取点击事件距离整个屏幕左边的距离,即绝对坐标;
    4. getRawY():获取点击事件距离整个屏幕顶边的距离,即绝对坐标;

View的滑动

  View的滑动是Android实现自定义控件的基础,同时在开发中我们也难免遇到View的滑动处理。其实不管是哪种滑动方式,其基本思想都是类似的:当点击事件传到View时,系统记下触摸点的坐标,手指移动时系统记下移动后触摸的坐标并算出偏移量,并通过偏移量来修改View的坐标。实现View滑动的方法有很多,接下来讲六种滑动方法:layout()、offsetLeftAndRight()与offsetTopAndBottom()、LayoutParams、动画、scollTo与scollBy以及Scroller。

layout()

  View进行绘制的时候会调用onLayout()方法来设置显示的位置,我们同样可以通过修改View的left、top、right、bottom这四种属性来控制View的坐标。我们自定义一个View,在onTouchEvent()方法中获取触摸点的坐标,代码如下:

@Override
public boolean onTouchEvent(MotionEvent event) {
    //获取手指触摸点的横坐标和纵坐标;
    int x = (int) event.getX();
    int y = (int) event.getY();
    Log.e(TAG,"onTouchEvent........");
    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            //获取手指第一次在屏幕按下的坐标;
            Log.e(TAG,"MotionEvent.ACTION_DOWN........");
            lastX = x;
            lastY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            //计算移动的距离;
            Log.e(TAG,"MotionEvent.ACTION_MOVE........");
            int offsetX = x - lastX;
            int offsetY = y - lastY;
            //调用layout()方法来重新放置它的位置;
            layout(getLeft() + offsetX,getTop() + offsetY,getRight() + offsetX,getBottom() + offsetY);
            break;
        case MotionEvent.ACTION_UP:
            Log.e(TAG,"MotionEvent.ACTION_UP........");
            break;
        default:
            break;
    }
    return true;
}

  在手指移动的时候,都会进入onTouchEvent()方法,然后进入ACTION_MOVE里面来,重新测算移动的距离,使用layout()方法对屏幕重新布局,从而达到移动View的效果。

public class CustomView1 extends View {

    private String TAG = CustomView1.class.getSimpleName();
    int lastX;
    int lastY;

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

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

    public CustomView1(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();
        Log.e(TAG,"onTouchEvent........");
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG,"MotionEvent.ACTION_DOWN........");
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG,"MotionEvent.ACTION_MOVE........");
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                layout(getLeft() + offsetX,getTop() + offsetY,getRight() + offsetX,getBottom() + offsetY);
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG,"MotionEvent.ACTION_UP........");
                break;
            default:
                break;
        }
        return true;
    }
}

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.ex.myapplication.view.CustomView1Activity">

    <com.ex.myapplication.view.CustomView1
        android:id="@+id/view_customView1_cv"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_margin="50dp"
        android:background="#FF0000"
        />

</LinearLayout>

  这样子,我们自定义的View就会随着我们手指的滑动改变自己的位置了。

offsetLeftAndRight()与offsetTopAndBottom()

  这两个方法和layout()方法的效果差不多,其使用方式也差不多。只需要将讲解layout()方法时MotionEvent.ACTION_MOVE的代码替换为如下代码即可:

case MotionEvent.ACTION_MOVE:
    Log.e(TAG,"MotionEvent.ACTION_MOVE........");
    int offsetX = x - lastX;
    int offsetY = y - lastY;
    offsetLeftAndRight(offsetX);
    offsetTopAndBottom(offsetY);
    break;

LayoutParams(改变布局参数)

  LayoutParams主要保存一个View的布局参数,因此我们可以通过LayoutParams来改变View的布局参数从而达到改变View位置的效果。同样将讲解layout()方法中MotionEvent.ACTION_MOVE代码替换为如下代码:

case MotionEvent.ACTION_MOVE:
    Log.e(TAG,"MotionEvent.ACTION_MOVE........");
    int offsetX = x - lastX;
    int offsetY = y - lastY;
    LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams)getLayoutParams();
    layoutParams.leftMargin = getLeft() + offsetX;
    layoutParams.topMargin = getTop() + offsetY;
    setLayoutParams(layoutParams);
break;

  因为父控件是LinearLayout,所以我们用了LinearLayout.LayoutParams。如果父控件是RelativeLayout,则要使用RelativeLayout.LayoutParams。除了使用布局的LayoutParams外,还可以使用ViewGroup.MarginLayoutParams来实现。代码如下:

ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams)getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);

动画

  可以采用View动画来移动,在res目录下新建anim文件夹并创建一个set类型的xml文件;

<?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>

  在Java代码中调用:

mView.setAnimation(AnimationUtils.loadAnimation(this,R.anim.translate));

  需要注意的是,View动画并不能改变View的位置参数。但是,在Android 3.0出现的属性动画可以解决这个问题,因为属性动画不仅可以执行动画,还能够改变View的位置参数。代码如下:

ObjectAnimator.ofFloat(mView,"translationX",0,300).setDuration(1000).start();

scrllTo与scollBy

  scrollTo(x,y)表示移动到一个具体的坐标点,而scrollBy(dx,dy)则表示移动的增量为dx、dy。其中,scrollBy()最终也是要调用scrollTo()方法。
  在View.java中scrollBy()和scrollTo()方法的源码如下:

public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

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();
        }
    }
}

  scrollTo()、scrollBy()移动的是View的内容,如果在ViewGroup中使用,则是移动所有的子View。如果要实现CustomView随手指移动的效果,需要把偏移量设置为负值。

Scroller

  在使用scrollTo()和scrollBy()方法进行滑动时,这个过程是瞬间完成的,用户体验不太好。可以使用Scroller来实现有过渡效果的滑动,这个过程不是瞬间完成的,而是在一定时间间隔内完成的。Scroller本身是不能实现View的滑动的,它需要与View的computeScroll()方法配合才能实现弹性滑动的效果。
  以下代码实现CustomView平滑地向右移动:

public class CustomView5 extends View {

    private String TAG = CustomView5.class.getSimpleName();
    int lastX;
    int lastY;
    Scroller mScroller;

    public CustomView5(Context context) {
        super(context);
        mScroller = new Scroller(context);
    }

    public CustomView5(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
    }

    public CustomView5(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mScroller = new Scroller(context);
    }
    //系统会在绘制View的时候在draw()方法中调用该方法。在这个方法中,我们调用父类的scrollTo()犯法并通过Scroller来不断获取当前的滚动值,每滑动一小段距离我们就调用invalidate()方法不断进行重绘,重绘就会调用computeScroll()方法,这样我们通过不断地移动一个小的距离并连贯起来就实现了平滑移动的效果;
    @Override
    public void computeScroll() {
        super.computeScroll();
        if(mScroller.computeScrollOffset()){
            ((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            invalidate();
        }
    }
    //在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();
    }

}
//实现将CustomView沿着X轴向右平移400像素;
mCustomView5.smoothScrollTo(-400,0);

属性动画

解析Scroller

View的事件分发机制

View的工作流程

自定义View

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-------------------last line for now-------------------