曲线运动-1

英文原文:Curved Motion – Part 1 

Material design 指南提倡使用真实的动作,Play Store app最近的更新里面,在从列表到详情的时候提供了一个曲线运动。在本系列文章中,我们将看到如何实现曲线运动。

Material design 指南建议:

Not all objects move the same way. Lighter or smaller objects may move faster because they require less force, and larger or heavier objects may need more time to speed up.

并不是所有的对象都以同样的方式运动。浅色点或者小点的对象应该移动得快些,因为它们需要的推力更小(模拟物理世界),而大点的或者重点的对象需要更多的时间来加速。

Use curved motion and avoid linear spatial paths. Identify the qualities of motion best suited to your object, and represent their motion accordingly. Curves represent that change over time, for a particular value range. Find a curve that fits that character of motion you are describing.

如果你看看目前Play Store 的app版本,你就会发现在切换到详情界面或者从详情界面返回的时候,图片是跟一个曲线路径移动的,而不是直线:

对于那些能够使用minSdkVersion=“21”的幸运儿这真的非常简单,因为Transitions 框架为你做了许多工作。我们以前在Styling Android上讲到过Transitions,因此本文不会深入其工作方式 - 我们关心的是如何使用这些Transitions中的曲线运动。

为此,我们先为Transitions定义两个代表场景(scene)的布局:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  <View
    android:id="@+id/view"
    android:layout_width="@dimen/view_size"
    android:layout_height="@dimen/view_size"
    android:layout_gravity="top|start"
    android:background="@color/sa_accent" />
</FrameLayout>

--

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  <View
    android:id="@+id/view"
    android:layout_width="@dimen/view_size"
    android:layout_height="@dimen/view_size"
    android:layout_gravity="bottom|end"
    android:background="@color/sa_accent" />
</FrameLayout>

除了内部View在父亲中的位置不一样之外,这两个布局是相同的。这将模拟view的不同位置以便使用Transitions 框架来创建过渡动画。

接下来我们看看我们的Activity:

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private FrameLayout container;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        container = (FrameLayout) findViewById(R.id.container);
        setupToolbar();
        setLollipopAnimator();
    }
    private void setupToolbar() {
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.setTitle(R.string.app_name);
        }
    }
    private void setLollipopAnimator() {
        LollipopSceneAnimator.newInstance(this, container, R.layout.scene1, R.layout.scene2, R.transition.arc1);
    }
}

这里没有特别之处。我选择了将所有的Transitions 逻辑封装在一个外部类中(LollipopSceneAnimation),好让今后能轻易的替换成别的实现方式。我们提供了几个参数来初始化它,分别是一个Context,scene的container ,两个scene的布局id以及一个Transition id (关键部分)。

LollipopSceneAnimation.java

final class LollipopSceneAnimator implements SceneAnimator {
    private final TransitionManager transitionManager;
    private Scene scene1;
    private Scene scene2;
    public static LollipopSceneAnimator newInstance(@NonNull Context context, @NonNull ViewGroup container,
                                            @LayoutRes int layout1Id, @LayoutRes int layout2Id, @TransitionRes int transitionId) {
        TransitionManager transitionManager = new TransitionManager();
        LollipopSceneAnimator sceneAnimator = new LollipopSceneAnimator(transitionManager);
        Scene scene1 = createScene(sceneAnimator, context, container, layout1Id);
        Scene scene2 = createScene(sceneAnimator, context, container, layout2Id);
        Transition transition = TransitionInflater.from(context).inflateTransition(transitionId);
        transitionManager.setTransition(scene1, scene2, transition);
        transitionManager.setTransition(scene2, scene1, transition);
        transitionManager.transitionTo(scene1);
        sceneAnimator.scene1 = scene1;
        sceneAnimator.scene2 = scene2;
        return sceneAnimator;
    }
    private static Scene createScene(@NonNull LollipopSceneAnimator sceneAnimator, @NonNull Context context,
                                     @NonNull ViewGroup container, @LayoutRes int layoutId) {
        Scene scene = Scene.getSceneForLayout(container, layoutId, context);
        scene.setEnterAction(new EnterAction(sceneAnimator, scene));
        return scene;
    }
    private LollipopSceneAnimator(TransitionManager transitionManager) {
        this.transitionManager = transitionManager;
    }
    private void sceneTransition(Scene from) {
        if (from == scene1) {
            transitionManager.transitionTo(scene2);
        } else {
            transitionManager.transitionTo(scene1);
        }
    }
    private static final class EnterAction implements Runnable, View.OnClickListener {
        private final LollipopSceneAnimator sceneAnimator;
        private final Scene scene;
        private EnterAction(@NonNull LollipopSceneAnimator sceneAnimator, @NonNull Scene scene) {
            this.sceneAnimator = sceneAnimator;
            this.scene = scene;
        }
        @Override
        public void run() {
            ViewGroup sceneRoot = scene.getSceneRoot();
            View view = sceneRoot.findViewById(R.id.view);
            view.setOnClickListener(this);
        }
        @Override
        public void onClick(View v) {
            sceneAnimator.sceneTransition(scene);
        }
    }
}

这次也没有什么特别的地方。在newInstance()中我们根据提供的layout id inflate 了两个scenes ,然后把它们连接到一个TransitionManager。EnterAction实现了一个OnClickListener接口,在scene进入的时候添加一个点击事件,以达到在两个scene 间来回切换的目的。

如果我们把这和一个标准的ChangeBounds Transition一起使用,则会让view在两个scene 的两个位置之间来回直线运动。但是如果你运行我们的代码实际得到的是一个曲线路径:

未命名.gif

其原因是我们传递到newInstance()里面的 transition id,它会inflated 进一个Transition对象。让我们来看看这个transition :

res/transition-v21/arc1.xml

<?xml version="1.0" encoding="utf-8"?>
<changeBounds xmlns:android="http://schemas.android.com/apk/res/android"
  android:duration="500">
  <arcMotion
    android:maximumAngle="90"
    android:minimumHorizontalAngle="15"
    android:minimumVerticalAngle="0" />
</changeBounds>

因此这也是一个ChangeBounds  transition,但是具有一个ArcMotion的子元素,就是它为我们做了所有复杂的工作。它会在开始点和结束点之间计算出一个合理的圆弧,但是里面设置的属性会让这个圆弧在起始点和结束点离得太近的情况下变成一条水平或者竖直的直线。

所以,现在你已经使用了Transitions了,只需通过简单的自定义你的Transition,就可以轻易的让transitions从直线变成弧线。

但是,我们大多数人并不能使用它,因此在下一篇文章中,我们将使用兼容到api 11的属性动画来实现。

这篇文章的源代码在这里