View 的滑动学习

目的

这篇文章主要对 View 的滑动进行学习,主要包括滑动的实现方式和弹性滑动提高用户体验


(一)View 的滑动

滑动的方式基本思想都是类似的:
当触摸事件传到View时,系统记下触摸点的坐标,手指移动时系统记下移动后的触摸的坐标并算出偏移量,并通过偏移量来修改View的坐标。

View 实现滑动的几种方式:

  • 第一种,layout()方法
  • 第二种,offsetLeftAndRight() 与 offsetTopAndBottom()
  • 第三种,通过改变 View 的 LayoutParams 使得 View 重新布局从而实现滑动
  • 第四种,通过动画给 View 施加平移效果来实现滑动
  • 第五种,通过 View 本身提供的 scrollTo/scrollBy 方法来实现滑动
1. layout() 方法

View 进行绘制的时候会调用 onLayout() 方法来设置显示的位置,因此我们同样也可以通过修改 View 的属性来控制 View 的坐标

public class RedView extends View {

    int lastX, lastY;

    public RedView(Context context) {
        super(context);
    }

    public RedView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

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

    @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方法重新放置它的位置
                layout(offsetX + getLeft(), offsetY + getTop(), offsetX + getRight(), offsetY + getBottom());
                break;
        }
        return true;
    }
}
2. offsetLeftAndRight() 与 offsetTopAndBottom()

将 ACTION_MOVE 中的代码替换

    case MotionEvent.ACTION_MOVE:
        //计算移动的距离
        int offsetX = x - lastX;
        int offsetY = y - lastY;
        //对 left 和 right 进行偏移
        offsetLeftAndRight(offsetX);
        //对 top 和 bottom 进行偏移
        offsetTopAndBottom(offsetY);
        break;
3. LaytouParams(改变布局参数)

LayoutParams 主要保存了一个 View 的布局参数,因此我们可以通过改变参数从而达到改变 View 位置的效果

    case MotionEvent.ACTION_MOVE:
        //计算偏移量
        int offsetX = x - lastX;
        int offsetY = y - lastY;
        
        LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
        layoutParams.leftMargin = getLeft() + offsetX;
        layoutParams.topMargin = getTop() + offsetY;
        setLayoutParams(layoutParams);
        break;

需要注意的是 LinearLayout 是 View 的父容器,如果父容器是 RelativeLayout,则要使用 RelativeLayout.LayoutParams
当然我们可以使用 ViewGroup.MarginLayoutParams 来实现,一劳永逸

    case MotionEvent.ACTION_MOVE:
        //计算偏移量
        int offsetX = x - lastX;
        int offsetY = y - lastY;
        
        ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
        layoutParams.leftMargin = getLeft() + offsetX;
        layoutParams.topMargin = getTop() + offsetY;
        setLayoutParams(layoutParams);
        break;
4. 动画

可以采用 View 动画来移动,在 res 目录新建 anim 文件夹并创建 translate.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true">
    <translate
        android:duration="1000"
        android:fromXDelta="0"
        android:toXDelta="300" />
</set>
  • android:fillAfter="true" View 平移后停留在最后的位置,如果为 false 则会返回起始位置
  • android:duration="1000" 动画的时间为 1000 ms
  • android:toXDelta="300" 平移的距离,300 像素
mView.setAnimation(AnimationUtils.loadAnimation(this, R.anim.translate));

在 Java 代码中执行动画,这里所用的是补间动画,虽然 View 位置发生改变,但是对系统来说,View 并没有发生改变,点击事件并不会发生。Android3.0 之后,可以通过属性动画来解决这个问题

ObjectAnimator.ofFloat(mView, "translationX", 0, 300).setDuration(1000).start();
5. scrollTo 与 scrollBy

scollTo(x, y) 表示移动到一个具体的坐标点,而 scollBy(dx, dy) 则表示移动的增量为 dx、dy。
两个方法区别: scrollBy() 相对移动,scrollTo() 绝对移动

其中 scollBy 最终也是要调用 scollTo 的。scollTo、scollBy 移动的是View的内容,如果在 ViewGroup 中使用则是移动他所有的子 View。我们将 ACTION_MOVE 中的代码替换成如下代码:

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

这里要实现 mView 随着我们手指移动的效果的话,我们就需要将偏移量设置为负值。

在View内部有两个属性 mScrllX 和 mScrollY,分别可以通过 getScrollX() 和 getScrollY() 方法得到

在滑动过程中,mScrollX 总是等于 View 左边缘和 View 内容左边缘在水平方方向的距离;mScrollY 总是等于 View 上边缘和 View 中内容上边缘在竖直方向的距离。View 边缘是指 View 的位置也就是 View 的四个顶点到父容器的距离,View 内边缘是内容距离 View 四边的距离。

无论是 scrollTo() 还是 scrollBy() 都只能改变 View 内容的位置而不能改变 View 在布局中的位置

mScrollX/Y 单位为像素 px。当 View 左边缘在 View 内容左边缘右边时,mScrollX 为正值,反之为负值;同理,当 View 上边缘在 View 内容上边缘下边时,mScrollX 为正值,反之为负值。也就是说,View 从左向右滑动,mScrollX 为负值,反之为正值;从上往下滑动,mScrollY 为负值,反之为正值


mScrollX 和 mScrollY 变换规律
各种滑动方式的对比
  • 改变布局参数:操作复杂,适用于有交互的 View
  • 属性动画:操作简单,适用于没交互 View 和实现复杂的动画效果
  • scrollTo/By:操作简单,适合对 View 内容的滑动

(二)弹性滑动

在我们进行滑动时,这个过程是瞬间完成的,所以用户体验不大好,这里我们可以添加有过渡的滑动

View 弹性滑动的几种方式

  • 第一种:Scroller
  • 第二种:通过动画

这几种的方式有一个共同的思想:将一次大的滑动分成若干次小的滑动并在一个时间段内完成

1. Scroller

Scroller 本身是不能实现 View 的滑动的,它需要与 View 的 computeScroll() 方法配合才能实现弹性滑动的效果

  1. 初始化Scroller对象
  2. 重写View的computeScroll()方法
  3. 调用mScroller.startScroll()方法
    public RedView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mScroller = new ScrollView(context);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {  //判断Scroller是否执行完毕
            ((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

    //缓慢的滑到指定位置
    public void smoothScrollTo(int destX, int destY) {
        int deltaX = destX - getScrollX();  
        int deltaY = destY - getScrollY();  
        //2000ms 内滑到向(deltaY,deltaY)的位置,效果就是慢慢滑动
        mScroller.startScroll(0, 0, deltaX, deltaY, 2000);
        invalidate();
    }

在Activity中调用:

mView.smoothScrollTo(-400, -400);

Scroller 本身并不能实现 View 的滑动,它需要配合 View 的 computeScroll 方法才能完成弹性滑动的效果,它通过不断地让 View 重绘,而每一次重绘距滑动起始时间间隔,通过这个时间间隔 Scroller 就可以得出 View 当前的滑动位置,知道了滑动位置就可以通过 scrollTo 方法来完成 View 的滑动。就这样,View 的每一次重绘都会导致 View 进行小幅度的滑动(弹性滑动的核心思想:将一次大的滑动分成若干次小的滑动并在一个时间段内完成),而多次的小幅度滑动就组成了弹性滑动,这就是 Scroller 的工作机制。

2. 通过动画

动画本身就是一种渐进的过程,因此通过它来实现的滑动天然就是具有弹性效果:

ObjectAnimator.ofFloat(mView, "translationX", 0, 100).setDuration(1000).start();

总结

滑动做为 View 最基本的行为,一切的华丽的、绚丽的 UI,归根结底都是建立在其基础上


最后

学习资料主要来源于《Android开发艺术探索》和《Android进阶之光》,对进阶学习的一个记录与总结,如果有幸能对大家有所帮助,那将荣幸之至。

下一章,将对 View 的分发机制进行学习

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