译文如下:
类库
app/build.gradle
// Google libraries
compile 'com.android.support:appcompat-v7:21.0.3'
compile 'com.android.support:recyclerview-v7:21.0.3'
compile 'com.android.support:palette-v7:21.0.0'
// Square libraries
compile 'com.squareup.picasso:picasso:2.4.0'
compile 'com.jakewharton:butterknife:6.0.0'
AppCompat
ToolBar是对原来的ActionBar泛化。
这个组件在我的布局中有一个特点,当用户向下滑动的时候,Toolbar是被隐藏的,当用户向上滑动的时候,Toolbar是被展示的;
隐藏的Toolbar
activity_main.xml
<android.widget.Toolbar
android:id="@+id/activity_main_toolbar"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:minHeight="?attr/actionBarSize"
android:background="@color/theme_primary"
android:elevation="10dp"
>
<com.hackvg.android.views.custom_views.LobsterTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textSize="22sp"
android:textColor="#FFF"
/>
</android.widget.Toolbar>
MoviesActivity.java
private RecyclerView.OnScrollListener recyclerScrollListener =
new RecyclerView.OnScrollListener() {
public boolean flag;
@Override
public void onScrolled(RecyclerView recyclerView,
int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
// Is scrolling up
if (dy > 10) {
if (!flag) {
showToolbar();
flag = true;
}
// Is scrolling down
} else if (dy < -10) {
if (flag) {
hideToolbar();
flag = false;
}
}
}
};
private void showToolbar() {
toolbar.startAnimation(AnimationUtils.loadAnimation(this,
R.anim.translate_up_off));
}
private void hideToolbar() {
toolbar.startAnimation(AnimationUtils.loadAnimation(this,
R.anim.translate_up_on));
}
translate_up_off.xml
<?xml version="1.0" encoding="utf-8"?>
<set
android:interpolator="@android:interpolator/fast_out_linear_in"
android:fillAfter="true">
<translate
android:duration="@integer/anim_trans_duration_millis"
android:startOffset="0"
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="0"
android:toYDelta="-100%"
/>
</set>
ButterKnife
这个框架避免了重复写诸如此类的句子 findViewById 或者
setOnClickListener(new OnClick...)
MovieDetailActivity.java
@InjectViews({
R.id.activity_detail_title,
R.id.activity_detail_content,
R.id.activity_detail_homepage,
R.id.activity_detail_company,
R.id.activity_detail_tagline,
R.id.activity_detail_confirmation_text,
}) List<TextView> movieInfoTextViews;
@InjectViews({
R.id.activity_detail_header_tagline,
R.id.activity_detail_header_description
}) List<TextView> headers;
@InjectView(R.id.activity_detail_book_info)
View overviewContainer;
@InjectView(R.id.activity_detail_fab)
ImageView fabButton;
@InjectView(R.id.activity_detail_cover)
ImageView coverImageView;
@InjectView(R.id.activity_detail_confirmation_image)
ImageView confirmationView;
@InjectView(R.id.activity_detail_confirmation_container)
FrameLayout confirmationContainer;
这个库有一个有趣的地方,就是@InjectViews注解,它允许注入多个视图到一个列表里面,所以你能使用接口,比如Setters或者Actions,对列表里的所有视图执行一次操作。
GUIUtils.java
public static final ButterKnife.Setter<TextView, Integer> setter = new ButterKnife.Setter<TextView, Integer>() {
@Override
public void set(TextView view, Integer value, int index) {
view.setTextColor(value);
}
};
在我的例子里,所有的TextView用来显示有关电影的信息并设置某个特定的颜色。
MoviesActivity.java
ButterKnife.apply(movieInfoTextViews, GUIUtils.setter,
lightSwatch.getTitleTextColor());
ButterKnife 也能让你处理一些view的事件:
@OnClick(R.id.activity_movie_detail_fab)
public void onClick() {
showConfirmationView();
}
调色板
Palette效果通过 Palette 你能获取下面的颜色:
- MutedSwatch
- VibrantSwatch
- DarkVibrantSwatch
- DarkMutedSwatch
- LightMutedSwatch
- LightVibrantSwatch
在这个App中,我将用到VibrantSwatch, DarkVibrantSwatch 和 LightVibrantSwatch。
Swatch效果注意,你不能经常提取一张图片的主色,因为它被推荐的做法是去check Palette,不返回null sets。
另一方面,决定这个颜色是一个复杂的任务,所以 Palette 提供了一个异步方法来生成颜色。
MoviesActivity.java
Palette.generateAsync(bookCoverBitmap, this);
public class MovieDetailActivity extends Activity implements
MVPDetailView, Palette.PaletteAsyncListener {
...
@Override
public void onGenerated(Palette palette) {
if (palette != null) {
Palette.Swatch vibrantSwatch = palette
.getVibrantSwatch();
Palette.Swatch darkVibrantSwatch = palette
.getDarkVibrantSwatch();
Palette.Swatch lightSwatch = palette
.getLightVibrantSwatch();
if (lightSwatch != null) {
// awesome palette code
}
}
}
}
我在Dialer这个App上发现一些有趣的效果,当触摸详情视图的时候,这个icon的颜色是依赖于触摸的图片颜色,如下图:
tint image
这个效果能动态实现,使用 ColorFilter 和 CompoundDrawable。
GUIUtils.java
public static void tintAndSetCompoundDrawable (Context context,
@DrawableRes int drawableRes, int color, TextView textview) {
Resources res = context.getResources();
int padding = (int) res.getDimension(
R.dimen.activity_horizontal_margin);
Drawable drawable = res.getDrawable(drawableRes);
drawable.setColorFilter(color, PorterDuff.Mode.MULTIPLY);
textview.setCompoundDrawablesRelativeWithIntrinsicBounds(
drawable, null, null, null);
textview.setCompoundDrawablePadding(padding);
}
效果:
icon color
转场
转场是从MoviesActivity跳转到MovieDetailActivity,转场动画用到了被选中电影的图片。
在 RecyclerView 的 adapter 中有一个特定的 transitionName,它将用来执行转场动画。
@Override
public void onBindViewHolder(MovieViewHolder holder,
int position) {
TvMovie selectedMovie = movieList.get(position);
holder.titleTextView.setText(selectedMovie.getTitle());
holder.coverImageView.setTransitionName("cover" + position);
String posterURL = Constants.POSTER_PREFIX
+ selectedMovie.getPoster_path();
Picasso.with(context)
.load(posterURL)
.into(holder.coverImageView);
}
在改变为MovieDetailActivity之前,在指定的Intent中共享一个ActivityOptions。
@Override
public void onClick(View v, int position) {
Intent i = new Intent (MoviesActivity.this,
MovieDetailActivity.class);
String movieID = moviesAdapter.getMovieList()
.get(position).getId();
i.putExtra("movie_id", movieID);
i.putExtra("movie_position", position);
ImageView coverImage = (ImageView) v.findViewById(
R.id.item_movie_cover);
photoCache.put(0, coverImage
.getDrawingCache());
// Setup the transition to the detail activity
ActivityOptions options = ActivityOptions
.makeSceneTransitionAnimation(this,
new Pair<View, String>(v, "cover" + position));
startActivity(i, options.toBundle());
}
最后,进入到MovieDetailActivity后,从intent中取出metadata数据。
@Override
public void onCreate(Bundle savedInstanceState) {
...
int moviePosition = getIntent()
.getIntExtra("movie_position", 0);
coverImageView.setTransitionName(
"cover" + moviePosition);
...
目前为止,可能是绝大多数应用程序的列表和详细视图战士,但是如果在详细视图与返回列表之间有一个中间状态呢?
当一个用户按下Floating Action Button去设置为最爱的电影,一个短暂的视图展示用来告诉用户它已经完成。
我对使用sharedElementReturnTransition来实现转场并不感兴趣,我喜欢使用动画来改变用户的体验。
不能够让电影被标记为最喜爱的电影却没有任何反应,所以设计不得不变得不同。
转场动画当确认视图(也就是下图的Saved视图)被显示后,它的转场需要被重写,电影的覆盖效果(也就是图1跳到图2的效果)并不会被显示,而是直接下拉到第一个Activity:getWindow().setReturnTransition(new Slide());
VectorDrawable
一个崭新的drawable,一个全新的世界被打开,比如矢量图形,图像缩放等等。
Lollipop也包含了强有力的工具去处理这些图形。
VectorDrawable 使用了SVG 的一部分,比如,下面是一个SVG格式的星星。
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
<svg version="1.1"
width="300px"
height="300px" >
<g id="star_group">
<path fill="#000000" d="M 200.30535,69.729172
C 205.21044,69.729172 236.50709,141.52218 240.4754,144.40532
C 244.4437,147.28846 322.39411,154.86809 323.90987,159.53312
C 325.42562,164.19814 266.81761,216.14828 265.30186,220.81331
C 263.7861,225.47833 280.66544,301.9558 276.69714,304.83894
C 272.72883,307.72209 205.21044,268.03603 200.30534,268.03603
C 195.40025,268.03603 127.88185,307.72208 123.91355,304.83894
C 119.94524,301.9558 136.82459,225.47832 135.30883,220.8133
C 133.79307,216.14828 75.185066,164.19813 76.700824,159.53311
C 78.216581,154.86809 156.16699,147.28846 160.13529,144.40532
C 164.1036,141.52218 195.40025,69.729172 200.30535,69.729172 z"/>
</g>
</svg>
接下来,用VectorDrawable来实现。
vd_star.xml
<?xml version="1.0" encoding="utf-8"?>
<vector
android:viewportWidth="400"
android:viewportHeight="400"
android:width="300px"
android:height="300px">
<group android:name="star_group"
android:pivotX="200"
android:pivotY="200"
android:scaleX="0.0"
android:scaleY="0.0">
<path
android:name="star"
android:fillColor="#FFFFFF"
android:pathData="@string/star_data"/>
</group>
</vector>
strings.xml
<string name="star_data">
M 200.30535,69.729172
C 205.21044,69.729172 236.50709,141.52218 240.4754,144.40532
C 244.4437,147.28846 322.39411,154.86809 323.90987,159.53312
C 325.42562,164.19814 266.81761,216.14828 265.30186,220.81331
C 263.7861,225.47833 280.66544,301.9558 276.69714,304.83894
C 272.72883,307.72209 205.21044,268.03603 200.30534,268.03603
C 195.40025,268.03603 127.88185,307.72208 123.91355,304.83894
C 119.94524,301.9558 136.82459,225.47832 135.30883,220.8133
C 133.79307,216.14828 75.185066,164.19813 76.700824,159.53311
C 78.216581,154.86809 156.16699,147.28846 160.13529,144.40532
C 164.1036,141.52218 195.40025,69.729172 200.30535,69.729172 z
</string>
这里有一点不同,这里叫做group,paths等等。android:viewport{Width|Height}指定了画布的大小,而android:width和android:height指定了图片的大小。
<animated-vector>允许<paths>包含动画,如移动,旋转和变形。
举个例子,一个星星在执行缩放动画效果的时候也同时执行着旋转动画,同时星星的形状也改变为一根棒棒糖的形状,最后动画结束又变回星星的形状。
值得注意的一点,让它显示一个形变的动画的话也需要含有一样的SVG命令,否则会有异常触发。
avd_star.xml
<?xml version="1.0" encoding="utf-8"?>
<animated-vector
android:drawable="@drawable/vd_star">
<target
android:name="star_group"
android:animation="@anim/appear_rotate" />
<target
android:name="star"
android:animation="@anim/star_morph" />
</animated-vector>
这个<animated-vector>与vd_star.xml是有所关联的,<animated-vector>是这些动画实现的基础。
- 第一部分,start_group
appear_rotate.xml
<set
android:ordering="sequentially"
android:interpolator="@android:anim/decelerate_interpolator"
>
<set
android:ordering="together"
>
<objectAnimator
android:duration="300"
android:propertyName="scaleX"
android:valueFrom="0.0"
android:valueTo="1.0"/>
<objectAnimator
android:duration="300"
android:propertyName="scaleY"
android:valueFrom="0.0"
android:valueTo="1.0"/>
</set>
<objectAnimator
android:propertyName="rotation"
android:duration="500"
android:valueFrom="0"
android:valueTo="360"
android:valueType="floatType"/>
</set>
- 第二部分,star_morph,是使用<objectAnimator>将SVG数据改为另外一个SVG数据
当星星的形状改变为一个棒棒糖形状之后,它将变回星星形状。
star_morph.xml
<set
android:ordering="sequentially"
android:fillAfter="true">
<objectAnimator
android:duration="500"
android:propertyName="pathData"
android:valueFrom="@string/star_data"
android:valueTo="@string/star_lollipop"
android:valueType="pathType"
android:interpolator="@android:anim/accelerate_interpolator"/>
<objectAnimator
android:duration="500"
android:propertyName="pathData"
android:valueFrom="@string/star_lollipop"
android:valueTo="@string/star_data"
android:valueType="pathType"
android:interpolator="@android:anim/accelerate_interpolator"/>
</set>
activity_detail.xml
<ImageView
android:id="@+id/activity_movie_detail_confirmation_image"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_gravity="center"
android:src="@drawable/avd_star"
/>
MovieDetailActivity.java
@Override
public void animateConfirmationView() {
Drawable drawable = confirmationView.getDrawable();
if (drawable instanceof Animatable)
((Animatable) drawable).start();
}
效果:
形变动画
粘性的头部
另一方面,我被Google Dialer的实现效果所吸引,当你滑动联系信息的时候,这个图像会变得很小,直到被固定在头部。
Sticky headers通过translationY的值与移动距离来允许title是否被固定在ScrollView顶部,来实现最终效果。
MovieDetailActivity.xml
@Override
public void onScrollChanged(ScrollView scrollView,
int x, int y, int oldx, int oldy) {
if (y > coverImageView.getHeight()) {
movieInfoTextViews.get(TITLE).setTranslationY(
y - coverImageView.getHeight());
if (!isTranslucent) {
GUIUtils.setTheStatusbarNotTranslucent(this);
getWindow().setStatusBarColor(mBrightSwatch.getRgb());
isTranslucent = true;
}
}
if (y < coverImageView.getHeight() && isTranslucent) {
GUIUtils.makeTheStatusbarTranslucent(this);
isTranslucent = false;
}
}
ring