一個實用的android框架(二)—— UI

原文出處: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:MoviesActivityMovieDetailActivity。其中,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進行注入的庫。它避免了重複的書寫findViewByIdsetOnClickListener的過程。使用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(調色板)。它可以用來提取一張圖片中的主要色調。

Palette效果示例

這些顏色被保存在了一個叫作Swatch的類中。這個類裏面包含了其他的各種屬性,如:背景色,一段可讀文字放在背景色之上的顏色。

使用Palette,你還可以獲取到下列幾種類型的顏色:

  • MutedSwatch
  • VibrantSwatch
  • DarkVibrantSwatch
  • DarkMutedSwatch
  • LightMutedSwatch
  • LightVibrantSwatch

在這個項目中,我使用到了VibrantSwatchDarkVibrantSwatchLightVibrantSwatch

Palletes使用示例

需要注意的是,有的時候可能無法從一張圖片中提取到某一顏色。所以使用的時候,必須要檢查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的顏色也都被設置成了聯繫人圖片的顏色。

Dialer效果

這個效果可以通過給TextView設置一個帶有ColorFilterCompoundDrawable實現。

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

結果:

CompoundDrawable效果

過渡效果

過渡效果在於,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:widthandroid: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)中,另一個引起我注意的是:在聯繫人頁面滾動的時候,標題欄的高度逐漸變小,直到小到一定程度就不再變化。

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

Sticky Header效果圖

*這個部分還有一些小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新特性的首個使用者,當中一定會包含最新的技術,非常適合用來學習。

發佈了33 篇原創文章 · 獲贊 43 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章