幾個月前(本文發表於2014年11月10日),Google 釋出了app和web應用的 Material Design 設計準則之後,設計師 Emmanuel Pacamalan 釋出了一則概念視訊,演示了 Instagram 如果做成Material風格會是什麼樣子:
這僅僅是停留在原型設計上,估計很多人都會問,能否用相對簡單的辦法實現它呢?答案是肯定的。不僅僅能實現,而且無須要求在最新的 Lollipop 版本,實際上幾年前 Android 4.0 釋出之後我們就可以實現這些效果了。
鑑於此,我決定開始寫一個新的系列文章,那就是如何將《INSTAGRAM with Material Design》視訊中的效果轉變成現實。當然,我們並不是真的要做一個Instagram應用,只是將介面做出來而已,並且儘量減少一些不必要的細節。
開始
本文將要實現的是視訊中前7秒鐘的效果。我覺得對於第一次嘗試來說已經足夠了,同時也考慮到了我們也要準備和配置我們的專案。
我想要提醒諸位的是,裡面的實現方法不僅僅是能實現,也是我個人最喜歡的實現方式。還有,我不是一個美工,因此專案中的所有圖片是直接從網上的公開渠道獲取的。(主要來源是 resources page )。
好了,下面是最終效果的兩組截圖和視訊(分別從 Android 4.0 和 5.0 上獲得的):
視訊地址:http://www.youtube.com/embed/rTucTiIlQDA
視訊地址:http://www.youtube.com/embed/fYhpc1LddHE
準備
在我們的專案中,將使用一些熱門的 Android 開發工具和庫。並不是所有這些東西本篇文章都會用到,我只是將它們準備好以備不時之需。
初始化專案
首先我們需要建立一個新的 Android 專案。我使用的是 Android Studio 和 Gradle 構建系統。最低版本要求sdk是15(即 Android 4.0.4 )。
然後我們將新增一些依賴。沒什麼好講的,下面是 build.gradle 以及 app/build.gradle 檔案的程式碼:
build.gradle
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:0.14.0' classpath 'com.jakewharton.hugo:hugo-plugin:1.1.+' } } allprojects { repositories { jcenter() } } |
1 |
app/build.gradle |
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 29 |
apply plugin: 'com.android.application' apply plugin: 'hugo' android { compileSdkVersion 21 buildToolsVersion "21.1" defaultConfig { applicationId "io.github.froger.instamaterial" minSdkVersion 15 targetSdkVersion 21 versionCode 1 versionName "1.0" } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile "com.android.support:appcompat-v7:21.0.0" compile 'com.android.support:support-v13:21.+' compile 'com.android.support:support-v4:21.+' compile 'com.android.support:palette-v7:+' compile 'com.android.support:recyclerview-v7:+' compile 'com.android.support:cardview-v7:21.0.+' compile 'com.jakewharton:butterknife:5.1.2' compile 'com.jakewharton.timber:timber:2.5.0' compile 'com.facebook.rebound:rebound:0.3.6' } |
簡而言之,我們有如下工具:
- 一些相容包(CardView, RecyclerView, Palette, AppCompat),我喜歡使用最新的控制元件。當然你完全可以使用 ListView、Actionbar 甚至 View/FrameView 來替代,但是為什麼要這麼折騰?
- ButterKnife – View註解工具簡化我們的程式碼。(比方說不再需要寫 findViewById() 來引用 View,以及一些更強大的功能,比如 onClick() 等等)。
- Rebound – 我們目前還沒有用到,但是我以後肯定會用它。這個 Facebook 開發的動畫庫可以讓你的動畫效果看起來更自然。(它提供了一些工具使用彈性動畫,從 Facebook 的 Messager 的 Chat Heads 中你可以看到他們是如何展示的,我確定你希望在你的下一個專案中使用它)
- Timber 和 Hugo – 對這個專案而言並不是必須的,我僅僅是用它們列印log日誌。
圖片資源
本專案中將使用到一些 Material Design 的圖示資源。應用程式圖示來自於 NSTAGRAM with Material Design視訊,這裡 是專案的全套資源。
樣式
我們從定義app的預設樣式開始。同時為 Android 4.0 和 5.0 定義 Material Desing 樣式的最簡單的方式是直接繼承 Theme.AppCompat.NoActionBar 或者 Theme.AppCompat.Light.NoActionBar 主題。為什麼是 NoActionBar?因為新的sdk中為我們提供了實現 Actionbar 功能的新模式。本例中我們將使用 Toolbar 控制元件,Toolbar是比 ActionBar 更好更靈活的解決方案。我們不會深入講解這個問題,但你可以去閱讀 Android開發者部落格 AppCompat v21。
根據概念視訊中的效果,我們在 AppTheme 中定義了三個基本色調:
styles.xml
1 2 3 4 5 6 7 8 |
<?xml version="1.0" encoding="utf-8"?> <resources> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <item name="colorPrimary">@color/style_color_primary</item> <item name="colorPrimaryDark">@color/style_color_primary_dark</item> <item name="colorAccent">@color/style_color_accent</item> </style> </resources> |
colors.xml
1 2 3 4 5 6 |
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="style_color_primary">#2d5d82</color> <color name="style_color_primary_dark">#21425d</color> <color name="style_color_accent">#01bcd5</color> </resources> |
關於這三個顏色的含義,你可以看這裡 Material Theme Color Palette documentation。
Layout
佈局
專案目前主要使用了3個主要的佈局元素:
- Toolbar – 包含導航圖示和應用程式logo的頂部bar
- RecyclerView – 用於顯示feed
- Floating Action Button – 一個實現了 Material Design 中 action button pattern的 ImageButton。
在開始實現佈局之前,我們先在 res/values/dimens.xml 檔案中定義一些預設值:
dimens.xml
1 2 3 4 5 6 |
<?xml version="1.0" encoding="utf-8"?> <resources> <dimen name="btn_fab_size">56dp</dimen> <dimen name="btn_fab_margins">16dp</dimen> <dimen name="default_elevation">8dp</dimen> </resources> |
這些值的大小是基於 Material Design 設計準則中的介紹。
現在我們來實現 MainActivity 中的layout:
activity_main.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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/root" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary"> <ImageView android:id="@+id/ivLogo" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" android:scaleType="center" android:src="@drawable/img_toolbar_logo" /> </android.support.v7.widget.Toolbar> <android.support.v7.widget.RecyclerView android:id="@+id/rvFeed" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@id/toolbar" android:scrollbars="none" /> <ImageButton android:id="@+id/btnCreate" android:layout_width="@dimen/btn_fab_size" android:layout_height="@dimen/btn_fab_size" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:layout_marginBottom="@dimen/btn_fab_margins" android:layout_marginRight="@dimen/btn_fab_margins" android:background="@drawable/btn_fab_default" android:elevation="@dimen/default_elevation" android:src="@drawable/ic_instagram_white" android:textSize="28sp" /> </RelativeLayout> |
稍微解釋一下上面的程式碼:
- 關於 Toolbar 最重要的特徵是它現在是 activity 中 layout 的一部分,而且繼承自 ViewGroup,因此我們可以在裡面放一些UI元素(它們將利用剩餘空間)。本例中,它被用來放置logo圖片。同時,因為 Toolbar是比 Actionbar 更靈活的控制元件,我們可以自定義更多的東西,比如設定背景顏色為colorPrimary(否則 Toolbar 將是透明的)。
- RecyclerView 雖然在 xml 中用起來非常簡單,但是如果 java 程式碼中沒有設定正確,app是不能啟動的,會報 java.lang.NullPointerException 異常(原因是沒有配置好 LayoutAdapter,它負責排程 RecyclerView 中的每一項)。
- Elevation(ImageButton中)屬性不相容API 21以前的版本。所以如果我們想做到 Floating Action Button 的效果,需要在 Lollipop 以及之前的裝置上使用不同的背景。
Floating Action Button
為了簡化 FAB 的使用,我們將用對 Lollipop 及之前的裝置使用不同的樣式:
- FAB for Android v21:
- FAB for Android pre-21:
我們需要建立兩個不同的xml檔案來設定按鈕的背景: /res/drawable-v21/btn_fab_default.xml(Lollipop之後裝置) 和 /res/drawable/btn_fab_default.xml(Lollipop之前的裝置) :
drawable-v21/btn_fab_default.xml
1 2 3 4 5 6 7 8 9 |
<?xml version="1.0" encoding="utf-8"?> <ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/fab_color_shadow"> <item> <shape android:shape="oval"> <solid android:color="@color/style_color_accent" /> </shape> </item> </ripple> |
_drawable/btn_fab_default.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<?xml version="1.0" encoding="utf-8"?> <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="2dp" android:top="2dp"> <shape android:shape="oval"> <solid android:color="@color/fab_color_shadow" /> </shape> </item> <item android:bottom="2dp" android:left="2dp" android:right="2dp" android:top="2dp"> <shape android:shape="oval"> <solid android:color="@color/style_color_accent" /> </shape> </item> </layer-list> </item> <item android:state_pressed="true"> <shape android:bottom="2dp" android:left="2dp" android:right="2dp" android:shape="oval" android:top="2dp"> <solid android:color="@color/fab_color_pressed" /> </shape> </item> </selector> |
上面的程式碼涉及到兩個顏色的定義,在 res/values/colors.xml 中新增:
1 2 |
<color name="btn_default_light_normal">#00000000</color> <color name="btn_default_light_pressed">#40ffffff</color> |
可以看到在 API 21 之前的裝置上顯示陰影有點複雜。不幸的是,在xml中達到真實的陰影效果沒有漸變方法。其他的辦法是使用圖片的方式,或者通過Java程式碼實現(參見creating fab shadow)。
Toolbar
現在我們來完成 Toolbar。我們已經有了背景和應用的logo,現在還剩下navigation以及menu選單圖示了。
關於navigation,不巧的是,在xml中 app:navigationIcon=”” 是不起作用的,而 android:navigationIcon=”” 又只能在Lollipop上有用,所以只能使用程式碼的方式了:
1 |
toolbar.setNavigationIcon(R.drawable.ic_menu_white); |
至於menu圖示,我們使用標準的定義方式就好了:
menu_main.xml
1 2 3 4 5 6 7 8 9 10 |
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity"> <item android:id="@+id/action_inbox" android:icon="@drawable/ic_inbox_white" android:title="Inbox" app:showAsAction="always" /> </menu> |
在 Activity 中 inflate 這個menu:
一切本應正常,但是正如我在 twitter 上提到的,Toolbar onClick selectors 有不協調的情況:
為了解決這個問題,需要做更多的工作,首先為 menu item (res/layout/menu_item_view.xml) 建立一個自定義檢視:
menu_item_view.xml
1 2 3 4 5 6 |
<?xml version="1.0" encoding="utf-8"?> <ImageButton xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="?attr/actionBarSize" android:layout_height="?attr/actionBarSize" android:background="@drawable/btn_default_light" android:src="@drawable/ic_inbox_white" /> |
現在我們為 Lollipop 及之前的裝置分別建立 onClick 的selector,在 Lollipop 上有 ripple 效果:
drawable-v21/btn_default_light.xml
1 2 3 |
<?xml version="1.0" encoding="utf-8"?> <ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/btn_default_light_pressed" /> |
1 |
_drawable/btn_default_light.xml |
1 2 3 4 5 6 |
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@color/btn_default_light_normal" android:state_focused="false" android:state_pressed="false" /> <item android:drawable="@color/btn_default_light_pressed" android:state_pressed="true" /> <item android:drawable="@color/btn_default_light_pressed" android:state_focused="true" /> </selector> |
現在,專案中的所有的color應該是這樣子了:
colors.xml
1 2 3 4 5 6 7 8 9 10 11 12 |
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="style_color_primary">#2d5d82</color> <color name="style_color_primary_dark">#21425d</color> <color name="style_color_accent">#01bcd5</color> <color name="fab_color_pressed">#007787</color> <color name="fab_color_shadow">#44000000</color> <color name="btn_default_light_normal">#00000000</color> <color name="btn_default_light_pressed">#40ffffff</color> </resources> |
最後我們應該將自定義檢視放到menu item中。在 onCreateOptionsMenu() 方法中:
以上就是 Toolbar 的所有東西。並且 onClick 的按下效果也達到了預期的效果:
Feed
最後需要實現的是feed,基於 RecyclerView 實現。我們需要設定兩個東西:layout manager( RecyclerView 需要知道如何管理每一個item)和 adapter(提供這些item)。
首先要做的很簡單,因為這裡其實只是想實現 ListView 的效果,所以直接用 LinearLayoutManager 就行了。其次,我們需要做更多的事情,但是沒有捷徑去實現這些。
我們首先從item的佈局開始(res/layout/item_feed.xml):
item_feed.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 29 30 31 32 |
<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:card_view="http://schemas.android.com/apk/res-auto" android:id="@+id/card_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_margin="8dp" card_view:cardCornerRadius="4dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <ImageView android:layout_width="match_parent" android:layout_height="wrap_content" android:src="@drawable/ic_feed_top" /> <io.github.froger.instamaterial.SquaredImageView android:id="@+id/ivFeedCenter" android:layout_width="match_parent" android:layout_height="wrap_content" /> <ImageView android:id="@+id/ivFeedBottom" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout> </android.support.v7.widget.CardView> |
在上面的程式碼中:
- CardView – 在我們的每一個list item周圍包裝成圓角和陰影輪廓(這在 Android API 21及以前都可以實現)
- ImageView 對應於feed元素的設計( SquaredImageView 是一種 ImageView 的實現,它具有整齊的dimensions)
FeedAdapter 也非常簡單:
FeedAdapter.java
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
public class FeedAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int ANIMATED_ITEMS_COUNT = 2; private Context context; private int lastAnimatedPosition = -1; private int itemsCount = 0; public FeedAdapter(Context context) { this.context = context; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = LayoutInflater.from(context).inflate(R.layout.item_feed, parent, false); return new CellFeedViewHolder(view); } private void runEnterAnimation(View view, int position) { if (position >= ANIMATED_ITEMS_COUNT - 1) { return; } if (position > lastAnimatedPosition) { lastAnimatedPosition = position; view.setTranslationY(Utils.getScreenHeight(context)); view.animate() .translationY(0) .setInterpolator(new DecelerateInterpolator(3.f)) .setDuration(700) .start(); } } @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { runEnterAnimation(viewHolder.itemView, position); CellFeedViewHolder holder = (CellFeedViewHolder) viewHolder; if (position % 2 == 0) { holder.ivFeedCenter.setImageResource(R.drawable.img_feed_center_1); holder.ivFeedBottom.setImageResource(R.drawable.img_feed_bottom_1); } else { holder.ivFeedCenter.setImageResource(R.drawable.img_feed_center_2); holder.ivFeedBottom.setImageResource(R.drawable.img_feed_bottom_2); } } @Override public int getItemCount() { return itemsCount; } public static class CellFeedViewHolder extends RecyclerView.ViewHolder { @InjectView(R.id.ivFeedCenter) SquaredImageView ivFeedCenter; @InjectView(R.id.ivFeedBottom) ImageView ivFeedBottom; public CellFeedViewHolder(View view) { super(view); ButterKnife.inject(this, view); } } public void updateItems() { itemsCount = 10; notifyDataSetChanged(); } } |
沒什麼特別之處需要說明。
通過以下方法將他們放在一起:
1 2 3 4 5 6 |
private void setupFeed() { LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); rvFeed.setLayoutManager(linearLayoutManager); feedAdapter = new FeedAdapter(this); rvFeed.setAdapter(feedAdapter); } |
下面是整個 MainActivity 類的原始碼:
MainActivity.java
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 29 30 31 32 33 34 35 36 37 38 39 |
public class MainActivity extends ActionBarActivity { @InjectView(R.id.toolbar) Toolbar toolbar; @InjectView(R.id.rvFeed) RecyclerView rvFeed; private MenuItem inboxMenuItem; private FeedAdapter feedAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.inject(this); setupToolbar(); setupFeed(); } private void setupToolbar() { setSupportActionBar(toolbar); toolbar.setNavigationIcon(R.drawable.ic_menu_white); } private void setupFeed() { LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); rvFeed.setLayoutManager(linearLayoutManager); feedAdapter = new FeedAdapter(this); rvFeed.setAdapter(feedAdapter); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); inboxMenuItem = menu.findItem(R.id.action_inbox); inboxMenuItem.setActionView(R.layout.menu_item_view); return true; } } |
當你在裝置上build並執行應用程式之後,你應該看到如下畫面:
- Android Lollipop:
- Android pre-21:
動畫
最後一件也是最重要的事情就是進入時的動畫效果,再瀏覽一遍概念視訊,可以發現在主 Activity 啟動的時候有如下動畫,分成兩步:
- 顯示 Toolbar 以及其裡面的元素
- 在 Toolbar 動畫完成之後顯示 feed 和 floating action button。
Toolbar 中元素的動畫表現為在較短的時間內一個接一個地進入。實現這個效果的主要問題在於navigation圖示的動畫,navigation圖示是唯一一個不能使用動畫的,其他的都好辦。
Toolbar animation
首先我們只是需要在activity啟動的時候才播放動畫(在旋轉螢幕的時候不播放),還要知道menu的動畫過程是不能在 onCreate() 中去實現的(我們在 onCreateOptionsMenu() 中實現)。
在 MainActivity 建立一個布林型別的變數 pendingIntroAnimation,在 onCreate() 方法中初始化:
1 2 3 4 |
//... if (savedInstanceState == null) { pendingIntroAnimation = true; } |
在 onCreateOptionsMenu() 中實現:
1 2 3 4 5 6 7 8 9 10 11 |
@Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); inboxMenuItem = menu.findItem(R.id.action_inbox); inboxMenuItem.setActionView(R.layout.menu_item_view); if (pendingIntroAnimation) { pendingIntroAnimation = false; startIntroAnimation(); } return true; } |
在我們啟動程式之後,startIntroAnimation() 將只被呼叫一次。
現在該來準備Toolbar中元素的動畫了,也非常簡單。很好的一點就是保持注意力,一般情況下,動畫由一下兩步組成:
- 準備 – 我們為每一個動畫元素建立初始狀態。如果我們希望展現擴充套件動畫,我們需要確保我們的每一項是隱藏的。
- 動畫 – 這一步我們的view會進行動畫至最終狀態或位置。
好的,讓我們嘗試為 Toolbar 實現這兩個步驟:
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 29 30 31 |
private static final int ANIM_DURATION_TOOLBAR = 300; private void startIntroAnimation() { btnCreate.setTranslationY(2 * getResources().getDimensionPixelOffset(R.dimen.btn_fab_size)); int actionbarSize = Utils.dpToPx(56); toolbar.setTranslationY(-actionbarSize); ivLogo.setTranslationY(-actionbarSize); inboxMenuItem.getActionView().setTranslationY(-actionbarSize); toolbar.animate() .translationY(0) .setDuration(ANIM_DURATION_TOOLBAR) .setStartDelay(300); ivLogo.animate() .translationY(0) .setDuration(ANIM_DURATION_TOOLBAR) .setStartDelay(400); inboxMenuItem.getActionView().animate() .translationY(0) .setDuration(ANIM_DURATION_TOOLBAR) .setStartDelay(500) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { startContentAnimation(); } }) .start(); } //... |
在上面的程式碼中:
- 首先我們將所有的元素都通過移動到螢幕之外隱藏起來(這一步我們將FAB也隱藏了)。
- 讓Toolbar元素一個接一個的開始動畫。
當動畫完成,呼叫了 startContentAnimation() 開始 content 的動畫( FAB 和 feed 卡片的動畫)
Content動畫
在這一步中我們將讓 FAB 和 feed 卡片動起來。FAB 的動畫很簡單,跟上面的方法類似,但是 feed 卡片稍微複雜些。
- startContentAnimation 方法:
FAB animation
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//... private static final int ANIM_DURATION_FAB = 400; private void startContentAnimation() { btnCreate.animate() .translationY(0) .setInterpolator(new OvershootInterpolator(1.f)) .setStartDelay(300) .setDuration(ANIM_DURATION_FAB) .start(); feedAdapter.updateItems(); } //... |
- FeedAdapter,包含 feed item 的動畫:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
public class FeedAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int ANIMATED_ITEMS_COUNT = 2; private Context context; private int lastAnimatedPosition = -1; private int itemsCount = 0; public FeedAdapter(Context context) { this.context = context; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = LayoutInflater.from(context).inflate(R.layout.item_feed, parent, false); return new CellFeedViewHolder(view); } private void runEnterAnimation(View view, int position) { if (position >= ANIMATED_ITEMS_COUNT - 1) { return; } if (position > lastAnimatedPosition) { lastAnimatedPosition = position; view.setTranslationY(Utils.getScreenHeight(context)); view.animate() .translationY(0) .setInterpolator(new DecelerateInterpolator(3.f)) .setDuration(700) .start(); } } @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { runEnterAnimation(viewHolder.itemView, position); CellFeedViewHolder holder = (CellFeedViewHolder) viewHolder; if (position % 2 == 0) { holder.ivFeedCenter.setImageResource(R.drawable.img_feed_center_1); holder.ivFeedBottom.setImageResource(R.drawable.img_feed_bottom_1); } else { holder.ivFeedCenter.setImageResource(R.drawable.img_feed_center_2); holder.ivFeedBottom.setImageResource(R.drawable.img_feed_bottom_2); } } @Override public int getItemCount() { return itemsCount; } public static class CellFeedViewHolder extends RecyclerView.ViewHolder { @InjectView(R.id.ivFeedCenter) SquaredImageView ivFeedCenter; @InjectView(R.id.ivFeedBottom) ImageView ivFeedBottom; public CellFeedViewHolder(View view) { super(view); ButterKnife.inject(this, view); } } public void updateItems() { itemsCount = 10; notifyDataSetChanged(); } } |
就是這些了!如果我們build並執行我們的專案,我們就會得到看到文章開頭展示的最終版本的效果。
原始碼
完整的程式碼在 Github repository。
作者: Miroslaw Stanek