好久沒有寫博客了,主要還是任務過多哈。在開發的過程當中,也記錄了很多東西,但是技術這個事吧,其實,時效性真的事非常強……就比如說,你昨天還津津樂道的一個難點解決方案,你過個幾天再回過頭去看它,就會有一種莫名的“輕視感”(不知道有沒有這個說法,反正大家自己體會吧……),覺得它也不多如此嘛。然后慢慢的就不知道分享什么了。也因此,趁着熱度還在,趕緊跟大家分享一下剛剛完成一個基於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不太了解的同學多多包含,自己多看點源碼,跑一遍。看到手機上真實的效果后,會激起很高的學習熱情的。
大家加油!