一个实用的android框架(二)—— UI

来源:互联网 发布:dedecms中js如何使用 编辑:程序博客网 时间:2024/06/10 09:19

原文出处:http://saulmm.github.io/a-useful-stack-on-android-2-user-interface/

原码github地址:https://github.com/saulmm/Material-Movies

作者:Saúl Molinero

系列文章:

  • 一个实用的android框架(一)——架构
  • 一个实用的android框架(二)—— UI
  • 一个实用的android框架(三)—— 兼容性

这是“一个实用的android架构”系列的第二章节。在第一章节中,我主要介绍了项目的整体架构。在这个章节,我将主要介绍这个项目的UI和设计。

怎么利用材料设计(MaterialDesign)去材料化(materialize)一个安卓应用不在本章的范围之内,在这里有一个David Gonzalez关于这方面做得精彩演讲,你可以用来参考。(译者注:演讲网址可能需要翻墙,题目是What Material Design means to Android,可以百度到对应墙内转载)

通过阅读项目的目录结构可以发现,项目中只有两个Activity:MoviesActivityMovieDetailActivity。其中,MoviesActivity使用RecyclerView来显示所有的电影,MovieDetailActivity则用来显示选中电影的全部信息。

项目地址:Github

app/build.gradle

// Google librariescompile '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 librariescompile '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设置一个带有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);    }

结果:

CompoundDrawable效果

过渡效果

过渡效果在于,MoviesActivity和MovieDetailActivity有一个共享的元素:选定电影的封面。

RecyclerView的Adapter中,指定了需要展示过渡效果控件的transitionName

@Overridepublic 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声明好了需要被共享的元素。

@Overridepublic 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中去获取相应的数据。

@Overridepublic 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

@Overridepublic 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

@Overridepublic 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新特性的首个使用者,当中一定会包含最新的技术,非常适合用来学习。

0 0