译文-Android的MVP模式开发系列文章(2)

原文标题:A useful stack on android #2, user interface
本文是对该文章的翻译,如有错误,望朋友们指出,谢谢。
原作者这个系列目前写了三篇,第三篇文章讲的更多的是兼容性的实现,有兴趣的朋友可以去阅读原文,暂时先不翻译了,我将考虑模仿作者实现一个简单的App,算是对这一系列文章中内容中所提到的各个知识点的巩固。


译文如下:

这是'A useful stack on android'系列的第二篇文章,在第一篇中,我介绍了该项目的架构,而本文的重点则是用户界面以及App的整体设计。

我不想谈论如何用Material Design去实现一个App,你能通过这个网站找到很好的描述,作者是David Gonzalez。

该项目的设计结构只有两个Activity,一个是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

ToolBar是对原来的ActionBar泛化。

这个新的组件是一个ViewGroup,所以我们能够放views到它里面,在我的例子中就包含了一个使用特别字体的自定义TextView。

这个组件在我的布局中有一个特点,当用户向下滑动的时候,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 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

ButterKnife的作者是 Jake Wharton,这是一个用于视图注入的类库。

这个框架避免了重复写诸如此类的句子 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();
}

调色板

Android L介绍了一个新的类库,Palette。它能够提取出一个图片的主色。
Palette效果

这些颜色是被聚合到一个叫Swatch的容器中,它包含了背景色和可读文字的背景色。

通过 Palette 你能获取下面的颜色:

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

在这个App中,我将用到VibrantSwatch, DarkVibrantSwatchLightVibrantSwatch

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

一个十分有趣的特性在lollipop中被介绍到,那就是VectorDrawables

一个崭新的drawable,一个全新的世界被打开,比如矢量图形,图像缩放等等。

Lollipop也包含了强有力的工具去处理这些图形。

VectorDrawable 使用了SVG 的一部分,比如,下面是一个SVG格式的星星。
star

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

译者注:上面的代码是svg的代码,不是Android中的代码,关于怎么查看svg这里附上一个w3c的简单说明

接下来,用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>

这里有一点不同,这里叫做group,paths等等。android:viewport{Width|Height}指定了画布的大小,而android:widthandroid:height指定了图片的大小。

允许包含动画,如移动,旋转和变形。

举个例子,一个星星在执行缩放动画效果的时候也同时执行着旋转动画,同时星星的形状也改变为一根棒棒糖的形状,最后动画结束又变回星星的形状。

值得注意的一点,让它显示一个形变的动画的话也需要含有一样的SVG命令,否则会有异常触发。


avd_star.xml

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

这个vd_star.xml是有所关联的,是这些动画实现的基础。

  • 第一部分,start_group

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>
  • 第二部分,star_morph,是使用将SVG数据改为另外一个SVG数据

当星星的形状改变为一个棒棒糖形状之后,它将变回星星形状。

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>

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

尝试去复制这个效果吧,我已经找到这个Roman Nurik,View.setTranslationY(float translationY)这个方法设置的属性将被用在ScrollView的监听事件中。

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

Resources:

2015-03-17 00:0516