這篇文章是實現InstaMaterial的一部分,今天我們將仔細講解上篇文章中跳過的細節。也就是說我們實現的還是視訊中9-13秒這個時間段。
這是今天這篇文章完成之後的最終效果(棒棒糖以及棒棒糖之前):是youtube視訊,沒法看。暫時連不上。。。
初始化
沒有什麼大書特書的,我們只需為feed卡片元素中的按鈕(喜歡以及評論按鈕)加上圖示就可以了。這一步的程式碼提交在這裡this commit.完了之後,我們還是不忙著去實現新的東西(新的UI元素),還需要、、、
修正bug以及優化效能
是啊,即便是小如InstaMaterial 這樣的demo級應用,你也總能找到提升的空間。
Toolbar theme
首先我們遺漏了Toolbar的樣式,這就是為什麼menu按鈕(Toolbar左邊的按鈕)的按下顏色是預設的深色。如下:
(作者的目的是做的和視訊一模一樣,即便是顏色的深淺,個人認為沒必要這麼較真是吧)
下載按鈕(ToolBar右邊的按鈕)的按下顏色是淺色的,因為它是使用的帶selector的自定義view,selector的定義如下
menu_item_view.xml
1 |
android:background="@drawable/btn_default_light" |
但是這隻在Lollipop上有效果(這裡翻譯可能有誤,原文是This inconsistency appears only in Android Lollipop ),解決的辦法很簡單。只需在activity_comments.xml 和activity_main.xml的ToolBar控制元件中加上一行程式碼:
1 |
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" |
注:在toolbar中是這樣使用的:
1 2 3 4 5 6 7 8 |
<android.support.v7.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/tools" android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" android:elevation="@dimen/default_elevation" app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> |
這樣Toolbar上的所有元素都將有著繼承自Dark.ActionBar主題的樣式。
順便說下,如果你對android主題和樣式的定義感興趣,想知道他們的區別,這篇文章值得一讀-Styling Views on Android (Without Going Crazy).
按照上面的做了之後,menu的按下效果看起來就是這個樣子了(只在Lollipop 中):
RecyclerView 元素的預載入
另一個問題是在app啟動之後feed列表的滾動不太順滑。幾乎每次在滾到第二個卡片的時候都有卡頓。幸好,造成這個問題的原因簡單。RecyclerView (以及其他基於adapter的view,比如ListView、GridView等)使用了快取機制重用子view(簡而言之就是,系統只將螢幕可見範圍之內的元素儲存在記憶體中,在滾動的時候不斷的重用這些記憶體中已經存在的view,而不是新建view)。
這個機制在我們這裡會導致一個問題,啟動應用之後,在螢幕可見範圍內,我們只有一張卡片可見(估計作者的螢幕比較小),當我們滾動的時候,RecyclerView找不到可以重用的view了,它將建立一個新的,因此在滑動到第二個feed的時候就會有一定的延時,但是第二個feed之後的滾動是流暢的,因為這個時候RecyclerView已經有能重用的view了。
如何解決這個問題?
好在本例使用的是LinearLayoutManager ,因此很簡單。只需重寫getExtraLayoutSpace()方法。根據官方文件的描述getExtraLayoutSpace將返回LayoutManager應該預留的額外空間(顯示範圍之外,應該額外快取的空間)。
1 2 3 4 5 6 |
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this) { @Override protected int getExtraLayoutSpace(RecyclerView.State state) { return 300; } }; |
下面是實際效果:
在重寫getExtraLayoutSpace()之前
重寫之後:
Feed 卡片上的按鈕
下面就讓我們開始卡片上那些按鈕(目前只有喜歡和評論按鈕)的工作吧.從視訊中的效果來看是非常讚的,圓形擴散的selector
Android Lollipop中的Ripple效果
按鈕所使用的Selector無非就是棒棒糖中介紹的Ripple效果-一種水波擴散效果。已經有無數關於Ripple效果的文章,這裡就不贅述了,我只給出兩篇文章的連結:
- Ripples – Part 1 and Ripples – Part 2 from Styling Android blog
- Implementing Material Design in Your Android app from official Android Developers Blog
我們專案中實現Ripple的方法是非常簡單的:
在feed_item.xml中將下面的drawable作為按鈕的背景
res/drawable-v21/btn_feed_action.xml
不要在L之前的裝置上使用ripples效果
我承諾過我將實現概念視訊中的所有效果,但是有時候,去做一件達不到預期的事情,還不如不做。在pre-21的裝置上實現ripple就是一件達不到預期的事情。
我當然知道可以使用諸如mimic ripple effect 這樣的庫來相容老裝置,但是沒有一個庫達到了該有的效果。它們需要新增額外的程式碼(比如新增額外的佈局來包裹),在Lollipop版本上無法使用原生的Ripple 效果,效能問題等等。
But why is so hard to copy Ripple effect into pre-21 Android?
但是為什麼在pre-21的裝置上覆制Ripple 效果會這麼難呢?
Ripple揭祕
在棒棒糖版本之前,整個UI都是在UI主執行緒中管理的。幾乎每個人都知道ANR對話方塊,NetworkOnMainThreadException,我們也是知道“不要將耗時操作放在UI執行緒中,僅僅在UI執行緒中顯示操作結果”這條黃金定律。一切都看似可行,除了那句“整個UI都是被UI主執行緒所管理的”。
隨著app的佈局日益複雜,UI需要更多的時間去繪製、測量。現在問題來了,如果我們的動畫執行到一半,而開啟了另外一個UI任務(比如為新的activity inflat佈局),但是隻有一個UI執行緒,那麼動畫將被停止。
Lollipop中所引入的Render執行緒將解決這個問題。Render執行緒通過將渲染分成兩部分來解決,簡單的來說就是:我們有一個被UI執行緒建立的動畫單元的列表,這些動畫將被甩到獨立的render執行緒當中。因此在執行開銷較大的UI操作的時候,動畫也能繼續下去。
這就是ripple效果的工作原理。他們是在render執行緒中執行的。所以不會被開啟新的activity這樣的操作打斷。
所以沒法在pre-21的安卓系統上完全實現ripple效果。
老版本上的相容“ripple”效果
因為不能在pre-21上實現ripple,那麼我們就實現類似的效果就可以了。我們建立了一個帶進入退出漸變的圓形的selector,儘管沒有ripple那麼花哨,但是看起來還是可以.
res/drawable/btn_feed_action.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?xml version="1.0" encoding="utf-8"?> <!--drawable/btn_feed_action.xml--> <selector xmlns:android="http://schemas.android.com/apk/res/android" android:enterFadeDuration="200" android:exitFadeDuration="200"> <item android:state_pressed="false"> <shape android:shape="oval"> <solid android:color="@android:color/transparent" /> </shape> </item> <item android:state_pressed="true"> <shape android:shape="oval"> <solid android:color="#6621425d" /> </shape> </item> </selector> |
評論釋出按鈕
評論釋出按鈕非常有趣。正如你在視訊中看到的,按鈕可以在兩個狀態之間做簡單的動畫切換,並且點選的時候有ripple效果。
注:評論釋出按鈕對應的類是SendCommentButton.java,這一節中講解的內容都是在這個類中。結合程式碼更容易看明白。
關於這個按鈕SendCommentButton,我將用到下面幾個android元素:
ViewAnimator:作為SendCommentButton的基類,它有一個對我們非常有用的特性,可以設定子view切換時的進入和退出效果。如果你對ViewAnimator很陌生,其直接子類ViewFlipper, ViewSwitcher或間接子類ImageSwitcher, TextSwitcher應該很熟悉。
自定義view:包括xml以及inflate xml的程式碼
<merge>標籤消除冗餘的view
實現
好了,現在來開始實現,從動畫開始。實際上我們需要四個動畫,傳送狀態兩個(進入和退出),完成狀態兩個(進入和退出,不過是相反的方向):
傳送狀態:
滑出頂端
從頂端滑入
完成狀態:
從底部滑入
滑出底部
現在來實現button的佈局:
因為ViewAnimator本身是一個FrameLayout,因此需要使用<merge>標籤來減少了一層view。
最後,我們只有一個需求了,當button切換到完成狀態之後,隔兩秒它會自動切換回去。
程式碼很簡單:
init()方法(29行)將前面建立的佈局inflate給了ViewAnimator。順便可以去看下這篇文章proper Layout Inflation,裡面講解了些很可能被你忽略的細節。
為了防止在activity結束的時候按鈕的狀態還沒有切換回來,onDetachedFromWindow()去掉了回撥方法revertStateRunnable()。
其餘的都非常簡單,通過setInAnimation() and setOutAnimation()兩個方法來實現進入和退出動畫。
好了,剛剛我們完成了SendCommentButton 的實現。注:這部分最好根據文中提到的變數名方法名對照程式碼理解。
selector
和feed中的操作按鈕一樣,我們需要為SendCommentButton準備兩個selector。棒棒糖的裝置我們使用ripple效果。pre-21的裝置我們製造出標準的按下效果和陰影效果就可以了,就像在第一篇文章中對浮動操作按鈕的做法。
下面是兩種xml的程式碼:
res/drawable-v21/btn_send_comment.xml:
1 2 3 4 5 6 7 8 9 10 |
<?xml version="1.0" encoding="utf-8"?> <!--drawable-v21/btn_send_comment.xml--> <ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="#ffffff"> <item> <shape android:shape="rectangle"> <solid android:color="@color/btn_send_normal" /> </shape> </item> </ripple> |
btn_send_comment_v21.xml hosted with ❤ by GitHub
res/drawable/btn_send_comment.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<?xml version="1.0" encoding="utf-8"?> <!--drawable/btn_send_comment.xml--> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="false"> <layer-list> <item android:bottom="0dp" android:left="2dp" android:right="0dp" android:top="2dp"> <shape android:shape="rectangle"> <solid android:color="@color/fab_color_shadow" /> <corners android:radius="2dp" /> </shape> </item> <item android:bottom="1dp" android:left="1dp" android:right="1dp" android:top="1dp"> <shape android:shape="rectangle"> <solid android:color="@color/btn_send_normal" /> <corners android:radius="2dp" /> </shape> </item> </layer-list> </item> <item android:state_pressed="true"> <shape android:bottom="0dp" android:left="2dp" android:right="0dp" android:shape="rectangle" android:top="2dp"> <solid android:color="@color/btn_send_pressed" /> <corners android:radius="2dp" /> </shape> </item> </selector> |
btn_send_comment.xml hosted with ❤ by GitHub
錯誤振動提示
最後,我們將增加一個視訊中沒有出現的效果-當傳送評論時如果輸入框中沒有內容,則播放振動動畫。下面是效果圖:
Implementation is pretty simple – we have to create shake animation and custom CycleInterpolator for repeating this animation. Everything is in a few lines of code:
res/anim/shake_error.xml:
實現很簡單-建立一個振動動畫並使用自定義的CycleInterpolator插值器來重複播放這個動畫,只有幾行程式碼:
1 2 3 4 5 6 |
<?xml version="1.0" encoding="utf-8"?> <translate xmlns:android="http://schemas.android.com/apk/res/android" android:duration="300" android:fromXDelta="0%" android:interpolator="@anim/cycle_2" android:toXDelta="2%" /> |
shake_error.xml hosted with ❤ by GitHub
res/anim/cycle_2.xml:
1 2 3 |
<?xml version="1.0" encoding="utf-8"?> <cycleInterpolator xmlns:android="http://schemas.android.com/apk/res/android" android:cycles="2"/> |
cycle_2.xml hosted with ❤ by GitHub
通過如下的程式碼來將這個動畫應用到按鈕中:
1 |
btnSendComment.startAnimation(AnimationUtils.loadAnimation(this, R.anim.shake_error)); |
這就是今天所講的全部內容了,這裡是含有SendCommentButton類的最後一次提交:last commit 。下篇文章中我們將繼續實現概念視訊中的效果。
原始碼
討論中例子的原始碼在這裡:repository.