[置顶] android应用框架系列二,图形界面
A useful stack on android #2, user interface
13 Feb 2015原文链接:http://saulmm.github.io/a-useful-stack-on-android-2-user-interface/原文作者:Saúl Molinero
这是‘A useful stack on android’系列文章的第二篇,在第一篇中我回顾了项目的总体架构,这篇文章的重点是图形界面和应用的整体设计。
我不愿意谈论如何使用Material Design实现APP,你可以在这里找到David Gonzalez的非常不错的描述。
看一下结构设计,这里仅仅两个Activity:MoviesActivity
拥有一个RecyclerView包涵了所有的电影,MovieDetailActivity
用来显示所选电影的详细内容。
这个项目在GitHub上可看见。
Libraries
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
是为了对旧的Action Bar泛化。
这个新的widget是一个ViewGroup,所以我们可以在其放入views,在我的例子中包涵了一个自定义的拥有特殊字体的TextView
。
这个组件的优势是,当用户向下滑动时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
orsetOnClickListener(new OnClick...)
.
使用ButterKnife,代码可读性更强,另外代码变少了。
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
,能允许注解多个views到一个list,所以可以用接口例如Setters
或者Actions
对列表中的所有views执行一次操作。
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); }};
在我的例子里所有的TextViews
显示的电影信息都被设置了一个确定的字体颜色。
MoviesActivity.java
ButterKnife.apply(movieInfoTextViews, GUIUtils.setter, lightSwatch.getTitleTextColor());
使用ButterKnife
你同样可以处理一些view事件:
@OnClick(R.id.activity_movie_detail_fab)public void onClick() { showConfirmationView();}
Palette
在Google的Android L发布时候介绍了一个新的类库叫做Palette,它能够提取图像的主要颜色。
这些颜色集中在Swatch容器中,它包涵了背景色和文字的背景色。
使用Palette你能获取下面的Swatch设置颜色:
MutedSwatch
VibrantSwatch
DarkVibrantSwatch
DarkMutedSwatch
LightMutedSwatch
LightVibrantSwatch
在这个应用中我用到了:VibrantSwatch
,DarkVibrantSwatch
和LightVibrantSwatch
.
注意:你不能总是提取到一张图片的特定颜色,所以记住检测Palette有没有返回null集合。
另一面,需要考虑提取颜色是一个复杂的任务,所以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这个APP一些有趣的想法,在联系人详细界面,图标被渲染为联系人的背景色:
这个效果能通过ColorFilter动态获取,然后给TextView设置颜色。
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); }
效果:
Transitions
MoviesActivity
和MovieDetailActivity
两个activity之间的transition,利用所选的电影图片作为shared element。
在RecyclerView
的adapter 指定transitionName
将用来执行transition。
@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); }
在跳转到详细activity之前,需要指定一个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()); }
最后,在详细activity里,从进来的intent获取meta data数据,来指定显示那个view(电影图片等)。
@Override public void onCreate(Bundle savedInstanceState) { ... int moviePosition = getIntent() .getIntExtra("movie_position", 0); coverImageView.setTransitionName( "cover" + moviePosition); ...
在任何包涵list和详细view的应用中,这些都是必须的,但是如果在详细视图与返回列表之间有一个中间状态呢?
当用户按下Floating Action Button
收藏该电影,一个简短的视图展示用来通知用户保存成功。
在这之后我不想利用sharedElementReturnTransition
实现transition,为了返回主activity,我更喜欢显示一个动画来改变用户体验。
不能让被标记为喜欢(收藏)的电影在返回的时候不做任何事情,所以设计它表现的不同。
当确认视图被显示,返回的transition需要重写,电影照片切换动画效果不会显示,给activity设置新返回效果:getWindow().setReturnTransition(new Slide());
VectorDrawable
Lollipop 介绍了一个非常有趣的特性VectorDrawables.
使用新的drawables,打开一个矢量绘图,图片缩放等等的新世界。
Lollipop同样包涵了一些强力的工具去处理这些新的图形al。
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>
这里有一点不同,这里是groups,paths,等,android:viewport{Width|Height}
指定了canvas大小, 而android:width
&android:height
指定了图片大小。
animated-vector>
允许<paths>
动画组,平移、旋转,和其它动画,变形。
在例子中,一个星星执行缩放动画,当其停止,一个旋转动画展示,同时星星的形状变为一个棒棒糖的形状,当再次改变又会变为原来星星的形状。
值得注意的是,显示一个变形动画,数据需要包涵相同的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>
这个<animated-vector>
和drawablevd_star.xml
是有关联的,targets是被动画显示的元素:
- 首先在动画group:
start_group
定义在 vector: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>
- 其次一个变形动画被使用,这是另一个
<objectAnimator>
从SVG
数据变为另一个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>
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(); }
效果:
Sticky headers
另一方面,Google Dialer吸引我的地方是当你在一个联系人信息里scroll
(下拉滑动),这个图片会变小直到滑动到一个点,在这个点图像会被恢复。
为了复制这个效果,我找到了Roman Nurik的一个代码提交,通过设置属性:在ScrollView
监听器中使用View.setTranslationY(float translationY)
方法来实现这个目标。
为获得这个效果,translationY
的值与ScrollView
共同作用允许设置title的位置。
MovieDetailActivity.java
@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,例如:如果你非常快的滑动会在图片和标题间出现空白,欢迎回复!
Resources:
-
First look at AnimatedVectorDrawable- Chiu-Ki Chan
-
VectorDrawables series- Styling android
-
appcompat v21: material design for pre-Lollipop devices!- Chris Banes
更多相关文章
- Android动画TweenAnimation的使用方法
- Android(安卓)用Animation-list实现逐帧动画
- Android(安卓)在代码中动态设置字体颜色需要注意的问题
- android selector 需要顺序的啊
- android 实现ImageView按压效果和解决背景图片拉申问题
- Button 有按下效果
- android TextView怎么设置个别字体颜色并换行?
- Android开发:Listview 多屏显示 item多选 操作错误 的解决
- Android动态设置主题样式