第五章 Android Scroll 分析

Android 坐标系

将屏幕的左上角的顶点作为Android坐标系的原点,从这个点向右是 x 轴正方向,向下是 y 轴正方向。

getRawX()getRawY()获得的坐标是Android坐标系上的坐标。

视图坐标系

描述子视图在父视图的位置关系,视图坐标系同样是从原点向右是 x 轴正方向,向下是 y 轴正方向。,原点不再是屏幕的左上角,而是父布局的左上角为坐标原点。

getX()getY()所获得的坐标是视图坐标系中的坐标。

触控事件 —— MotionEvent (P91)

getTop():获取到的是View自身的顶边到父布局的距离。
getLeft()getRight():。
getBottom():获取到的是View自身的底边到父布局的距离。
getX():获取点击事件距离控件左边的距离,即视图坐标。
getRawX():获取点击事件距离整个屏幕的左边的距离,即绝对坐标。
getScrollX():View 左边缘和 View 内容左边缘在水平方向上的距离。

控件滑动

public class DragView extends View {
    private static final String TAG = "DragView";
    private Scroller mScroller;
    public DragView(Context context) {
        this(context, null);
    }

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

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

    private void initData(Context context) {
        mScroller = new Scroller(context);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(getViewMeasuredWidth(widthMeasureSpec), getViewMeasuredWidth(heightMeasureSpec));
    }

    private int getViewMeasuredHeight(int heightMeasureSpec) {
        return 0;
    }

    private int getViewMeasuredWidth(int widthMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int result = 0;
        switch (widthMode){
            case MeasureSpec.EXACTLY:
                Log.i(TAG, "EXACTLY: ");
                break;
            case MeasureSpec.AT_MOST:  //wrap_content
                Log.i(TAG, "AT_MOST: ");
                result = 200;
                result = Math.min(result, widthSize);
                break;
        }
        return result;
    }

    private int lastX;
    private int lastY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        /**
         * 通过layout方法使得View达到滑动效果
         *   getX() 获取的是点击事件距离控件左边的距离,即视图坐标
         *   getRawX() 获取的是点击事件距离整个屏幕左边的距离,即绝对距离
         */
        //textGetXY(event);
        //textGetRawXY(event);
        /**
         * 通过使用offsetLeftAndRight(offsetX)使得View达到平移的效果
         */
        //textOffset(event);
        /**
         * 使用LayoutParams使得view达到平滑效果
         */
        //textLayoutParams(event);
        /**
         * 使用scrollBy使得view达到平滑效果
         */
        //textScrollToOrBy(event);

        textScrollSlowly(event);

        return true;
    }

    private void textScrollSlowly(MotionEvent event) {
        int startX = (int) event.getRawX();
        int startY = (int) event.getRawY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = startX - lastX;
                int offsetY = startY - lastY;
                ((View)getParent()).scrollBy(-offsetX, -offsetY);
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_UP:
                View viewGroup = (View) getParent();
                /**
                 * getScrollX(): 总是等于View左边缘和View内容左边缘在水平方向上的距离
                 *      要注意正负情况和ScrollBy()、ScrollTo()是一样的
                 *
                 *  viewGroup.getScrollX()获得的是当前viewGroup左边界和ViewGroup内容左边缘在水平方向上的距离,
                 *              也就是当前view左边界和ViewGroup左边界的距离,如果view没有滑动前在(0,0),那么现在就是滑动后的坐标的相反数
                 *
                 *     -viewGroup.getScrollX(), -viewGroup.getScrollY() 让滑动后的view再回原来位置
                 */
                mScroller.startScroll(viewGroup.getScrollX(), viewGroup.getScrollY(), -viewGroup.getScrollX(), -viewGroup.getScrollY());
                invalidate();
                break;
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

    }

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

    private void textScrollToOrBy(MotionEvent event) {
        int startX = (int) event.getRawX();
        int startY = (int) event.getRawY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = startX - lastX;
                int offsetY = startY - lastY;
                /**
                 * 并不能移动,因为scrollTo、scrollBy移动的是view的内容
                 */
                //scrollBy(offsetX, offsetY);
                /**
                 * 移动了ViewGroup中的所有子view
                 *
                 * scrollBy :视图移动的知识,相当于:view控件固定在手机屏幕上,而屏幕下是一个巨大的画布,也就是我们想要展示的视图,
                 *                  scrollBy仅仅是移动了view控件和手机屏幕,而view中的文本是画在画布上的,没有随着控件和手机屏
                 *                  幕滑动。     因此,要用-offsetX, -offsetY
                 */
                ((View)getParent()).scrollBy(-offsetX, -offsetY);
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
    }


    private void textLayoutParams(MotionEvent event) {
        int startX = (int) event.getRawX();
        int startY = (int) event.getRawY();
        ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = startX - lastX;
                int offsetY = startY - lastY;
                /**
                 * 在这里使用getLeft()使得view滑动紊乱  layoutParams.leftMargin才行
                 */
                layoutParams.leftMargin = layoutParams.leftMargin + offsetX;
                layoutParams.topMargin = layoutParams.topMargin + offsetY;
                setLayoutParams(layoutParams);
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
    }

    /**
     *  offsetLeftAndRight(offsetX)、offsetTopAndBottom(offsetY)
     *  相当于对左右、上下平移的一个封装
     * @param event
     */
    private void textOffset(MotionEvent event) {
        int startX = (int) event.getRawX();
        int startY = (int) event.getRawY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = startX - lastX;
                int offsetY = startY - lastY;
                offsetLeftAndRight(offsetX);
                offsetTopAndBottom(offsetY);
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
    }

    /**
     * 使用getRawX()的时候,每次执行完ACTION_MOVE需要对lastX重新赋值,为什么?
     *    因为getRawX()获取的是点击事件距离整个屏幕左边的距离,即绝对距离,在控件滑动的时候,lastX时刻发生变化
     * @param event
     */
    private void textGetRawXY(MotionEvent event) {
        int startX = (int) event.getRawX();
        int startY = (int) event.getRawY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = startX - lastX;
                int offsetY = startY - lastY;
                layout(getLeft()+offsetX, getTop()+offsetY, getRight()+offsetX, getBottom()+offsetY);
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
    }

    /**
     * 使用getX()的时候不需要每次执行完ACTION_MOVE重新赋值,为什么?
     *     原因:由于ACTION_DOWN中的lastX是当前按压点到当前控件左边界的距离,
     *          而当手指移动一个像素,控件也应该移动一个像素,所以lastX应该是不变的,所以不需要重新赋值。
     */
    private void textGetXY(MotionEvent event) {
        int startX = (int) event.getX();
        int startY = (int) event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = startX;
                lastY = startY;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = startX - lastX;
                int offsetY = startY - lastY;
                layout(getLeft()+offsetX, getTop()+offsetY, getRight()+offsetX, getBottom()+offsetY);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
    }
}

ViewDragView–实现类似QQ侧边栏的效果

public class DragViewGroup extends FrameLayout {

    private static final String TAG = "DragViewGroup";

    private ViewDragHelper mViewDragHelper;
    private View mMainView,mMenuView;
    private int mWidth;

    public DragViewGroup(@NonNull Context context) {
        this(context, null);
    }

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

    public DragViewGroup(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initData();
    }

    private void initData() {
        /**
         * ViewDragHelper的构造方法是私有的
         */
        mViewDragHelper = ViewDragHelper.create(this, mCallback);
    }

    /**
     * 当布局的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 = getMeasuredWidth();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        /**
         * 把触摸事件传递给ViewDragHelper,这一步必不可少
         */
        mViewDragHelper.processTouchEvent(event);
        return true;
    }

    private ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {

        /**
         * 刚触摸屏幕的时候就调用该方法,用于判断何时开始检测屏幕,
         *     该例中当触摸到的View为mMainView的时候返回true,表示开始检测屏幕
         */
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return mMainView == child;
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            return super.clampViewPositionVertical(child, top, dy);
        }

        /**
         * 触摸屏幕的时候不断调用该方法
         */
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return left;
        }

        /**
         * 拖动结束后松开会调用该方法
         */
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            if (mMainView.getLeft() < mWidth/2){
                /**
                 * 与下面两个方法类似:
                 *     mScroller.startScroll(x, y, dx, dy);
                 *     invalidate();
                 *
                 *   查看下面两个方法的源码,内部也是调用的 mScroller.startScroll()、invalidate();
                 *  那么就得重写computeScroll()
                 */
                mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }else {
                mViewDragHelper.smoothSlideViewTo(mMainView, mWidth/3, 0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }
        }

        /**
         * 在用户触摸到view后回调
         */
        @Override
        public void onViewCaptured(View capturedChild, int activePointerId) {
            super.onViewCaptured(capturedChild, activePointerId);
            Log.i(TAG, "onViewCaptured: ");
        }

        /**
         * 拖曳状态发生改变的时候回调,整个过程只调用三次,如果单击的话调用两次
         *      1:手指刚开始按压屏幕拖拽的时候回调(1)
         *      2:手指刚离开屏幕的时候回调(2),离开后view停止滑动的时候回调(3)。
         *      3:手指一直按压在屏幕上的时候,此时就算停止拖动,或者停止后再拖动都不会调用该方法
         *
         * @param state
         */
        @Override
        public void onViewDragStateChanged(int state) {
            super.onViewDragStateChanged(state);
            Log.i(TAG, "onViewDragStateChanged: ");
        }

        /**
         * 拖动的时候View位置发生改变的时候回调该方法
         */
        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
            Log.i(TAG, "onViewPositionChanged: ");
        }
    };

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mViewDragHelper.continueSettling(true)){
            ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
        }
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值