关于CoordinatorLayout的用法——复杂交互的克星

  好久没有写博客了,主要还是任务过多哈。在开发的过程当中,也记录了很多东西,但是技术这个事吧,其实,时效性真的事非常强……就比如说,你昨天还津津乐道的一个难点解决方案,你过个几天再回过头去看它,就会有一种莫名的“轻视感”(不知道有没有这个说法,反正大家自己体会吧……),觉得它也不多如此嘛。然后慢慢的就不知道分享什么了。也因此,趁着热度还在,赶紧跟大家分享一下刚刚完成一个基于CoordinatorLayout的比较复杂的交互逻辑。

  刚看到这个交互的时候,我也尝试了蛮多搜索的,但是很遗憾,结果就是只能自己搞啦~咱们先看一下效果图哈,先一饱眼福,再进行深究。另外也附上Github代码仓库,方便大家实际操作:

  https://github.com/wytings/SpecialTabIndicator

  

 

  看着效果还是蛮炫的,但是怎么做呢?下面慢慢给大家分析一下,我在开发这个交互的整个过程。主要分为两大部分:一、理论分析;二、技术点概况;三、技术实现细节

一、理论分析

  首先,一看到这个交互图。我首先想到是之前写的一篇:功能分解——Android下画分时图与k线图有感。我们要进行的第一步就是分解这个交互。

  1、顶部的TitleBar —— 背景从透明变黑、左边返回按钮白变蓝、右边loading progress 透明度0到1以及旋转;

  2、头部底层布局——可以切换的图片,有渐变色、有切换动画;

  3、头部下面的指示器——类似于普通的Indicator,但是selected和unselected的颜色会变化;

  4、底部ViewPager——ViewPager就没有什么说的了。

  以上就是我们分拆的4个大布局类,我们姑且把其序号当编号用,方便描述。分拆完了以后,我们再回过头去看看,他们各自的Layout行为表现:

  1号布局位置固定不变,颜色变化;

  2号布局位置固定不变,颜色和高度变化;

  3号布局位置、颜色变化;

  4号布局位置变化;

  然后就会发现、所有的变化都在回绕着2号布局的高度而发生改变。我们只需要让其他布局都去监听2号布局的高度变化,即可进行响应的改变了(当然这里面还有滑动冲突的解决)。有了这个思路后,即便自己写一套是不是感觉也明朗好多了?但是,令人更加雀跃的是,Android里面有一个布局是完全可以胜任这个事的,就是CoordinatorLayout

二、技术点概况

  尽管我们知道整体上是使用CoordinatorLayout来处理布局关系,但其实还是涉及了很多动画的改变,图标的变化等。我们要抽出一个个技术点来,才好进行具体的实施。接下来,我们要做的事就是这个。

  2.1、背景设置渐变色

  纯色背景的设置还是比较简单,渐变色的设置则需要进行的一定的处理。这里我想到的是 GradientDrawable。通过这个类,我们可以进行渐变背景的设置。这里比较建议代码操作而不是通过XML去操作,因为他们渐变逻辑是一样的,如果通过XM来进行,则需建立的XML文件较多。

  2.2、颜色渐变切换

  这里涉及到颜色切换的渐变,A颜色->B 颜色的过程得是逐渐改变而不是突变。通常这种逻辑主要涉及在动画处理。如果要单独处理则可以抽出其中的一个关键类:ArgbEvaluator。通过这个类,我们可以计算出两种颜色发生改变的中间值这个过程可调节的参数为0到1。

  2.3、PNG图标颜色渐变

  我们看到返回箭头其实是一个PNG图标,代码里面读取后就是一个Drawable,所以我们要对Drawable进行渐变处理。这里就涉及到一个叫TintColor的东西,通过设置它可以改变在绘制Drawable的时候的颜色。比如上面的返回图标其实是白色的,但是在绘制这个图标的时候,是可以改变颜色的。另外需要注意的是Drawable默认是系统复用的,所以需要进行mutate()一下,避免你在修改他属性的时候影响其他场景的使用。

  2.4、顶部图片切换动画

  这里没有使用ViewPager,是因为ViewPager切换动画比较局限。虽然ViewPager可以自定义PageTransformer进行自定义动画切换界面,但是依然很难满足上图的交互需求,尤其是要进行跨Tab切换的时候。那我们使用什么呢?我选择了ViewAnimationor。因为它可以自定义画面的进入还是出去,但是由于它是单纯为了显示,所以要自己添加手势的监听来支持左右切换。

  2.5、整个布局的摆放

  这个就涉及到了CoordinatorLayout的用法问题了。这个控件对布局的操作都依赖于CoordinatorLayout.Behavior,这个类是我们这个交互最核心的类。通过对子布局设置Behavior,可以决定其怎么展示。

三、技术实现细节

  上面我们已经大概说明了一些依赖的技术点。如果大家对以上所说的点有不熟悉的话,最好还是先去看看相关资料。这样对于接下来要说的,会更省心一些。另外,主要是挑几个要点讲不可能无脑贴代码。具体的代码实现,大家可以把Github上clone下来,自己慢慢看。

  首先,我们来看一下 GradientDrawable 这个类。

   /**
     * Create a new gradient drawable given an orientation and an array
     * of colors for the gradient.
     */
    public GradientDrawable(Orientation orientation, @ColorInt int[] colors) {
        this(new GradientState(orientation, colors), null);
    }

  通过构造方法,我们可以看出来,渐变色的实现还是比较简单的。

GradientDrawable gradient = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, new int[]{startColor, endColor});
view.setBackground(gradient);

  图标的颜色渐变切换就是支持取出Drawable,然后进行tint操作。

        Drawable wrappedDrawable = DrawableCompat.wrap(imageBack.getDrawable()).mutate();
        ColorStateList tint = ColorStateList.valueOf(currentColor);
        DrawableCompat.setTintList(wrappedDrawable, tint);

  我们来看一下上面的操作,首先取出返回按钮的Drawable,记得要mutate(),显性声明这个Drawable不再与其他地方进行分享,单独持有。

  第二步就是进行ColorStateList的创建,大家别看每次valueOf一下就会创建一个新的,里面是有缓存机制的。然后直接更新其TintList。

  最后我们再来看看大头的 CoordinatorLayout.Behavior。有两个方法 layoutDependsOn、onDependentViewChanged是影响其布局位置的。通过注释,我们也可以发现,第一个就是要声明,该Behavior附属的View依赖于哪一个View,在CoordinatorLayout遍历整个View节点的时候会不断回调,直到return true,告诉它这个view就是我的依赖。那么往后,这个Behavior里面所有方法中的但凡有dependency参数的都是这个return true声明的View。有点绕是吧…

  另外一个方法onDependentViewChanged就是在你声明依赖后,如果依赖的View发生位置或大小的变化时,就会通过这个回调进行通知,然后就可以进行相关View改变了。

  /**
         * Determine whether the supplied child view has another specific sibling view as a
         * layout dependency.
         *
         * <p>This method will be called at least once in response to a layout request. If it
         * returns true for a given child and dependency view pair, the parent CoordinatorLayout
         * will:</p>
         * <ol>
         *     <li>Always lay out this child after the dependent child is laid out, regardless
         *     of child order.</li>
         *     <li>Call {@link #onDependentViewChanged} when the dependency view's layout or
         *     position changes.</li>
         * </ol>
         *
         * @param parent the parent view of the given child
         * @param child the child view to test
         * @param dependency the proposed dependency of child
         * @return true if child's layout depends on the proposed dependency's layout,
         *         false otherwise
         *
         * @see #onDependentViewChanged(CoordinatorLayout, android.view.View, android.view.View)
         */
        public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }

        /**
         * Respond to a change in a child's dependent view
         *
         * <p>This method is called whenever a dependent view changes in size or position outside
         * of the standard layout flow. A Behavior may use this method to appropriately update
         * the child view in response.</p>
         *
         * <p>A view's dependency is determined by
         * {@link #layoutDependsOn(CoordinatorLayout, android.view.View, android.view.View)} or
         * if {@code child} has set another view as it's anchor.</p>
         *
         * <p>Note that if a Behavior changes the layout of a child via this method, it should
         * also be able to reconstruct the correct position in
         * {@link #onLayoutChild(CoordinatorLayout, android.view.View, int) onLayoutChild}.
         * <code>onDependentViewChanged</code> will not be called during normal layout since
         * the layout of each child view will always happen in dependency order.</p>
         *
         * <p>If the Behavior changes the child view's size or position, it should return true.
         * The default implementation returns false.</p>
         *
         * @param parent the parent view of the given child
         * @param child the child view to manipulate
         * @param dependency the dependent view that changed
         * @return true if the Behavior changed the child view's size or position, false otherwise
         */
        public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }

   最后一个就是,滑动冲突的处理,当然也是在Behavior里面操作的。我就不截图了。我们直接看具体的方法名:onStartNestedScroll、onNestedScrollAccepted、onNestedPreScroll、onNestedScroll、onNestedPreFling、onStopNestedScroll

 。是不是看着有点恐怖…是的,刚开始不熟悉的话会很痛苦,理解了就好了。注意这个方法名的循序就是一个滑动操作的通用过程。

  看看方法名,基本就知道是干嘛的了吧。需要注意的两点是:

  1、在onStarrtNestedScroll必须返回true,后续的方法才会收到剩余的Touch事件。然后就可以慢慢处理了。

  2、如果在嵌套滑动的时候,打算提前处理,如果还有没消耗完的距离,要记得告诉外面的View,避免生硬的滑动。就是onNestedPreScroll中的consumed.

  public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull ViewPager child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) 

其实,整个难点基本就没了…剩下的都是实打实的细节调整。另外,这篇不是入门级解说哈,像里面涉及的动画切换就不可能再分拆出来说了。否则,就光光那个ViewPager的Indicator都可以分拆出一个项目来进行说明了……

  这个还请那些对CoordinatorLayout不太了解的同学多多包含,自己多看点源码,跑一遍。看到手机上真实的效果后,会激起很高的学习热情的。

  大家加油!

 

posted @ 2018-03-15 19:30  RexWei  阅读(3735)  评论(0编辑  收藏  举报