Android Scroll分析

参考资料

郭霖 Scroller完全解析
鸿洋 ViewDragHelper完全解析
鸿洋 ViewDragHelper实战 自己打造Drawerlayout


-目录

  • 1)layout
  • 2)offsetLeftAndRight() offsetTopAndBottom()
  • 3)LayoutParams()
  • 4)scrollTo() scrollBy()
  • 5)Scroller
  • 6)属性动画
  • 7)ViewDragHelper

-实现滑动的7种方法

public class DragView extends View {
    private static final String TAG = "DragView";
    private int lastX, lastY;
    private Scroller scroller;

    public DragView(Context context, AttributeSet attrs) {
        super(context, attrs);
        scroller = new Scroller(context);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @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(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
                //方法二
//                offsetLeftAndRight(offsetX);
//                offsetTopAndBottom(offsetY);
                //方法三
//                LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
//                layoutParams.leftMargin = getLeft()+offsetX;
//                layoutParams.topMargin = getTop()+offsetY;
//                setLayoutParams(layoutParams);
                //方法四
                ((View)getParent()).scrollBy(-offsetX,-offsetY);

                break;
            case MotionEvent.ACTION_UP:
                View view =  (View)getParent();
                Log.i(TAG, "getScrollX: "+view.getScrollX());
                Log.i(TAG, "getScrollY: "+view.getScrollY());
                scroller.startScroll(view.getScrollX(),view.getScrollY(),-view.getScrollX(),-view.getScrollY());
                invalidate();
                break;
        }
        return true;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (scroller.computeScrollOffset()){
            Log.i(TAG, "getCurrX: "+scroller.getCurrX());
            Log.i(TAG, "getCurrY: "+scroller.getCurrY());
            ((ViewGroup)getParent()).scrollTo(scroller.getCurrX(),scroller.getCurrY());
            invalidate();
        }
    }
}

1) layout


2) offsetLeftAndRight() offsetTopAndBottom()


3) LayoutParams()

//使用MarginLayoutParams更加方便还不用考虑父布局是LinearLayout还是RelativeLayout
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft()+offsetX;
layoutParams.rightMargin = getRight()+offsetY;
setLayoutParams(layoutParams);

4) scrollTo() scrollBy()

任何一个控件都是可以滚动的,因为View类中有scrollTo()和scrollBy()两个方法,scrollBy()是让View相对于当前位置滚动某段距离,scrollTo()是让View相对于初始位置滚动某段距离。

scrollTo,scrollBy方法移动的是View的内容,如果ViewGroup中使用scrollTo,scrollBy,那么移动的将是所有子View。

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        layout = (LinearLayout) findViewById(R.id.layout);
        scrollToBtn = (Button) findViewById(R.id.scroll_to_btn);
        scrollByBtn = (Button) findViewById(R.id.scroll_by_btn);
        scrollToBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                layout.scrollTo(-60, -100); //注意此处是layout的scrollTo()
            }
        });
        scrollByBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                layout.scrollBy(-60, -100);//注意此处是layout的scrollBy()
            }
        });
    }

下图中为什么scrollBy(-60, -100),按钮确是向手机坐标系的x和y轴正向移动呢?
答:可以想象屏幕是一个放大镜,而下面是一个巨大的画布,使用scrollBy方法,将layout向X轴负方向(左)平移60,向Y轴负方向(上)平移100,则layout内的子view相当于向X轴和Y轴的正方向上移动了。

Android Scroll分析_第1张图片
20160110164232041.gif

5) Scroller

使用Scroller模仿ViewPager的例子

startScroll(int startX,int startY,int dx, int dy,int duration)
startScroll(int startX,int startY,int dx, int dy)
Android Scroll分析_第2张图片
20160114230048304.gif
/**
 * Created by 涂高峰 on 2017/6/21.
 */
public class ScrollerLayout extends ViewGroup {
    private static final String TAG = "ScrollerLayout";
    private Scroller mScroller;
    private int mDownX,mMoveX;
    private int leftBorder,rightBorder;
    private int mTouchSlop;
    public ScrollerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
        //大于这个距离,系统认为是移动
        mTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int count = getChildCount();
        for (int i=0; imTouchSlop){
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getRawX();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //如果子控件为Button之类的clickable控件,则会由button消费掉down事件,当viewgroup滑动时,会拦截move事件并处理
                //但是若子控件为TextView之类的非clickable控件,则viewgroup和textview都不会消费掉down事件.
                //由于没有任何view消费down事件,后续事件将由上层消费,而不会往下传递给viewgroup.所以此处需要将down事件消费掉,从而能继续接收后续事件
                return true;
            case MotionEvent.ACTION_MOVE:
                //偏移量
                int offsetX = mMoveX-x;
                //左边界处理
                if (getScrollX()+offsetX < leftBorder){
                    scrollTo(leftBorder,0);
                    return true;
                }
                //右边界处理
                if (getScrollX()+offsetX + getWidth()> rightBorder){
                    scrollTo(rightBorder-getWidth(),0);
                    return true;
                }
                //滑动处理
                scrollBy(offsetX,0);
                mMoveX = x;
                break;
            case MotionEvent.ACTION_UP:
                //手指抬起,判断是哪个子控件的index
                //小于第一个子控件的一半宽度则认为是第一个子控件
                //大于第一个子控件的一半宽度则认为是下一个子控件
                int index = (getScrollX()+getWidth()/2)/getWidth();
                Log.i(TAG, "index: "+index); //结果为  0  1  2
                //根据子空间index计算偏移量
                int dy = index * getWidth() - getScrollX();
                Log.i(TAG, "dy: "+dy);
                mScroller.startScroll(getScrollX(),0,dy,0);
                invalidate();
                break;
        }
        return super.onTouchEvent(event);
    }

    //重绘会调用此方法,此方法中的invalidate又会触发重绘,从而循环实现弹性滑动
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            invalidate();
        }
    }
}

6) 属性动画(动画中讲解)


7) ViewDragHelper

在自定义ViewGroup中,很多效果都包含用户手指去拖动其内部的某个View(eg:侧滑菜单等),针对具体的需要去写好onInterceptTouchEvent和onTouchEvent这两个方法是一件很不容易的事,需要自己去处理:多手指的处理、加速度检测等等。
好在官方在v4的支持包中提供了ViewDragHelper这样一个类来帮助我们方便的编写自定义ViewGroup

1)ViewDragHelper类相关的API:

方法 说明
create(ViewGroup forParent, ViewDragHelper.Callback cb) 创建viewDragHelper
captureChildView(View childView, int activePointerId) 捕获子视图
checkTouchSlop(int directions, int pointerId) 检查移动是否为最小的滑动速度
findTopChildUnder(int x, int y) 返回指定位置上的顶部子视图
flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) 解决捕获视图自由滑动的位置
getActivePointerId() 获取活动的子视图的id
getCapturedView() 获取捕获的视图
getEdgeSize() 获取边界的大小
getMinVelocity() 获取最小的速度
getTouchSlop() 获取最小的滑动速度
getViewDragState() 获取视图的拖动状态
isCapturedViewUnder(int x, int y) 判断该位置是否为捕获的视图
isEdgeTouched(int edges) 判断是否为边界触碰
setEdgeTrackingEnabled(int edgeFlags) 设置边界跟踪
settleCapturedViewAt(int finalLeft, int finalTop) 设置捕获的视图到指定的位置
smoothSlideViewTo(View child, int finalLeft, int finalTop) 滑动侧边栏到指定的位置
shouldInterceptTouchEvent(MotionEvent ev) 处理父容器是否拦截事件
processTouchEvent(MotionEvent ev) 处理父容器拦截的事件

2)ViewDragHelper.Callback相关API:

方法 说明
clampViewPositionHorizontal(View child, int left, int dx) 控制横轴的移动距离
clampViewPositionVertical(View child, int top, int dy) 控制纵轴的移动距离
getViewHorizontalDragRange(View child) 获取视图在横轴移动的距离
getViewVerticalDragRange(View child) 获取视图在纵轴的移动距离
onEdgeDragStarted(int edgeFlags, int pointerId) 处理当用户触碰边界移动开始的回调
onEdgeLock(int edgeFlags) 处理边界被锁定时的回调
onEdgeTouched(int edgeFlags, int pointerId) 处理边界被触碰时的回调
onViewCaptured(View capturedChild, int activePointerId) 当视图被捕获时的回调
onViewDragStateChanged(int state) 当视图的拖动状态改变的时候的回调
onViewPositionChanged(View changedView, int left, int top, int dx, int dy) 当捕获的视图位置发生改变的时候的回调
onViewReleased(View releasedChild, float xvel, float yvel) 当视图的拖动被释放的时候的回调
tryCaptureView(View child, int pointerId) 判断此时的视图是否为想要捕获的视图时会调用
getOrderedChildIndex(int index) 获取子视图的Z值
//方法的大致的回调顺序:

1)shouldInterceptTouchEvent:

DOWN:
    getOrderedChildIndex(findTopChildUnder)
    ->onEdgeTouched

MOVE:
    getOrderedChildIndex(findTopChildUnder)
    ->getViewHorizontalDragRange & 
      getViewVerticalDragRange(checkTouchSlop)(MOVE中可能不止一次)
    ->clampViewPositionHorizontal&
      clampViewPositionVertical
    ->onEdgeDragStarted
    ->tryCaptureView
    ->onViewCaptured
    ->onViewDragStateChanged

2)processTouchEvent:

DOWN:
    getOrderedChildIndex(findTopChildUnder)
    ->tryCaptureView
    ->onViewCaptured
    ->onViewDragStateChanged
    ->onEdgeTouched
MOVE:
    ->STATE==DRAGGING:dragTo
    ->STATE!=DRAGGING:
        onEdgeDragStarted
        ->getOrderedChildIndex(findTopChildUnder)
        ->getViewHorizontalDragRange&
          getViewVerticalDragRange(checkTouchSlop)
        ->tryCaptureView
        ->onViewCaptured
        ->onViewDragStateChanged

例子
1)任意移动
2)移动完毕后回到原位
3)边界移动时对View进行捕获(未成功。。)

Android Scroll分析_第3张图片
20150713095339390.gif
public class VDHDemo extends LinearLayout {
    private static final String TAG = "VDHDemo";
    private ViewDragHelper mDragger;

    private View mDragView;
    private View mAutoBackView;
    private Point mAutoBackOriPos = new Point();

    public VDHDemo(Context context, AttributeSet attrs) {
        super(context, attrs);
        //第二个参数为敏感度(sensitivity),敏感度越大mTouchSlop就越小
        //mTouchSlop为系统认为是移动的最小距离,即ViewConfiguration.get(context).getScaledPagingTouchSlop()
        mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                //返回true表示可以捕获该view,可根据第一个参数决定捕获哪个view
                //如: return xxView == child;
                return mDragView==child || mAutoBackView==child;
//                return true;
            }

            //边界控制
            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx) {
                final int leftBound = getPaddingLeft(); //左边界为viewgroup的paddingleft
                final int rightBound = getWidth() - leftBound - getPaddingRight() - 200; //200为子view的宽度

                final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
                return newLeft;
            }

            //边界控制
            @Override
            public int clampViewPositionVertical(View child, int top, int dy) {
                return top;
            }

            //手指释放时回调
            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
//                super.onViewReleased(releasedChild, xvel, yvel);
                //若为mAutoBackView,则回到初始位置,调用settleCapturedViewAt()
                //其内部为mScroller.startScroll(),别忘了invalidate和computeScroll
                //注意你拖动的越快,返回的越快
                if (releasedChild == mAutoBackView){
                    mDragger.settleCapturedViewAt(mAutoBackOriPos.x,mAutoBackOriPos.y);
                    invalidate();
                }
            }
            //如果子View不消耗事件,那么整个手势(DOWN-MOVE*-UP)都是直接进入onTouchEvent,
            // 在onTouchEvent的DOWN的时候就确定了captureView

            //如果消耗事件,那么就会先走onInterceptTouchEvent方法,判断是否可以捕获,
            // 而在判断的过程中会去判断另外两个回调的方法:getViewHorizontalDragRange和getViewVerticalDragRange,
            // 只有这两个方法返回大于0的值才能正常的捕获。
            @Override
            public int getViewHorizontalDragRange(View child)
            {
                return getMeasuredWidth()-child.getMeasuredWidth();
            }

            @Override
            public int getViewVerticalDragRange(View child)
            {
                return getMeasuredHeight()-child.getMeasuredHeight();
            }
        });
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mDragger.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mDragger.processTouchEvent(event);
        return true;
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mDragView = getChildAt(0);
        mAutoBackView = getChildAt(1);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        //onLayout结束后将mAutoBackView的返回原点设置为其初始的点
        mAutoBackOriPos.x = mAutoBackView.getLeft();
        mAutoBackOriPos.y = mAutoBackView.getTop();
    }

    @Override
    public void computeScroll() {
        if (mDragger.continueSettling(true)){
            invalidate();
        }
    }
}

你可能感兴趣的:(Android Scroll分析)