View 位移的几种方式及细节

控件的位移,有几种方法,以前的文章中提到过,这一篇是多了种方法,并且深入一些细节来讲述之间的原理的区别。比如说自定义一个view,重写onTouchEvent()方法,在这里面做位移的操作。


    public class DrawView extends View {

    private final static String TAG = "DrawView";
        public DrawView(Context context) {
            this(context, null);
        }

        public DrawView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }

        public DrawView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }

        Paint mPaint= new Paint();
        private void init(){
            mPaint.setAntiAlias(true);
            //设置画笔宽度
            mPaint.setStrokeWidth(5);
            //设置画笔颜色
            mPaint.setColor(Color.GRAY);
            //设置画笔样式
            mPaint.setStyle(Paint.Style.FILL_AND_STROKE);

        }

        @Override
        protected void onDraw(Canvas canvas) {
            canvas.drawColor(Color.BLUE);
            canvas.drawRect(new Rect(0, 0, 200, 200), mPaint);
        }

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            dragView();
            return super.onTouchEvent(event);
        }

        private void dragView() {
            ... // 位移的方法
        }
    }

布局文件
    <com.example.cn.desigin.view.DrawView
             android:layout_width="200dp"
             android:layout_height="150dp"/>

我们在 dragView() 方法中写出各种位移的方法,分几种不同的方法,先说静态坐标系,
    setTranslationX()、setTranslationY() 方法

    private int mX;
    private int mY;
    private void onTouchEvent7(MotionEvent event) {
        int rawX = (int)event.getRawX();
        int rawY = (int)event.getRawY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN://手指按下
                break;
            case MotionEvent.ACTION_MOVE://手指滑动
                //滑动时计算偏移量
                float moveX = rawX - mX;
                float moveY = rawY - mY;
                //随手指移动
                setTranslationX(getTranslationX() + moveX);
                setTranslationY(getTranslationY() + moveY);
                break;
            case MotionEvent.ACTION_UP://手指松开
                break;
        }
        mX = rawX;
        mY = rawY;
        Log.e(TAG, "onTouchEvent7"  + "     " + getTranslationX()+"   " + getTranslationY()
                + "     " + getLeft()+"   " + getTop() );
    }

在讲述上面代码之前,先说一点关于MationEvent 触摸事件坐标的知识,
getX()     触摸点距离当前 View 自身左边的距离
getY()     触摸点距离当前 View 自身顶部的距离
getRawX()    触摸点距离屏幕左边的距离( 绝对坐标系 )
getRawY()    触摸点距离屏幕顶部的距离( 绝对坐标系 )

我们知道,位移动画,有两种可以实现,一种是视图动画,TranslateAnimation;另一种是属性动画,通过ObjectAnimator来实现,最终调用的是view中的setTranslationX()和setTranslationY()方法。这两种都可以实现位移,区别是,当视图view从A处移动到B处,点击事件的点击区域,视图动画的点击区域仍然在移动前的地方,即A处,属性动画的点击区域,则随着视图的移动,来到了B处。今天重点说的是属性动画的位移方式。看上面的代码,onTouchEvent7()方法中的代码,进入方法,我们先获取了手机距离屏幕的距离,即
        int rawX = (int)event.getRawX();
        int rawY = (int)event.getRawY();
这两个值是手指头距离手机屏幕左上角的绝对距离,单位是px,switch语句外,我们会记录一下手指上一次的坐标,这样,ACTION_DOWN 的里面,我们什么操作也没做,但在最后,mX 记录的就是手指按下的距离x轴的距离,mY 就是y轴的距离,当我们手指头滑动时,rawX 会重新手指头所在屏幕的坐标,此时 float moveX = rawX - mX;计算出偏移量,然后getTranslationX() 是view距父View的原始的 translationX 的值,此时把 translationX 和 moveX 相加,计算出新的值,然后通过 setTranslationX()方法把 translationX 值更新,setTranslationY()方法也是同样的道理,然后就是 mX = rawX; mY = rawY; 重新更新一下 mX 和 mY 的值,重复执行 ACTION_MOVE 过程,这样,视图就可以移动了。
我们知道,
left = getLeft()    view 自身左侧到父View左侧的距离
top = getTop()    view 自身顶部到父View顶部的距离
right = getRight()    view 自身右侧到父View左侧的距离
botton = getBotton()    view 自身底部到父View顶部的距离
这四个属性好理解,但 getTranslationX() 和 getTranslationY() 呢? Translation 是偏移的意思, getTranslationX() 是x轴上偏移的距离,那么问题来了,是什么偏移了?偏移的参照物是什么?我们知道一个控件 View,里面包含着画布画面的视图内容,控件可以相对父控件左右上下位移,也可以控件不动,内容在父控件的可以绘制区域内上下左右移动,getTranslationX() 移动的就是内容,view本身的位置是不变的;至于偏移的参照物,是屏幕左上角、父view的左上角还是什么?我通过打开开发者模式以及上面代码中的log日志,判断出 getTranslationX() 偏移是内容偏移,参照物则是view本省的左上角,默认状态下,getTranslationX() 和 getTranslationY() 值都是零,我们移动后,内容往右下移动,比如说x和y都移动了20px,那么此时,getTranslationX() 和 getTranslationY() 值都是20。数值往右为正,往下为正,反之则为负。既然这两个值明白了,那么
getX()    其值为:getLeft()+getTranslationX(),当setTranslationX 时,getLeft()不会变,getX会变
getY()    其值为:getTop()+getTranslationY(),当setTranslationY 时,getTop()不会变,getY会变
setTranslationX() 和 setTranslationY() 移动时,它移动的是view的视图内容,内容可以移动的范围是父View允许绘制的区域,如果把开发者模式打开,我们可以看的更清楚。
我们看看下面的代码,大部分与上面方法类似,这次我们用的是 getX() 和 getY(),获取手指头距离自身view的左上角的值,
    private void onTouchEvent8(MotionEvent event) {
        int rawX = (int)event.getX();
        int rawY = (int)event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN://手指按下
                mX = rawX;
                mY = rawY;
                break;
            case MotionEvent.ACTION_MOVE://手指滑动
                //滑动时计算偏移量
                float moveX = rawX - mX;
                float moveY = rawY - mY;
                //随手指移动
                setTranslationX(getTranslationX() + moveX);
                setTranslationY(getTranslationY() + moveY);
                break;
            case MotionEvent.ACTION_UP://手指松开
                break;
        }
        Log.e(TAG, "onTouchEvent8"  + "     " + getTranslationX()+"   " + getTranslationY());
    }
我们发现,只有在 ACTION_DOWN 手指按下时,更新了 mX = rawX; mY = rawY; 值,拿到了手指按下时距离view左上角的距离,然后就是 ACTION_MOVE 手机滑动了,此时会拿到最新的值,然后求出偏量值 moveX 和 moveY,然后就是 setTranslationX() 和 setTranslationY() 设置偏移坐标了,我们发现,接下来只要手指头没有抬起来,就一直没有更新mX和mY的
值了。为什么呢?因为mX和mY始终是手指头相对view的左上角的值,这个左上角,是内容的左上角,setTranslationX 后,内容移动了,我们手指头也移动了。比如说,我们手指头往右移动了一下,对于手机来说,不是一次性执行完move行为,而是分段多次处理,比如第一次按下时,mX = 100, 此时ACTION_MOVE,假设rawX为103,即3个px,那么 moveX = 3,
getTranslationX() 为0,此时 setTranslationX(getTranslationX() + moveX); 即 setTranslationX(3);此时内容往右移动了3px,我们继续看ACTION_MOVE,此时 int rawX = (int)event.getX(); rawX 重新获取值,由于内容已经移动了3px,此时就相当于原先rawX为103的地方,随着内容的移动,rawX的值变为了100,所以新值int rawX = (int)event.getX(); 依据已经不是一开的坐标了,而是移动后的内容的左上角,因此就这样,每次移动一点,event.getX()的值就还原一点,然后手指头再次右滑,重新获取值相减,就这样移动了。

下面看一下通过控制 getLeft() 和 getTop() 的值来控制位移
    private int lastX;
    private int lastY;
    private void onTouchEvent6(MotionEvent event) {
        int x = (int) event.getRawX();
        int y = (int) event.getRawY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                // 计算偏移量
                int offsetX = x - lastX;
                int offsetY = y - lastY;// 同时对left和right进行偏移
                offsetLeftAndRight(offsetX);
                // 同时对top和bottom进行偏移
                offsetTopAndBottom(offsetY);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        // 记录触摸点坐标
        lastX = x;
        lastY = y;
        Log.e(TAG, "onTouchEvent6"  + "     " + getTranslationX()+"   " + getTranslationY()
                + "     " + getLeft()+"   " + getTop());
    }
offsetLeftAndRight() 和 offsetTopAndBottom() 控制的是View中的 protected int mLeft; protected int mRight; protected int mTop; protected int mBottom;四个属性,offsetLeftAndRight()传进去的值是位移偏移量,正数向右移动,负数向左移动;offsetTopAndBottom()正数向下移动,负数向上移动。

上面说的是控件不动,内容移动,现在说说移动控件位置的方法

    private int lastX;
    private int lastY;
    private void onTouchEvent3(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 offX = x - lastX;
                int offY = y - lastY;
                layout(getLeft() + offX, getTop() + offY, getRight() + offX, getBottom() + offY);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
    }
这一次,我们使用的手指在距离view的左上角的距离,计算出位移的偏移量int offX = x - lastX;int offY = y - lastY; 注意,getLeft() getTop() 等是指控件view的左边和上边 距离父控件View的左边的距离和顶部的距离,这个上面也介绍过了,getRight() 和 getBottom() 是指控件view的右边和底部的边距离父View的左边和顶部的距离。不明白的可以再百度一下。我们前两章讲了view的绘制过程,其中ViewGroup中是通过onLayout()方法来确定子view的所在位置,在onLayout()方法中,会计算出子view所在的位置后,然后调用child.layout(childLeft, childTop, childLeft + width, childTop + height); 方法来固定子view的位置,所以这里也是计算出了偏移量,再加上原先的距离父View的left的值,即
layout(getLeft() + offX, getTop() + offY, getRight() + offX, getBottom() + offY); 来确定子view的位置,子view就这么移动了。

同样是移动view,第二种方式

    private int mLastX4 = 0;
    private int mLastY4 = 0;
    private void onTouchEvent4(MotionEvent event) {
        int rawX = (int)event.getRawX();
        int rawY = (int)event.getRawY();

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                //计算移动的距离
                int offsetX = rawX - mLastX4;
                int offsetY = rawY - mLastY4;
                ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft() + offsetX;
                layoutParams.topMargin = getTop() + offsetY;
                setLayoutParams(layoutParams);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        mLastX4 = rawX;
        mLastY4 = rawY;
    }
上面是通过layout来直接确定view的位置,这种方法是根据子view的LayoutParams中的margin值,确切的说是marginLeft和marginTop来确定的,这个方法看上去没问题,但我们看清楚,是marginLeft和marginTop,也就是说,如果父View是FrameLayout,这个随手指头移动是没问题的,但如果是LinearLayout这种,并且view不是第一个子view,而是第二个或是第三个,那么layoutParams.topMargin 代表的是距离上面那个view的距离,但getTop()代表的是子view距离父View的顶部的距离,此时就乱了,所以,用上述的代码,保证是在父View中的唯一一个
子view。  
可能会有人说,如果把 MotionEvent.ACTION_MOVE: 中代码换成如下呢?
       int offsetX = rawX - mLastX4;
       int offsetY = rawY - mLastY4;
       ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
       layoutParams.leftMargin = layoutParams.leftMargin + offsetX;
       layoutParams.topMargin = layoutParams.topMargin + offsetY;
       setLayoutParams(layoutParams);         
这样不管是多少个view,也不管是第几个,它距离上面的view的margin值就可以了。这样看似解决了问题,但引出了新问题,以LinearLayout为例,它之后的view,都是以它为基础决定位置的,它一旦位移了,它后面的view都会随着它移动,形成关联移动的效果。

上面的说完了,我们再说滑动坐标系,为了 View 的滑动,View 提供了方法来实现这个功能,就是 scrollTo() 和 scrollBy(),

    private int mLastX = 0;
    private int mLastY = 0;
    private void onTouchEvent2(MotionEvent event) {
        int rawX = (int)event.getRawX();
        int rawY = (int)event.getRawY();

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = rawX - mLastX;
                int offsetY = rawY - mLastY;
                //View内容移动
                scrollBy(-offsetX,-offsetY);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        mLastX = rawX;
        mLastY = rawY;
    }
这里主要调用了scrollBy() 方法,通过它来控制内容移动,它的移动和上面的两种情况都不一样,调用这个方法时,view本身不会移动,内容会移动,但内容移动是在view自身的范围内移动,不会在父View的可绘制区域移动,内容一旦超过自身的区域,就不会显示,比较像是view本身的画布的移动。scrollBy()是调用了scrollTo()方法,通过控制 mScrollX 和mScrollY来控制画布,并且移动时会调用 onScrollChanged()回调方法,mScrollX 的值等于 view 左边缘和 view 内容左边缘在水平方向的距离,而 mScrollY 的值 等于 view 上边缘和view 内容上边缘在竖直方向的距离。假如 view 左边缘距离父View的距离是 x1, view 被调用了scrollBy() 方法,位移了,此时 内容左边缘距离父View的距离是 x2,那么mScrollX =x1 - x2。也就是说,如果scrollBy()中传的值是负数,则内容向右移动和向下移动,如果是正数,则向左和上运动,所以上述代码中 ACTION_MOVE 中的位移,偏移量是 offsetX 和offsetY,而我们传入的则是 scrollBy(-offsetX,-offsetY) 相反数。 这个scrollBy()有点特殊,上面的几种位移方法,都是正数往右和上,负数往左和下,这个则颠倒了过来。
scrollBy()是位移,上述代码由于是在move中执行,每次都是移动一点点,所以看起来是比较连贯的,但如果我们直接执行scrollBy(-60,-60),这时候我们看到控件view里的内容是一瞬间就过去了,像瞬移一样,如果我们想把 这一部分均匀的移动过去,怎么办?android 提供了一个辅助类 Scroller,可以进行拆分,让位移慢慢的过去。

        在init()中初始化点击事件
        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                scroller.startScroll(getScrollX(), getScrollY(), -10, -5, 1000);
                invalidate();
               
            }
        });
        
    Scroller scroller = new Scroller(getContext());
    
        @Override
    public void computeScroll() {
        // 判断Scroller是否执行完毕
        if(scroller.computeScrollOffset()){
            //本控件动
            scrollBy(scroller.getCurrX(),scroller.getCurrY());
            // 通过重绘来不断调用computeScroll
            invalidate();
        }
    }

这样,点击后,控件内容就在1秒钟内,移动了上述距离,这次是匀称的。写了个搞笑的,把 setOnClickListener 这个点击事件这一部分替换掉,还是用触摸事件
    private int mLastX5 = 0;
    private int mLastY5 = 0;
    private void onTouchEvent5(MotionEvent event) {
        int rawX = (int)event.getRawX();
        int rawY = (int)event.getRawY();

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                //计算移动的距离
                int offsetX = rawX - mLastX5;
                int offsetY = rawY - mLastY5;
                scroller.startScroll(0, 0, -offsetX, -offsetY, 1000);
                break;
            case MotionEvent.ACTION_UP:
                invalidate();
                break;
        }
        mLastX5 = rawX;
        mLastY5 = rawY;
    }
实际上 ACTION_MOVE 中只会最后一次执行scroller.startScroll(0, 0, -offsetX, -offsetY, 1000);方法,手指头抬起时,执行 invalidate() 方法,然后就是view的computeScroll()方法,和上面的例子类似。
 

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/89161963
今日推荐