今天我們將要實現的是feed和評論界切換以及相關的動畫效果。(視訊中9-13秒那部分)。我將忽略涉及到的button效果(波紋、傳送完成的動畫等,在下篇文章專門討論),將重點放在comment Acitvity進入的效果上面。
初始化
我們首先將一些不太重要的東西加入前面文章所建立的專案中previously created project。
.Picasso 一個圖片非同步載入庫(用於評論列表中顯示作者的頭像),
.AndroidManifest.xml
中新增評論的activity
當然我們還要建立新activity的佈局檔案。除了底部的評論輸入框之外基本上和上篇文章是一致的。我們再次用到了Toolbar,
RecyclerView
等。沒有什麼好值得講的。
activity_comments.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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".CommentsActivity"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" android:elevation="@dimen/default_elevation"> <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> <LinearLayout android:id="@+id/contentRoot" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@id/toolbar" android:background="@color/bg_comments" android:elevation="@dimen/default_elevation" android:orientation="vertical"> <android.support.v7.widget.RecyclerView android:id="@+id/rvComments" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:scrollbars="none" /> <LinearLayout android:id="@+id/llAddComment" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/bg_comments" android:elevation="@dimen/default_elevation"> <EditText android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" /> <Button android:id="@+id/btnSendComment" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Send" /> </LinearLayout> </LinearLayout> </RelativeLayout> |
下一步,建立評論列表item的佈局:
item_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 29 30 31 32 33 34 35 36 37 |
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:paddingBottom="8dp" android:paddingTop="8dp"> <ImageView android:id="@+id/ivUserAvatar" android:layout_width="@dimen/comment_avatar_size" android:layout_height="@dimen/comment_avatar_size" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" android:background="@drawable/bg_comment_avatar" /> <TextView android:id="@+id/tvComment" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginRight="16dp" android:layout_weight="1" android:text="Lorem ipsum dolor sit amet" /> </LinearLayout> <View android:layout_width="match_parent" android:layout_height="1dp" android:layout_gravity="bottom" android:layout_marginLeft="88dp" android:background="#cccccc" /> </FrameLayout> |
評論作者圓角頭像部分的程式碼片段:
bg_comment_avatar.xml
1 2 3 4 5 6 |
<?xml version="1.0" encoding="utf-8"?> <!--drawable/bg_comment_avar.xml--> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="#999999" /> </shape> |
最後一件事情是處理feed卡片底部評論按鈕的的onClick事件,這個事件將開啟顯示當前照片評論的CommentsActivity。我們暫時將卡片的整個底部區域作為點選按鈕,但是以後將會替換為真正的評論按鈕。我們在RecyclerView adapter中新增onClick的listener,將為每個卡片的底部設定這個listener。
FeedAdapter 類(只包含改動部分):
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 |
//.. implements View.OnClickListener public class FeedAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements View.OnClickListener { private OnFeedItemClickListener onFeedItemClickListener; //.. @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { //... holder.ivFeedBottom.setOnClickListener(this); holder.ivFeedBottom.setTag(position); } //.. @Override public void onClick(View v) { if (v.getId() == R.id.ivFeedBottom) { if (onFeedItemClickListener != null) { onFeedItemClickListener.onCommentsClick(v, (Integer) v.getTag()); } } } public void setOnFeedItemClickListener(OnFeedItemClickListener onFeedItemClickListener) { this.onFeedItemClickListener = onFeedItemClickListener; } //.. public interface OnFeedItemClickListener { public void onCommentsClick(View v, int position); } } |
MainActivity 類(只包含改動部分):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//... implements FeedAdapter.OnFeedItemClickListener public class MainActivity extends ActionBarActivity implements FeedAdapter.OnFeedItemClickListener { //... private void setupFeed() { //... feedAdapter.setOnFeedItemClickListener(this); //... } //... @Override public void onCommentsClick(View v, int position) { } } |
為了防止遺漏,我們將這一部分的程式碼提交在了這裡:onClick on item in RecyclerView adapter。
進入動畫
首先我們將建立進入的過度動畫,根據概念視訊上的顯示,以下是我們需要實現的效果:
1.靜態的Toolbar-新的activity開啟的時候Actionbar不能有過渡與切換效果(我們想讓使用者以為他們仍然在同一個視窗中)
2.評論列表介面要根據使用者點選的位置展開(不管列表滾動到何處)
3.在展開效果結束之後,評論列表中的每一項要依次展示
靜態的Toolbar
這是本文最簡單的部分。因為在MainActivity與CommentsActivity中Toolbar是非常相似的,所以我們只需要將Acitvity預設的切換效果遮蔽了就可以了,那樣新的視窗不會有任何移動僅僅是繪製在上個視窗的上面。這樣就實現了Toolbar的靜態顯示。下面是程式碼:
1 2 3 4 5 6 7 8 9 10 |
public class MainActivity extends ActionBarActivity implements FeedAdapter.OnFeedItemClickListener { //... @Override public void onCommentsClick(View v, int position) { final Intent intent = new Intent(this, CommentsActivity.class); startActivity(intent); //Disable enter transition for new Acitvity overridePendingTransition(0, 0); } } |
通過呼叫overridePendingTransition(0, 0);我們遮蔽了MainActivity的退出效果,以及CommentsActivity的進入效果。
從被點選的地方展開CommentsActivity
現在我們建立可以從任何點選的地方開始的擴充套件動畫。這個動畫分為:
.展開背景
.顯示內容
在我們開始動畫部分的程式碼之前,先將CommentsActivity設定成半透明。不然的話擴充套件動畫將顯示這預設的視窗背景之上,而不是MainAcitvity的view之上。這是因為每個activity的視窗背景都是定義在它所採用的主題中了的。如果我們想讓activity變半透明,我們需要修改樣式:
1 2 3 4 5 6 7 8 9 10 11 |
<?xml version="1.0" encoding="utf-8"?> <!-- styles.xml--> <resources> <!--...--> <style name="AppTheme.CommentsActivity" parent="AppTheme"> <item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowIsTranslucent">true</item> </style> </resources> |
下面是改變背景與不改變背景的區別
現在我們可以開始製造展開的效果了。首先,我們需要得到動畫的Y軸初始值。我們的例子中其實完全不需要知道點選的精確位置(動畫很快,使用者不會注意到幾個畫素的差異的),我使用被點選view的Y值來替代,並且將這個Y值傳遞給CommentsActivity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class MainActivity extends ActionBarActivity implements FeedAdapter.OnFeedItemClickListener { //... @Override public void onCommentsClick(View v, int position) { final Intent intent = new Intent(this, CommentsActivity.class); //Get location on screen for tapped view int[] startingLocation = new int[2]; v.getLocationOnScreen(startingLocation); intent.putExtra(CommentsActivity.ARG_DRAWING_START_LOCATION, startingLocation[1]); startActivity(intent); overridePendingTransition(0, 0); } } |
下一步,在CommentsActivity中實現background的展開動畫。為了簡便起見,我們使用Scale動畫(因為在此刻還沒有任何內容,因此沒人知道到底是縮放開來的還是展開的),別忘了使用setPivotY() 方法設定正確的初始位置。
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 |
public class CommentsActivity extends ActionBarActivity { public static final String ARG_DRAWING_START_LOCATION = "arg_drawing_start_location"; @InjectView(R.id.toolbar) Toolbar toolbar; @InjectView(R.id.contentRoot) LinearLayout contentRoot; @InjectView(R.id.rvComments) RecyclerView rvComments; @InjectView(R.id.llAddComment) LinearLayout llAddComment; private CommentsAdapter commentsAdapter; private int drawingStartLocation; @Override protected void onCreate(Bundle savedInstanceState) { //... drawingStartLocation = getIntent().getIntExtra(ARG_DRAWING_START_LOCATION, 0); if (savedInstanceState == null) { contentRoot.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { contentRoot.getViewTreeObserver().removeOnPreDrawListener(this); startIntroAnimation(); return true; } }); } } //... private void startIntroAnimation() { contentRoot.setScaleY(0.1f); contentRoot.setPivotY(drawingStartLocation); llAddComment.setTranslationY(100); contentRoot.animate() .scaleY(1) .setDuration(200) .setInterpolator(new AccelerateInterpolator()) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { animateContent(); } }) .start(); } private void animateContent() { commentsAdapter.updateItems(); llAddComment.animate().translationY(0) .setInterpolator(new DecelerateInterpolator()) .setDuration(200) .start(); } //... } |
多虧了onPreDrawListener ,我們才可以在view樹完成測量並且分配空間而繪製過程還沒有開始的時候播放動畫。
上面的程式碼中我們已經實現了展開背景與顯示內容的動畫,下面是執行的效果:
是不是感覺還是少了點什麼東西?
還需要準備評論列表中每個評論項的動畫。很簡單,但是需要注意幾件重要的事情:
1.每個item的動畫需要有一定延時。否則所有的動畫將在瞬間結束使用者只能感受到一個動畫。
2.adapter需要有鎖定動畫的功能,因為在使用者滾動列表的時候動畫是不需要的。
3.同樣的我們還要讓每個單獨的item能鎖定與解鎖動畫(比如新增一個評論)
目前CommentsAdapter 的程式碼如下:
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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
public class CommentsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private Context context; private int itemsCount = 0; private int lastAnimatedPosition = -1; private int avatarSize; private boolean animationsLocked = false; private boolean delayEnterAnimation = true; public CommentsAdapter(Context context) { this.context = context; avatarSize = context.getResources().getDimensionPixelSize(R.dimen.btn_fab_size); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = LayoutInflater.from(context).inflate(R.layout.item_comment, parent, false); return new CommentViewHolder(view); } @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { runEnterAnimation(viewHolder.itemView, position); CommentViewHolder holder = (CommentViewHolder) viewHolder; switch (position % 3) { case 0: holder.tvComment.setText("Lorem ipsum dolor sit amet, consectetur adipisicing elit."); break; case 1: holder.tvComment.setText("Cupcake ipsum dolor sit amet bear claw."); break; case 2: holder.tvComment.setText("Cupcake ipsum dolor sit. Amet gingerbread cupcake. Gummies ice cream dessert icing marzipan apple pie dessert sugar plum."); break; } Picasso.with(context) .load(R.drawable.ic_launcher) .centerCrop() .resize(avatarSize, avatarSize) .transform(new RoundedTransformation()) .into(holder.ivUserAvatar); } private void runEnterAnimation(View view, int position) { if (animationsLocked) return; if (position > lastAnimatedPosition) { lastAnimatedPosition = position; view.setTranslationY(100); view.setAlpha(0.f); view.animate() .translationY(0).alpha(1.f) .setStartDelay(delayEnterAnimation ? 20 * (position) : 0) .setInterpolator(new DecelerateInterpolator(2.f)) .setDuration(300) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { animationsLocked = true; } }) .start(); } } @Override public int getItemCount() { return itemsCount; } public void updateItems() { itemsCount = 10; notifyDataSetChanged(); } public void addItem() { itemsCount++; notifyItemInserted(itemsCount - 1); } public void setAnimationsLocked(boolean animationsLocked) { this.animationsLocked = animationsLocked; } public void setDelayEnterAnimation(boolean delayEnterAnimation) { this.delayEnterAnimation = delayEnterAnimation; } public static class CommentViewHolder extends RecyclerView.ViewHolder { @InjectView(R.id.ivUserAvatar) ImageView ivUserAvatar; @InjectView(R.id.tvComment) TextView tvComment; public CommentViewHolder(View view) { super(view); ButterKnife.inject(this, view); } } } |
展示頭像我們使用帶CircleTransformation的Picasso庫,我們利用了RecyclerView 的notifyItemInserted方法實現了新增一個item的動畫效果,其餘的程式碼都很簡單。
下面是在CommentsActivity 中使用的程式碼:
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 |
public class CommentsActivity extends ActionBarActivity { //... private void setupComments() { //... rvComments.setOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { commentsAdapter.setAnimationsLocked(true); } } }); } @OnClick(R.id.btnSendComment) public void onSendCommentClick() { commentsAdapter.addItem(); commentsAdapter.setAnimationsLocked(false); commentsAdapter.setDelayEnterAnimation(false); rvComments.smoothScrollBy(0, rvComments.getChildAt(0).getHeight() * commentsAdapter.getItemCount()); } } |
Item的動畫在使用者滾動RecyclerView的時候被鎖定,這就是進入動畫的所有東西了。
退出動畫
最後一件事是實現退出動畫,沒有非常特別的技巧,我們只需建立一個transition 動畫來滑出activity就可以了,記住Toolbar必須是靜止的,因此我們再次使用overridePendingTransition(0, 0);並且播放內容部分的動畫。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class CommentsActivity extends ActionBarActivity { //... @Override public void onBackPressed() { contentRoot.animate() .translationY(Utils.getScreenHeight(this)) .setDuration(200) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { CommentsActivity.super.onBackPressed(); overridePendingTransition(0, 0); } }) .start(); } } |
以上就是我們實現概念app的第二階段的所有內容。下一篇我們將討論這章遺漏的內容(按鈕的動畫效果)
原始碼
討論中例子的原始碼在這裡: repository.