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轴的正方向上移动了。

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)
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; i<count; i++){
            View child = getChildAt(i);
            measureChild(child,widthMeasureSpec,heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        for (int i=0; i<count; i++){
            View child = getChildAt(i);
            child.layout(i*child.getMeasuredWidth(), 0, (i+1)*child.getMeasuredWidth(), child.getMeasuredHeight());
        }
        leftBorder = getChildAt(0).getLeft();
        rightBorder = getChildAt(getChildCount()-1).getRight();
        Log.i(TAG, "leftBorder: "+leftBorder);
        Log.i(TAG, "rightBorder: "+rightBorder);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int x = (int) ev.getRawX();
        switch (ev.getAction()){
            case  MotionEvent.ACTION_DOWN:
                mDownX = x;
                mMoveX = x;
                break;
            case MotionEvent.ACTION_MOVE:
                //按下的坐标与当前移动坐标绝对值 大于 系统默认的移动距离
                //拦截此移动事件,不向子view传递,进入自身的onTouchEvent
                if (Math.abs(mDownX - x)>mTouchSlop){
                    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进行捕获(未成功。。)

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();
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 前言 本篇谈论Android Scroll的应用以及如何在应用中添加滑动效果。你可以学到: 发生滑动效果的原因 如...
    张文靖同学阅读 558评论 0 1
  • 链接 Android Scroll 分析 这是我重读《Android 群英传》的时候做的读书笔记,这里主要讲了 A...
    MrFu阅读 1,133评论 4 28
  • 内容是博主照着书敲出来的,博主码字挺辛苦的,转载请注明出处,后序内容陆续会码出。 当了解了Android坐标系和触...
    Blankj阅读 6,626评论 3 61
  • 概念 滑动是如何产生的 滑动一个VIew,本质上是移动一个View。移动一个View需要改变他的坐标,所以滑动一个...
    Reiser实验室阅读 284评论 0 0
  • 大家知道有一本书名字就叫孤独是生命的礼物。这个书名太贴近我心了,我享受孤独带给我的慰藉也享受着它的纯真。没错,孤...
    钱満満阅读 237评论 0 1