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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值