原文出处:http://saulmm.github.io/a-useful-stack-on-android-2-user-interface/
原码github地址:https://github.com/saulmm/Material-Movies
作者:Saúl Molinero
系列文章:
这是“一个实用的android架构”系列的第二章节。在第一章节中,我主要介绍了项目的整体架构。在这个章节,我将主要介绍这个项目的UI和设计。
怎么利用材料设计(MaterialDesign)去材料化(materialize)一个安卓应用不在本章的范围之内,在这里有一个David Gonzalez关于这方面做得精彩演讲,你可以用来参考。(译者注:演讲网址可能需要翻墙,题目是What Material Design means to Android,可以百度到对应墙内转载)
通过阅读项目的目录结构可以发现,项目中只有两个Activity:MoviesActivity
和MovieDetailActivity
。其中,MoviesActivity
使用RecyclerView来显示所有的电影,MovieDetailActivity
则用来显示选中电影的全部信息。
项目地址:Github
库
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
在Google提供的新的AppCompat中,一个全新的元素Toolbar被引入了。
简单来说,Toolbar
是一个一般化的ActionBar。这个新的控件实际上是一个ViewGroup,所以我们可以让其包含任意的子View。在这个项目中,我让它包含了一个自定义TextView
,用来显示特定的字体。
在布局中使用这个控件的好处就是:当用户往下滑动的时候,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 xmlns:android="http://schemas.android.com/apk/res/android"
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
Jake Wharton开发的ButterKnife是一个用来给View进行注入的库。它避免了重复的书写findViewById
和setOnClickListener
的过程。使用ButterKnife,代码的可读性会大大提高,也更加的简洁。(译者注:请一定要使用ButterKnifeZelezny!请一定要使用ButterKnifeZelezny!请一定要使用ButterKnifeZelezny!)
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
可以将多个View存放到一个List中,所以你可以使用Setter
或者Actions
一次性的给列表中全部的View设定某一属性。
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
在发布Android L的同时,Google也介绍了一个新的库Palette(调色板)。它可以用来提取一张图片中的主要色调。
这些颜色被保存在了一个叫作Swatch的类中。这个类里面包含了其他的各种属性,如:背景色,一段可读文字放在背景色之上的颜色。
使用Palette
,你还可以获取到下列几种类型的颜色:
MutedSwatch
VibrantSwatch
DarkVibrantSwatch
DarkMutedSwatch
LightMutedSwatch
LightVibrantSwatch
在这个项目中,我使用到了VibrantSwatch
,DarkVibrantSwatch
和LightVibrantSwatch
。
需要注意的是,有的时候可能无法从一张图片中提取到某一颜色。所以使用的时候,必须要检查Palette
的返回结果是否为空。
另外一方面需要考虑的是,抽取颜色的这个过程是很复杂的,因此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
}
}
}
}
在Lollipop的一个典型程序Dialer(联系人)中,我发现了一个有意思的特性。在完整的视图中,所有icon的颜色也都被设置成了联系人图片的颜色。
这个效果可以通过给TextView
设置一个带有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);
}
结果:
过渡效果
过渡效果在于,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);
}
在跳转到详情页面之前,在intent中已经通过ActivityOptionis
声明好了需要被共享的元素。
@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());
}
最后,在详情页面中指定了一个view是被共享的,并从intent中去获取相应的数据。
@Override
public void onCreate(Bundle savedInstanceState) {
...
int moviePosition = getIntent()
.getIntExtra("movie_position", 0);
coverImageView.setTransitionName(
"cover" + moviePosition);
...
任何包含列表页和详情页的应用都可能需要这种过渡效果。但是,如果列表页和详情页之间有可能出现其他的中间页呢。(译者注:即列表页不一定跳到详情页,详情页也不一定返回到列表页)
当用户点击浮动按钮(Floating Action Button)
去给一个电影标注为“喜欢”的时候,一个短暂的过渡页展示了出来,从而告知用户这个操作成功了。
这样一来,返回到列表页的时候,我就不再需要设置sharedElementReturnTransition
来指定过渡效果了。我现在需要考虑是使用一个动画来提高用户体验。只是把电影标记为“喜欢”而不对展示作任何改变是一个糟糕的设计,所以我需要使它看起来更加独特。
当确认页被展示的时候,返回的过渡效果就被覆盖了。因此,共享元素的动画效果不会被展示。这个时候,返回的效果仅仅是activity向下滑动退出:getWindow().setReturnTransition(new Slide());
VectorDrawable
Lollipop中引入的一个有趣的特性就是VectorDrawable。这个新的drawable将带给我们全新的体验:矢量图,图片缩放等。Lollipop也包含了实用的工具来处理这些新的图片。VectorDrawable
支持使用SVG
来定义的图片。例如,这就是一个SVG
格式的星星:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg"
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 xmlns:android="http://schemas.android.com/apk/res/android"
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>
和之前Vector不同的是,这其中包括group和path等标签。android:viewport{Width|Height}
指定了画布(Canvas)的宽高, android:width
和android:height
指定了图片的宽高。
<animated-vector>
支持各种动画效果:通过一组<path>
规定的效果,简单位移,旋转以及其他动画效果和形变。
在这个项目中,一个星星被展示的时候带有放大的效果。当这个页面结束的时候,一个旋转的动画效果展示了出来。同时,这个星星的形状逐渐变成了棒棒糖的形状,然后就转变成了原本的形状(星星)。需要注意的是,想要实现形变的效果,那么数据必须放在同一个SVG
文件中。不然的话,程序就会报错。
`avd_star.xm`
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
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
联合在一起的。其中的target就是需要演示的动画效果:
- 第一个target是
star_group
,它被定义在vd_star.xml
中,它会启动一个缩放和旋转的动画。
appear_rotate.xml
<set
xmlns:android="http://schemas.android.com/apk/res/android"
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>
- 第二个target是一个形变的动画。它是通过另外一个
<objectAnimator>
来讲一个SVG变换成另一个SVG
在此,我想强调的是:形变能够成功的条件是,SVG
文件中的元素必须是一样的,仅仅是在数值上会有差别。
在这个<Set>
中,就定义了将星星的形状转变成棒棒糖,然后再转变回星星。
star_morph.xml
<set xmlns:android="http://schemas.android.com/apk/res/android"
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>
MovieDetailActivity.java
@Override
public void animateConfirmationView() {
Drawable drawable = confirmationView.getDrawable();
if (drawable instanceof Animatable)
((Animatable) drawable).start();
}
Sticky headers
Google联系人(Dialer)中,另一个引起我注意的是:在联系人页面滚动的时候,标题栏的高度逐渐变小,直到小到一定程度就不再变化。
为了实现这个效果,我找了Roman Nurik发布的一段代码(译者注:需要翻墙,文件名为StickyFragment.java,可自行寻找墙内链接)。在这个代码中,通过设置ScrollView
的listener以及View.setTranslationY(float translationY)
实现了这个效果。
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;
}
}
*这个部分还有一些小bug。例如,当你快速滑动的时候,封面图片和标题栏之间会产生一个间隔。欢迎大家上传(Pull)改进代码到Github。*
参考
First look at AnimatedVectorDrawable - Chiu-Ki Chan
VectorDrawables series - Styling android
appcompat v21: material design for pre-Lollipop devices! - Chris Banes
译者总结
这个章节主要介绍了Material Design以及Android L的一些新特性。就目前来说,是开发人员中比较流行的话题。这个项目中涉及到的页面效果都还比较酷炫,很有参考价值。
另外,作者的另一个思路也值得借鉴,那就是参考Google官方应用。在这个章节中,作者多次提到了他的灵感是来源于联系人应用。这些Google官方应用作为Android新特性的首个使用者,当中一定会包含最新的技术,非常适合用来学习。