Scroll滑动分析-《Android群英传》第五章

本文选自《android群英传》第五章。主要是滑动方面知识,讲解了坐标系、MotionEvent剄七种滑动的实现方法。

1-滑动的产生

滑动一个View本质就是移动View,通过改变View的坐标去实现这个效果。这里就需要监听用户触摸的事件。下面包含两个部分:Android窗口坐标系和屏幕触控事件MotionEvent

1-Android坐标系

以屏幕左上角为原点,向右为X正方向,向下为Y轴正方向。

  • Android中通过getLocationOnScreen(intlocation[])能获得当前视图的左上角在Andriod坐标系中的坐标。

  • 触控事件中,getRawX()和getRawY(),获得的同样是android坐标系中的坐标

2-视图坐标系

是当前视图以父视图左上角为原点建立的坐标系,用于描述当前视图在父视图中的坐标。

  • 触控事件中通过getX()、getY()获得在视图坐标系中的坐标。

3-MotionEvent

一共有几种常用事件常量:ACTION_DOWN单点按下\UP单点离开\MOVE单点移动\CANCEL动作取消\OUTSIDE动作超出边界\POINTER_DOWN多点触摸按下\POINTER_UP多点触摸离开

一般情况通过onTouchEvent中的event.getAction()来获取事件类型的常量。

系统有很多获取坐标值和相对距离的方法。主要分为两类。

View提供的获取坐标的方法

getTop:View的顶部到父控件顶边的距离。
getLeft/Right/Bottom:对应View的左边/右边/底部分别到父控件左边/右边/底部的距离。

MotionEvent提供的方法

getX()获得点击事件event距离控件左边的距离。视图坐标。
getY()获得点击事件event距离控件顶部的距离。视图坐标。
getRawX()/RawY()获得点击事件距离整个屏幕左边/顶部的距离。绝对坐标。

2-实现滑动

1-Layout实现滑动

View控件有layout决定View的位置。在View控件的onTouchEvent()方法中去获得控件滑动前后的偏移。通过layout方法去重新设置。
自定义View

public class ScrollByLayoutView extends AppCompatImageView{
    float downX;
    float downY;

  /**
   * 三个构造函数千万不能少
   */
    public ScrollByLayoutView(Context context) {
        super(context);
    }
    public ScrollByLayoutView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public ScrollByLayoutView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

   /**
   * 进行偏移计算,之后调用layout
   */
    public boolean onTouchEvent(MotionEvent event) {
        float curX = event.getX(); //手指实时位置的X
        float curY = event.getY(); //Y
        switch(event.getAction()){
            case MotionEvent.ACTION_DOWN:
                downX = curX; //按下时的坐标
                downY = curY;
                Log.i("ScrollByLayoutView", "onTouchEvent: ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = (int)(curX - downX); //X偏移
                int offsetY = (int)(curY - downY); //Y偏移
                //用getLeft得到当前控件距离父控件左边的距离+偏移量,得到变化后的距离,然后调用layout
                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
                Log.i("ScrollByLayoutView", "onTouchEvent: ACTION_MOVE");
                break;
        }
        return true;
    }
}

使用控件:

<com.example.xxx.ScrollByLayoutView
        android:id="@+id/scroll_way1_imageview"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/jide"/>

这样就实现了滑动,对于offset偏移值的计算,也可以使用event的getRawX/Y来计算。

  • 特别注意:使用绝对坐标需要在每次调用layout之后重新设置初始值(downX = curX)

2-offsetLeftAndRight和offsetTopAndBottom

直接替换layout

//对left和right, top和bottom同时偏移
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);

3-LayoutParams

利用布局的参数来移动View控件。

//方法三:通过布局设置在父控件的位置。但是必须要有父控件, 而且要指定父布局的类型,不好的方法。
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
//方法四:用ViewGroup的MarginLayoutParams的方法去设置marign
// 相比于上面方法, 就不需要知道父布局的类型。
// 缺点:滑动到右侧控件会缩小
ViewGroup.MarginLayoutParams mlayoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
mlayoutParams.leftMargin = getLeft() + offsetX;
mlayoutParams.topMargin = getTop() + offsetY;
setLayoutParams(mlayoutParams);

4-scrollTo\scrollBy

scrollTo去移动到制定坐标
scrollBy表示移动的增量

  • 这两个方法是移动View的内容,因此需要在View的父控件中调用。
//方法五:scrollTo/scrollBy,在父控件中调用来操作父控件内部的控件
                ((View)getParent()).scrollBy(offsetX, offsetY);

然而结果是错误的,因为滑动的参考物并不是之前的。
这里的移动类似于移动了玻璃,所以View控件移向了完全相反的地方。需要取反。

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

5-Scroller

和scrollTo/By比较类似,但是去别在于scrollTo/By的位移是瞬间完成的。而Scroller却是平滑移动的。减少了突兀感。

这里实现一个效果,就是滑动后,控件自动返回最初始位置,这里在ACTION_UP中实现。
三个步骤:
1.初始化scroller
自定义View构造中初始化。

Scroller mScroller;

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

2.重载自定义View的computeScroll

public void computeScroll() {
        super.computeScroll();
        //判断scroller是否执行完毕。
        if(mScroller.computeScrollOffset()){
            ((View)getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            //通过重绘来不断调用 computeScroll
            invalidate();
        }
}

判断是否执行完毕

3.ACTION_UP中将View滑动回初始位置

case MotionEvent.ACTION_UP:
      View viewGroup = (View) getParent();
      mScroller.startScroll(viewGroup.getScrollX(), viewGroup.getScrollY(),
                        -viewGroup.getScrollX(), -viewGroup.getScrollY());
      invalidate();
       break;

6-属性动画

参考动画章节

7-ViewDragHelper

Google在其support库中为我们提供了一个DrawerLayout和SlidingPaneLayout两个布局来帮助开发者实现侧滑效果,这两个布局,大大的方便了我们自己创建自己的滑动布局,然而,这两个强大的布局背后,却隐藏着一个鲜为人知,却功能强大的类——ViewDragHelper,通过ViewDragHelper,基本可以实现各种不同的侧滑,拖放需求,因此这个方法也是各种滑动解决方案的终极绝招。

QQ侧滑菜单
public class DragViewGroup extends FrameLayout {

    //侧滑类
    private ViewDragHelper mViewDragHelper;
    private View mMenuView,mMainView;
    private int mWidth;

    public DragViewGroup(Context context) {
        super(context);
        initView();

    }

    public DragViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public DragViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    /**-------------------------------------------
     * 1、初始化数据:调用ViewDragHelper.create方法
     * ------------------------------------------*/
    private void initView() {
        mViewDragHelper = ViewDragHelper.create(this,callback); //需要监听的View和回调callback
    }

    /**-------------------------------
     * 2、事件拦截和触摸事件全部交给ViewDragHelper进行处理
     * ------------------------------*/
    //事件拦截
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }
    //触摸事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将触摸事件传递给ViewDragHelper

        mViewDragHelper.processTouchEvent(event);

        return true;
    }

    /**--------------------------------------------
     * 3、也需要重写computeScroll()
     *    内部也是通过scroller来进行平移滑动, 这个模板可以照搬
     * -------------------------------------------*/
    @Override
    public void computeScroll() {
        if(mViewDragHelper.continueSettling(true)){
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    /**------------------------------
     * 4、处理的回调:侧滑回调
     * ----------------------------*/
    private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {

        /*-------------------------------
        * 何时开始触摸:
        *  1.指定哪一个子View可以被移动.
        *  2.如果直接返回true,在该布局之内的所有子View都可以随意划动
        * ------------------------------*/
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            //如果当前触摸的child是mMainView开始检测
            return mMainView == child;
        }

        /*-------------------------------
        * 处理水平滑动:
        *  1. 返回值默认为0,如果为0则不处理该方向的滑动。
        *  2. 一般直接返回left,当需要精准计算pading等值时,可以先对left处理再返回
        * ------------------------------*/
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return left;
        }

        /*-------------------------------
        * 处理垂直滑动:
        *  1. 返回值默认为0,如果为0则不处理该方向的滑动。
        *  2. 一般直接返回top,,当需要精准计算pading等值时,可以先对left处理再返回
        * ------------------------------*/
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            return 0;
        }

        /*---------------------------------------------
        *  拖动结束后调用,类似ACTION_UP。
        *   这里是实现侧滑菜单,一般滑动可以不用这段代码
        * ---------------------------------------------*/
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            //手指抬起后缓慢的移动到指定位置
            if(mMainView.getLeft() <500){
                //关闭菜单
                mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }else{
                //打开菜单
                mViewDragHelper.smoothSlideViewTo(mMainView,300,0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }
        }
    };

    /**---------------------------------------------------
     * 5、获取子控件用于处理
     *  1. 上面完成了滑动功能,这里简单的按照第1、2的顺序指定子控件View的内容
     *  2. onSizeChanged能够获得menu等子控件的宽度等信息,有需求可以后续处理
     * ----------------------------------------------*/
    //XML加载组建后回调
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mMenuView = getChildAt(0);
        mMainView = getChildAt(1);
    }


    //组件大小改变时回调
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = mMenuView.getMeasuredWidth();
    }
}

使用(作为父控件,里面依次放menu和main):

    <com.example.xxxx.DragViewGroup
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/colorAccent"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/colorPrimary"/>

    </com.example.xxxx.DragViewGroup>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

猎羽

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

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

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

打赏作者

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

抵扣说明:

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

余额充值