InstaMaterial概念设计第二部分-评论界面的过度动画

泡在网上的日子 / 文 发表于2015-02-06 01:49 次阅读 Instagram,Material

今天我们将要实现的是feed和评论界面切换以及相关的动画效果。(视频中9-13秒那部分)。我将忽略涉及到的button效果(波纹、发送完成的动画等,在下篇文章专门讨论),将重点放在comment Acitvity进入的效果上面。

初始化

我们首先将一些不太重要的东西加入前面文章所创建的项目中previously created project

.Picasso 一个图片异步加载库(用于评论列表中显示作者的头像),

.AndroidManifest.xml中添加评论的activity

当然我们还要创建新activity的布局文件。除了底部的评论输入框之外基本上和上篇文章是一致的。我们再次用到了Toolbar,RecyclerView等。没有什么好值得讲的。

activity_comments.xml 

<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>

comments_activity_preview.png

下一步,创建评论列表item的布局:

item_comment.xml

<?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>

comments_list_item.png

评论作者圆角头像部分的代码片段:

bg_comment_avatar.xml

<?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 类(只包含改动部分):

//.. 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 类(只包含改动部分):

//... 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的静态显示。下面是代码:

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变半透明,我们需要修改样式:

<?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>

下面是改变背景与不改变背景的区别

Expand animation with translucent background Expand animation without translucent background


现在我们可以开始制造展开的效果了。首先,我们需要得到动画的Y轴初始值。我们的例子中其实完全不需要知道点击的精确位置(动画很快,用户不会注意到几个像素的差异的),我使用被点击view的Y值来替代,并且将这个Y值传递给CommentsActivity:

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() 方法设置正确的初始位置。

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树完成测量并且分配空间而绘制过程还没有开始的时候播放动画。

上面的代码中我们已经实现了展开背景与显示内容的动画,下面是运行的效果:

Expand animation

是不是感觉还是少了点什么东西?

还需要准备评论列表中每个评论项的动画。很简单,但是需要注意几件重要的事情:

1.每个item的动画需要有一定延时。否则所有的动画将在瞬间结束用户只能感受到一个动画。

2.adapter需要有锁定动画的功能,因为在用户滚动列表的时候动画是不需要的。

3.同样的我们还要让每个单独的item能锁定与解锁动画(比如添加一个评论)

目前CommentsAdapter 的代码如下:

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);
        }
    }
}

展示头像我们使用带CircleTransformationPicasso库,我们利用了RecyclerView 的notifyItemInserted方法实现了添加一个item的动画效果,其余的代码都很简单。

下面是在CommentsActivity 中使用的代码:

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);并且播放内容部分的动画。

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.

英文原文:InstaMaterial concept (part 2) - Comments window transition 

转载请注明出处:http://jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0206/2422.html 

收藏 赞 (6) 踩 (0)
上一篇:Probe:截断view的onMeasure等方法
你应该听说过安卓布局优化的最佳实践:让view尽可能的简单,避免view树结构过于复杂等等,但现实是,你很难知道每次UI绘制过程中(measure → layout → draw)到底发生了什么。通过一些开发工具我们可以了解到一些(debug GPU overdraw, show hardware laye
下一篇:与Sevice实现双向通信(一)
经常有这样的应用场景,我们需要Client端调用Service完成一些事情,Service也可以通过回调通知客户端。 这是我写的第一个系列文章: 与Sevice实现双向通信(一) 与Sevice实现双向通信(二) 与Sevice实现双向通信(三) 这是系列文章的第一篇,本文主要实现