原文出處: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新特性的首個使用者,當中一定會包含最新的技術,非常適合用來學習。