仿潮自拍个人中心拖拉效果

一、前言

一直都想跟大家分享一篇好的文章,但苦于心中的墨水,写出来都平平无奇。我自己也在反思是不是没有表达清楚了,本篇文章我会尽量详细的讲解。
好久没有文章更新了,是我欠大家的一份承诺,拖拉的习惯一直没有改掉,希望大家能够监督监督我,同时我也尽量克制自己的懒惰,及时分享自己的一些学习成果。

二、正文

习惯了先上效果图,再逐一解剖,图文并茂有助于大家的理解

1、效果图

demo.gif

录制的不是很清晰,文章的末尾会给出源码地址。
第一眼看到这个效果的时候我就想到了通过自定义View去实现(最终还真被我实现了七七八八的效果),简单的分析下自定义控件的基本需求有以下几种:

  1. 整个屏幕作为把手(可以拖拽的)进行拖拽
  2. 底部上拉布局有一定的高度限制,不一定覆盖整个屏幕(高度根据具体情况调整)
  3. 当从底部上拉一点点时抬手,布局缩回,若超过一定高度,自动展开到最大,下拉同理(根据具体情况设定阈值)
  4. 支持快速拖拽(效果同3)
  5. 根据布局滑动的偏移量,来控制其他 View 的动画效果
  6. 若 ViewPager 嵌套 Fragment + 滚动 View ,需要处理滑动冲突(若 [滚动View] 滑动到了顶部并且下滑的趋势大于左右的趋势,则让他父类拦截事件,反之则自己消费事件)

通过自定义控件的方式很难实现运动惯性的效果,总感觉有瑕疵,那么只能另寻方案。苦思冥想... 滑动...折叠...好像抓住了什么,对,就是折叠效果,一下让我想到了 design库下面的 CoordinatorLayout 视图,我相信大家对它并不陌生,是 Google IO/15 大会发布的,专门用来打造各种炫酷的效果,一般是结合 AppbarLayout, CollapsingToolbarLayout, Toolbar 来使用。如果你对 CoordinatorLayout 的使用还不是很熟悉,推荐浏览以下地址:

CoordinatorLayout 英文地址
CoordinatorLayout 中文地址(任玉刚)

网上有关 CoordinatorLayout,AppBarLayout 的文章太多了,我这里就不再赘述。

2、CoordinatorLayout的xml布局

xml

由于 xml 布局文件太长,查看布局请点击文章末尾的源码链接地址。

由 xml 布局当中可以看出 AppBarLayout(通过手势变化来控制子View的运动轨迹)作为 CoordinatorLayout 的第一个子View(孩子),CollapsingToolbarLayout 作为 AppBarLayout 的第一个 Child View

    <android.support.design.widget.CollapsingToolbarLayout
        android:id="@+id/collapsing"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:collapsedTitleGravity="left|top|start"
        app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
        app:layout_scrollInterpolator="@android:anim/linear_interpolator">

设置了 layout_scrollFlags 属性为:

app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"

layout_scrollFlags 的相关介绍 scroll ,exitUntilCollapsed 大家比较熟悉,snap 属性控制手指抬起后 Child View 要么向上全部滚进屏幕,要么向下全部滚出屏幕。

CollapsingToolbarLayout 的 Child View 都设置了如下属性:

app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="1.0"

parallax 滚动有视差效果,layout_collapseParallaxMultiplier="1.0" 相当于你滚动了多少,我就滚动多少。

接下来看一个小技巧:

    <android.support.design.widget.TabLayout
        android:layout_height="@dimen/4dp"
        app:tabIndicatorHeight="@dimen/4dp"

你会发现 TabLayout 的高度只设置了 4dp,刚好是指示器的高度。在最开始的实现中我把 TabLayout 的高度设置了 56dp ,发现 [作品] 区域的动画效果并不理想,所有后来就有了这个讨巧的办法 TabLayout 只显示指示器的高度,唯一麻烦的是需要监听 mTabLayout.addOnTabSelectedListener 来实现指示器左右移动的动效。

布局没什么难度,一起来看一下运行的效果:

![scroll
](http://upload-images.jianshu.io/upload_images/2258857-d751b4a523ed9445?imageMogr2/auto-orient/strip)
](http://upload-images.jianshu.io/upload_images/2258857-d751b4a523ed9445?imageMogr2/auto-orient/strip)

这里有一点需要注意的地方,如果你采用 ViewPager 的方式,每个 Fragment 布局最顶层是滚动 View 的话,那么滚动 View 必须继承于 NestedScrollingChild、NestedScrollingParent,不然滚动到最顶部 AppBarLayout 不会有下拉效果 。你可以查看 ViewCompat 类的源码进行了解

compat

从布局的效果图中可以看出,头像,[作品],名称区域直接滑出了屏幕,怎么才能实现缩放,平移,透明度的效果呢?接下来我以头像的运动效果来简单介绍下。

3、头像运动效果

拆分头像运动效果:

  1. 平移动画 X轴方向运动到左上角;Y轴方向也运动到左上角
  2. 缩放动画 X轴方向从 1.0 缩放到 0.5;Y轴方向也从 1.0 缩放到 0.5(这里是原点缩放,默认的是中心点缩放)

首先我们需要监听 AppBarLayout 滚动事件:

    mAppBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
        @Override
        public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
            //动画处理
        }
    });

参数 verticalOffset 表示垂直方向的偏移量

通过 getTotalScrollRange 获取总共可以滑动的范围(最大的偏移量)

appBarLayout.getTotalScrollRange() 

那么我们就可以拿到滑动比率 :

float ratio = Math.abs((float) verticalOffset / appBarLayout.getTotalScrollRange()); //[0~1]

头像的缩放范围是 [1~0.5] ,那么我们可以进行如下处理:

    mHeaderView.setPivotX(0);
    mHeaderView.setPivotY(0);
    mHeaderView.setScaleY(0.5f + 0.5f * (1.0f - ratio));
    mHeaderView.setScaleX(0.5f + 0.5f * (1.0f - ratio));

头像的平移动画也拆分两步

  1. 头像保持不动
  2. 在1的前提下进行平移

由于 CollapsingToolbarLayout 设置的子 View 的 layout_collapseParallaxMultiplier 的视差参数是1.0,那么视图向上移动的距离就是 verticalOffset 垂直方向上的偏移量,由于向上移动 Y 坐标在减小,为了保持位置不动就加上垂直方向的偏移量。

mHeaderView.setY(mHeaderStartY + Math.abs(verticalOffset));

mHeaderStartY 变量表示头像运动前的Y坐标。这样就可以使头像的位置固定不变,接下来在固定的基础上进行平移

   mHeaderView.setY(mHeaderStartY + Math.abs(verticalOffset) - (mHeaderStartY - mHeaderEndY) * ratio);
   
   mHeaderView.setX(mHeaderStartX - (mHeaderStartX - mHeaderEndX) * ratio);

mHeaderStartX 表示头像运动前的 X 坐标,mHeaderEndX,mHeaderEndY 分别是左上角的坐标分别为18dp,12dp

这样头像的运动效果就实现了,其他的效果类似。这里就不在赘述。有一点需要注意,[作品] 区域的字体大小变化并没有动态的改变 setTextSize 的值(滑动会发现字体抖动),采用的缩放的方式,缩放的区域为 [1.0~0.7],根据具体的情况而定。

其他效果完成之后,运行走一波。玩着玩着就会发现,为啥 RecyclerView 未滚动到顶部,下拉 [头像+作品] 区域,AppBarLayout 并不会向下滚动。但潮自拍的个人中心却可以滚动。对于追求卓越品质的我,怎么会允许这样的问题存在呢。

那么只能调试 CoordinatorLayout AppBarLayout 源码了,断点调试了 CoordinatorLayout onTouchEvent 方法发现并没有找到突破口。 苦思冥想 ... 冥想苦思 ...

突然想到了关联的滚动视图必须继承 NestedScrollingChild、NestedScrollingParent 接口,那么是搜索他两试试呢?一搜索还真被我找到了,当时内心的那份喜悦是无以言表的。

在 AppBarLayout 类里面搜索 NestedScrollingChild,直接定位到了以下代码:

private WeakReference<View> mLastNestedScrollingChildRef;

发现了一个加有弱引用的 View,继续跟踪(省略了初始化的地方),就定位到了下面这个方法:

        @Override
        boolean canDragView(AppBarLayout view) {
            if (mOnDragCallback != null) {
                // If there is a drag callback set, it's in control
                return mOnDragCallback.canDrag(view);
            }

            // Else we'll use the default behaviour of seeing if it can scroll down
            if (mLastNestedScrollingChildRef != null) {
                // If we have a reference to a scrolling view, check it
                final View scrollingView = mLastNestedScrollingChildRef.get();
                return scrollingView != null && scrollingView.isShown()
                        && !ViewCompat.canScrollVertically(scrollingView, -1);
            } else {
                // Otherwise we assume that the scrolling view hasn't been scrolled and can drag.
                return true;
            }
        }

代码很简单,重点看

ViewCompat.canScrollVertically(scrollingView, -1);

滑到最顶部时,返回 false 。那么 canDragView 就很清晰了,滚动 View 未滑动到顶部返回 false ;滑动到顶部则放回 true

继续跟踪 canDragView 在哪里被调用了

HeaderBehavior 类下的 onTouchEvent 方法 :

    case MotionEvent.ACTION_DOWN: {
        final int x = (int) ev.getX();
        final int y = (int) ev.getY();
        if (parent.isPointInChildBounds(child, x, y) && canDragView(child)) {
           //滚动到顶部  下拉执行这里
            mLastMotionY = y;
            mActivePointerId = ev.getPointerId(0);
            ensureVelocityTracker();
        } else {
        // 未滚动到顶部 下拉执行这里
            return false;
        }
        break;
    }

那么接着我们来看一看 HeaderBehavior 类的一个申明:

HeaderBehavior<V extends View> extends ViewOffsetBehavior<V>

Behavior 怎么这么熟悉呢?对了,我们的布局 ViewPager 不就也有一个 behavior 行为吗?

    <android.support.v4.view.ViewPager
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

查看 appbar_scrolling_view_behavior 字符串:

 <string name="appbar_scrolling_view_behavior" translatable="false">android.support.design.widget.AppBarLayout$ScrollingViewBehavior</string>

定位到 ScrollingViewBehavior 类:

behavior
 public static class Behavior extends HeaderBehavior<AppBarLayout>

共同继承了 ViewOffsetBehavior 类,产生了关联。回到 HeaderBehavior 类的 onTouchEvent 方法调试跟踪 RecyclerView 滚动到顶部与未滚动到顶部下拉 [头像+作品] 区域,断点刚好触发在这里,执行的情况如下:

 if (parent.isPointInChildBounds(child, x, y) && canDragView(child)) {
           //滚动到顶部  下拉执行这里
           //省略了其他代码
        } else {
        // 未滚动到顶部 下拉执行这里
            return false;
        }

而影响 canDragView(child) 的返回值正是如下代码:

ViewCompat.canScrollVertically(scrollingView, -1)

那么我们通过重写 RecyclerView 的 canScrollVertically 方法,来实现未滑动到顶部也可以实现下拉效果:

    @Override
    public boolean canScrollVertically(int direction) {
        return false;
    }

运行一下,发现跟潮自拍个人中心效果一样。

本篇到这里就要结束了,下一篇带个大家实现知乎列表图片的滚动视差效果。

源码地址

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

推荐阅读更多精彩内容