前言
作为Android 开发者,不仅要学习功能的实现,还需要定制用户的界面。如何制作一款符合大家审美的app是个根令人头疼的问题。
不过好在google在15发布了MD设计规范,帮助程序员设计更好的app。
动画
MD设计规范对于动画的解释
有意义的动画效果
动画效果(简称动效)可以有效地暗示、指引用户。动效的设计要根据用户行为而定,能够改变整体设计的触感。
动效应当在独立的场景呈现。通过动效,让物体的变化以更连续、更平滑的方式呈现给用户,让用户能够充分知晓所发生的变化。
动效应该是有意义的、合理的,动效的目的是为了吸引用户的注意力,以及维持整个系统的连续性体验。动效反馈需细腻、清爽。转场动效需高效、明晰。
如何实现符合MD设计的动画
真实的动作
物理世界中物体拥有质量,所以只有当施加给它们力量的时候才会移动,因此物体没法在瞬间开始或者结束动作。动画突然开始或者停止,或者在运动时突兀的变化方向,都会使用户感到意外和不和谐的干扰。
主要有两点:
- 迅速的加速和平滑的减速会感到自然和愉快
物理世界中物体拥有质量,所以只有当施加给它们力量的时候才会移动,因此物体没法在瞬间开始或者结束动作。动画突然开始或者停止,或者在运动时突兀的变化方向,都会使用户感到意外和不和谐的干扰。- 特殊情况:进入和退出的场景
当一个物体进入这个场景时,请确保它在最高速度下移动,这个行为模拟了自然移动:一个人进入场景的时候,并不是从场景的边缘开始走入的,而是从更远的地方。当然,一个物体退出这个场景时,需要维持它的速度,缓慢的离开场景,逐渐的进入和缓慢的离开会把用户的注意力吸引到这个动作上,在大多数情况下,这是你希望的效果。
不是所有物体的移动方式是相同的,轻的/小的物体可能会更快的加速和减速,因为它们质量比较小,所以只需要施加给他们较少的力就可以。大的/重的物体可能花需要更多的时间来到达他的最高速度或者回到停止状态。仔细琢磨如何将他们的动作应用到你的应用的UI元素中。
下面分析一下开源的两个app的动画实现:
FAB随着list滑动,下拉退出,上拉出现
简影讯这个FAB会根据RecycleView的滑动来进入和退出界面,符合第二种动画。既要以最大速度退出和进入。下面看代码实现:
布局类似在CoordinatorLayout
下添加RecycleView
加上FloatingActionButton
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:title="MdView" />
<android.support.design.widget.TabLayout
android:id="@+id/tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<android.support.design.widget.FloatingActionButton
app:layout_behavior="com.othershe.mdview.ScrollAwareFABBehavior"
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="16dp"
android:src="@android:drawable/ic_menu_share" />
</android.support.design.widget.CoordinatorLayout>
关键代码:app:layout_behavior="com.othershe.mdview.ScrollAwareFABBehavior"
这行是用来控制fab行为的,而这个行为是用户自定义的。
具体代码可以参考浮动操作按钮详解
在此之前要先了解Behavior
这个类:
参考文章:深入理解CoordinatorLayout.Behavior
在android5.0之后新的嵌套滑动机制中,引入了:NestScrollChild和NestedScrollingParent两个接口,用于协调子父控件滑动状态,而CoordinatorLayout实现了NestedScrollingParent接口,在实现了NestScrollChild这个接口的子控件在滑动时会调用NestedScrollingParent接口的相关方法,将事件发给父控件,由父控件决定是否消费当前事件,在CoordinatorLayout实现的NestedScrollingParent相关方法中会调用Behavior内部的方法。
我们实现Behavior的方法,就可以嵌入整个CoordinatorLayout所构造的嵌套滑动机制中,可以获取到两个方面的内容:
1、某个view监听另一个view的状态变化,例如大小、位置、显示状态等
需要重写layoutDependsOn
和onDependentViewChanged
方法
2、某个view监听
CoordinatorLayout
内NestedScrollingChild
的接口实现类的滑动状态
重写onStartNestedScroll
和onNestedPreScroll
方法。注意:是监听实现了NestedScrollingChild
的接口实现类的滑动状态,这就可以解释为什么不能用ScrollView而用NestScrollView来滑动了。
下面看怎么定义这个fab行为类的:
- 继承FAB的行为类
FloatingActionButton.Behavior
private boolean mIsAnimatingOut = false;
private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL ||
super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target,
nestedScrollAxes);
}}
重写了onStartNestedScroll
这个方法,这个方法是用来决定是否要响应CoordinatorLayout
的滚动,返回ture 表示接收。
- 重写onNestedScroll处理滑动事件,包括定义fab进入和退出的动画。
@Override
public void onNestedScroll(final CoordinatorLayout coordinatorLayout, final FloatingActionButton child,
final View target, final int dxConsumed, final int dyConsumed,
final int dxUnconsumed, final int dyUnconsumed) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
if (dyConsumed > 0 && !mIsAnimatingOut && child.getVisibility() == View.VISIBLE) {
animateOut(child);
} else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
animateIn(child);
}
}
- 因为FloatingActionButton.Behavior的基类已经有了animateIn() 和 animateOut()方法,同时它也设置了一个私有变量mIsAnimatingOut,这些方法和变量都是私有的,所以现在我们需要重新实现这些动画方法。
private void animateOut(final FloatingActionButton button) {
ViewCompat.animate(button).translationY(button.getHeight() + getMarginBottom(button))
.setInterpolator(INTERPOLATOR).withLayer()
.setListener(new ViewPropertyAnimatorListener() {
public void onAnimationStart(View view) {
mIsAnimatingOut = true;
}
public void onAnimationCancel(View view) {
mIsAnimatingOut = false;
}
public void onAnimationEnd(View view) {
mIsAnimatingOut = false;
view.setVisibility(View.INVISIBLE);
}
}).start();
}
private void animateIn(FloatingActionButton button) {
button.setVisibility(View.VISIBLE);
ViewCompat.animate(button).translationY(0)
.setInterpolator(INTERPOLATOR).withLayer().setListener(null)
.start();
}
private int getMarginBottom(View v) {
int marginBottom = 0;
final ViewGroup.LayoutParams layoutParams = v.getLayoutParams();
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
marginBottom = ((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin;
}
return marginBottom;
}
这里核心控制进出是否为最大速度的就是插值器
private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
- 最后一步就是把这个CoordinatorLayout Behavior与浮动操作按钮联系起来。我们可以在xml的自定义属性pp:layout_behavior中定义它:
<android.support.design.widget.FloatingActionButton
app:layout_behavior="com.codepath.floatingactionbuttontest.ScrollAwareFABBehavior" />
- 因为我们是在xml中静态的定义这个behavior,为了让 layout inflation顺利进行,我们必须实现一个构造函数。
public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {
// ...
public ScrollAwareFABBehavior(Context context, AttributeSet attrs) {
super();
}
响应式交互
响应式交互能让用户信任,并且吸引他们。 当用户操作一个美观且符合常理的应用时,他们会感到满意甚至很高兴。那是一种经过深思熟虑、有目的、非随机的而且可以带有轻微异想天开但不会让人分心的交互。
在 material design 中,应用是响应式的并且渴望用户操作的:
- 触摸,语音,键盘及鼠标作为首要考虑的输入方式。
- 虽然 UI 元素是有形的,但是他们被限制在屏幕里面(电脑或者移动设备的屏幕),视觉元素和动效能减少这种割裂,让用户能够立即感知自己的操作。
总结来说就是:让用户知道他点击了某个按钮
同样是剪影讯源码分析:
RecyclerView的item里面使用了cardView,使用了卡片效果。
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:layout_margin="3dp"
android:id="@+id/cardview"
android:foreground="?android:attr/selectableItemBackground"
>
<LinearLayout
android:id="@+id/item_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
>
<TextView
android:id="@+id/item_tv"
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center" />
</LinearLayout>
</android.support.v7.widget.CardView>
cardView的
android:clickable="true"
android:foreground="?android:attr/selectableItemBackground"
这两行设置了水波纹的点击效果:
除了selectableItemBackground
还有一种水波纹
selectableitembackgroundborderless
只能在v21以上使用
要注意一点,在adapter里面设置点击事件的时候一定要设置最外层的布局的点击事件才有效的实现,否则冲突就无法实现点击的水波纹。
holder.cardView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
}
});
水波纹可以加在所有可以点击的控件上,比如Button
<Button
android:foreground="?android:attr/selectableItemBackground"
android:layout_width="match_parent"
android:layout_height="50dp" />
有意义的转场动画
为了让界面切换不显的突兀,android里设置了专场动画。Activity的转场动画很早就有,但是太过于单调,样式也不好看,于是Google在Android5.0之后,又推出的新的转场动画,效果还是非常炫的。
参考文章: Android5.0之Activity的转场动画
- 旧转场动画回顾
首先我们还是先来看看在5.0之前如果我们想要在启动Activity时使用动画该怎么做呢?
startActivity(new Intent(this, Main3Activity.class));
overridePendingTransition(R.anim.in,R.anim.out);
对应的入场和出场动画就是两个补间动画,如下:
入场动画:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:toYDelta="0"
android:fromYDelta="100%"
android:duration="1500"/>
</set>
出场动画:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="1500"
android:fromYDelta="0"
android:toYDelta="-100%"/>
</set>
这种动画是针对整个Activity而言的,无法设置Activity中元素的入场/出场动画。
- 5.0之后的转场动画
分为两种: - 分解、滑动进入、淡入淡出
分解
先来看一张效果图:
就是这样一种效果,那我们接下来看看这种效果要怎么实现。
首先,把之前启动Activity的代码改成下面的写法:
startActivity(new Intent(this, Main2Activity.class), ActivityOptions.makeSceneTransitionAnimation(this).toBundle());
添加完成之后,在Main2Activity中设置该Activity的进出场动画即可:
getWindow().setEnterTransition(new Explode().setDuration(2000));
getWindow().setExitTransition(new Explode().setDuration(2000));
大家一定要记得在styles.xml文件中添加下面一行代码,表示激活Activity中元素的过渡效果:
<item name="android:windowContentTransitions">true</item>
滑动进入
有了上面的步骤,再设置滑动进入就很简单了,只需要修改Main2Activity中的两行代码即可:
getWindow().setEnterTransition(new Slide().setDuration(2000));
getWindow().setExitTransition(new Slide().setDuration(2000));
显示效果如下:
淡入淡出
Main2Activity修改代码如下:
getWindow().setEnterTransition(new Fade().setDuration(2000));
getWindow().setExitTransition(new Fade().setDuration(2000));
显示效果如下:
Activity的进场和退出可以分别设置不同的动画效果,如果没有分别设置,则进场和退出的动画反过来。比如退出是变淡,则进场就是变深。<只在设置了专场动画的两个Activity里>
-
元素共享
共享元素动画
共享元素动画是一个非常神奇的东东,我们先来看看效果:
可能这个Gif动画还不太清晰,我再来解释一下,在MainActivity和Main2Activity里边都有一个Button,只不过一个大一个小,从MainActivity跳转到Main2Activity时,我并没有感觉到Activity的跳转,只是觉得好像第一个页面的Button放大了,同理,当我从第二个页面回到第一个页面时,也好像Button变小了。OK,这就是我们的Activity共享元素。
当两个Activity中有同一个控件的时候,我们便可以采用共享元素动画。
使用共享元素动画的时候,我们需要首先给MainActivity和Main2Activity中的两个button分别添加Android:transitionName="mybtn"属性,并且该属性的值要相同,这样系统才知道这两个控件是共享元素。设置完成之后,接下来就是启动Activity的代码了,如下:
startActivity(new Intent(this,Main2Activity.class), ActivityOptions.makeSceneTransitionAnimation(this,view,"mybtn").toBundle());
还是上面那种启动方式的重载方法,只不过这里多了两个参数,view表示MainActivity中的共享元素(就是那个Button),第二个参数表示布局文件中transitionAnimation属性的值。OK,就这么简单。
多元素共享:
那我如果两个页面中有多个共享元素该怎么办呢?简单,android:transitionName属性还像上面一样设置,然后在启动Activity时我们可以通过Pair.create方法来设置多个共享元素,如下:
startActivity(new Intent(this, Main2Activity.class),
ActivityOptions.makeSceneTransitionAnimation(this, Pair.create(((View) iv1),"myiv"), create(((View) textView),"mytv")).toBundle());\
分析简影讯里使用的过场动画:
1.主界面的RecycleView进入的过渡动画:
如下:
分析源码:
这个是RecycleView加载Item的动画,所以写在RecycleView的Adapter。
其实RecycleView内部是可以设置Item的动画的,使用这个方法:
mRecyclerView.setItemAnimator(new DefaultItemAnimator(mRecyclerView));
除了默认的动画还有其他内置的动画:
SlideInOutLeftItemAnimator : which applies a slide in/out from/to the left animation
SlideInOutRightItemAnimator : which applies a slide in/out from/to the right animation
SlideInOutTopItemAnimator : which applies a slide in/out from/to the top animation
**SlideInOutBottomItemAnimator **: which applies a slide in/out from/to the bottom animation
ScaleInOutItemAnimator : which applies a scale animation
**SlideScaleInOutRightItemAnimator **: which applies a scale animation with a slide in/out from/to the right animation
但是我们都不用,所以自己写在Adapter里面:
定义属性动画:
protected Animator[] getAnimators(View view) {
return new Animator[]{
ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1f).setDuration(4000),
ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, 200, 0).setDuration(4000)
};
}
写一个函数设置是否加载动画:
public void setShowAnim(boolean showAnim) {
isShowAnim = showAnim;
}
在onBindViewHolder里为每一个item设置动画:
private int mLastPosition = -1;
Animator[] animators = getAnimators(holder.itemView);
if (isShowAnim && animators != null && animators.length > 0
&& holder.getAdapterPosition() > mLastPosition) {
if (animators.length > 1) {
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(animators);
animatorSet.start();
} else {
for (Animator animator : animators) {
animator.start();
}
}
mLastPosition = holder.getAdapterPosition();
}
mLastPosition 用来判断是不是最后一个,否则不设置加载动画。
2.进入详情页面的动画
2017-03-12_17_01_46_0-215.gif过度动画:在第二个Activity里设置:
public static void navigation(Activity activity, View view, MovieModel movieModel) {
Intent intent = new Intent(activity, MovieDetailActivity.class);
intent.putExtra("movie_model", movieModel);
if (Build.VERSION.SDK_INT >= 21) {
activity.getWindow().setExitTransition(new Explode());
ActivityCompat.startActivity(activity, intent,
ActivityOptions.makeSceneTransitionAnimation(activity).toBundle());
} else {
ActivityOptionsCompat option = ActivityOptionsCompat.makeScaleUpAnimation(view, 0, 0,
view.getMeasuredWidth(), view.getMeasuredHeight());
ActivityCompat.startActivity(activity, intent, option.toBundle());
}
}
第一个Activity里跳转调用:
startActivity(new Intent(MainActivity.this,DetailActivity.class), ActivityOptions.makeSceneTransitionAnimation(MainActivity.this,view,"mybtn").toBundle());
打动用户的细节
这里主要是一些图标的小动画
网友评论