Android Spring动画–基于物理的动画

时间:2020-02-23 14:29:17  来源:igfitidea点击:

在本教程中,我们将实现基于Spring的动画,这些动画是我们android应用程序中支持库的一部分。
Spring Animation是Android Physics Based动画API的一部分。

Android Spring动画

Android Spring Animation会根据弹簧特性(潮湿度,刚度和弹性)对视图进行动画处理。

一旦在build.gradle的dependencies部分中添加以下依赖项,即可在项目中实现Spring Animation:

dependencies {
    implementation 'com.android.support:support-dynamic-animation:27.1.1'
}

要创建Spring动画,我们需要创建一个SpringAnimation类,并传递动画类型-平移/旋转/缩放以及动画的最终位置和动画速度。

我们还可以设置其他属性,例如刚度和阻尼。

现实生活中的弹簧回到其最终位置时会反弹。

阻尼越高,刚度越低,则其振荡/反弹越多。

要为视图设置动画,必须始终在之前分配视图的最终位置。

要开始动画,我们调用" start()"或者" animateToFinalPosition(Float finalPosition)"。
后者更新最终位置并在内部调用start()。

此外,updateListener和removeListener分别是侦听器,用于侦听更新并在不再需要时删除侦听器。

简单的Spring动画

在Java中,要在任何视图上设置Spring动画,我们可以:

SpringAnimation springAnim = new SpringAnimation(fab, SpringAnimation.TRANSLATION_Y);
SpringForce springForce = new SpringForce();
springForce.setFinalPosition(-200f);
springForce.setStiffness(SpringForce.STIFFNESS_LOW);
springForce.setDampingRatio(SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
springAnim.setSpring(springForce);
springAnim.start();

该头寸必须为浮动值。
Y方向的负数是向上的。
X方向的负数向左。
fab是动画发生的视图实例。

像运动一样拖动视图弹簧

我们还可以拖动某个视图,然后看到它像Spring一样反弹。
为此,我们需要在视图上设置触摸监听器。

private SpringAnimation xAnimation;
private SpringAnimation yAnimation;
ImageView imageView;

private void imageViewDragSpringAnimation() {

      imageView.getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener);
      imageView.setOnTouchListener(touchListener);
  }

  private ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
      @Override
      public void onGlobalLayout() {
          xAnimation = createSpringAnimation(imageView, SpringAnimation.X, imageView.getX(),
                  SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
          yAnimation = createSpringAnimation(imageView, SpringAnimation.Y, imageView.getY(),
                  SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
      }
  };

  private View.OnTouchListener touchListener = new View.OnTouchListener() {
      @Override
      public boolean onTouch(View v, MotionEvent event) {
          switch (event.getActionMasked()) {
              case MotionEvent.ACTION_DOWN:
                  dX = v.getX() - event.getRawX();
                  dY = v.getY() - event.getRawY();
                  //cancel animations
                  xAnimation.cancel();
                  yAnimation.cancel();
                  break;
              case MotionEvent.ACTION_MOVE:
                  imageView.animate()
                          .x(event.getRawX() + dX)
                          .y(event.getRawY() + dY)
                          .setDuration(0)
                          .start();
                  break;
              case MotionEvent.ACTION_UP:
                  xAnimation.start();
                  yAnimation.start();
                  break;
          }
          return true;
      }
  };

  public SpringAnimation createSpringAnimation(View view,
                                               DynamicAnimation.ViewProperty property,
                                               float finalPosition,
                                               float stiffness,
                                               float dampingRatio) {
      SpringAnimation animation = new SpringAnimation(view, property);
      SpringForce springForce = new SpringForce(finalPosition);
      springForce.setStiffness(stiffness);
      springForce.setDampingRatio(dampingRatio);
      animation.setSpring(springForce);
      return animation;
  }

当ImageView显示在屏幕上并确定其宽度和高度时,将触发" GlobalLayoutListener"。

完成此操作后,我们将SpringAnimation的最终位置设置为静止时ImageView的当前X和Y坐标。

" onTouchListener"用于在屏幕上拖动视图。

当拖动开始时,我们捕获到视图的左上角和触摸点之间的差异,并且在移动视图时,差异会添加到当前位置。

当拖动停止时,SpringAnimation被取消,视图返回其原始位置。

连锁Spring动画

在这种类型的动画中,我们将视图组合在一起以使其动画在一起。

每个动画的动画取决于其弹簧属性。

在以下部分中,我们将在Android Studio项目中实现上述每种类型的Spring动画。

项目结构

Android Spring动画代码

下面给出了activity_main.xml布局的代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="https://schemas.android.com/apk/res/android"
  xmlns:app="https://schemas.android.com/apk/res-auto"
  xmlns:tools="https://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity">

  <android.support.design.widget.AppBarLayout
      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:popupTheme="@style/AppTheme.PopupOverlay" 

  </android.support.design.widget.AppBarLayout>

  <include layout="@layout/content_main" 

</android.support.design.widget.CoordinatorLayout>

下面给出了content_main.xml布局的代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="https://schemas.android.com/apk/res/android"
  xmlns:app="https://schemas.android.com/apk/res-auto"
  xmlns:tools="https://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  app:layout_behavior="@string/appbar_scrolling_view_behavior"
  tools:context=".MainActivity"
  tools:showIn="@layout/activity_main">

  <android.support.design.widget.FloatingActionButton
      android:id="@+id/fab"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_margin="@dimen/fab_margin"
      android:layout_marginBottom="16dp"
      android:layout_marginStart="16dp"
      app:backgroundTint="@color/colorPrimary"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintStart_toStartOf="parent" 

  <android.support.design.widget.FloatingActionButton
      android:id="@+id/fab2"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_margin="@dimen/fab_margin"
      android:layout_marginBottom="16dp"
      android:layout_marginEnd="16dp"
      app:backgroundTint="@android:color/black"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent" 

  <android.support.design.widget.FloatingActionButton
      android:id="@+id/fab3"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_gravity="bottom|end"
      android:layout_margin="@dimen/fab_margin"
      android:layout_marginBottom="16dp"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintLeft_toLeftOf="parent"
      app:layout_constraintRight_toRightOf="parent" 

  <android.support.design.widget.FloatingActionButton
      android:id="@+id/fab4"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_margin="@dimen/fab_margin"
      app:backgroundTint="@android:color/holo_green_dark"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintLeft_toLeftOf="parent"
      app:layout_constraintRight_toRightOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" 

  <android.support.design.widget.FloatingActionButton
      android:id="@+id/fab5"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_margin="@dimen/fab_margin"
      app:backgroundTint="#1A1A1A"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintHorizontal_bias="0.4"
      app:layout_constraintLeft_toLeftOf="parent"
      app:layout_constraintRight_toRightOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@+id/fab4" 

  <android.support.design.widget.FloatingActionButton
      android:id="@+id/fab6"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_margin="@dimen/fab_margin"
      app:backgroundTint="@android:color/holo_red_dark"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintLeft_toLeftOf="parent"
      app:layout_constraintRight_toRightOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@+id/fab5" 

  <ImageView
      android:id="@+id/imageView"
      android:layout_width="50dp"
      android:layout_height="50dp"
      android:background="#FF3456"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintLeft_toLeftOf="parent"
      app:layout_constraintRight_toRightOf="parent"
      app:layout_constraintTop_toTopOf="parent" 

</android.support.constraint.ConstraintLayout>

我们创建了六个FloatingActionButton。
三个用于简单的Spring动画,三个用于链接的Spring动画。

唯一的ImageView将用于显示拖动视图如何导致Spring Animation。

MainActivity.java的代码如下:

package com.theitroad.androidspringanimations;

import android.os.Bundle;
import android.support.animation.DynamicAnimation;
import android.support.animation.SpringAnimation;
import android.support.animation.SpringForce;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.MotionEvent;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ImageView;

public class MainActivity extends AppCompatActivity {

  private SpringAnimation xAnimation;
  private SpringAnimation yAnimation;
  ImageView imageView;

  private float dX;
  private float dY;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      Toolbar toolbar = findViewById(R.id.toolbar);
      setSupportActionBar(toolbar);

      imageViewDragSpringAnimation();
      chainedSpringAnimation();

      final FloatingActionButton fab = findViewById(R.id.fab);
      final FloatingActionButton fab2 = findViewById(R.id.fab2);
      final FloatingActionButton fab3 = findViewById(R.id.fab3);

      fab.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              SpringAnimation springAnim = new SpringAnimation(fab, SpringAnimation.TRANSLATION_Y);
              SpringForce springForce = new SpringForce();
              springForce.setFinalPosition(-200f);
              springForce.setStiffness(SpringForce.STIFFNESS_LOW);
              springForce.setDampingRatio(SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
              springAnim.setSpring(springForce);
              springAnim.start();
          }
      });

      fab2.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              final SpringAnimation springAnim = new SpringAnimation(fab2, SpringAnimation.TRANSLATION_Y);
              SpringForce springForce = new SpringForce();
              springForce.setFinalPosition(-200f);
              springForce.setStiffness(SpringForce.STIFFNESS_HIGH);
              springForce.setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
              springAnim.setSpring(springForce);
              springAnim.start();
          }
      });

      fab3.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              final SpringAnimation springAnim = new SpringAnimation(fab3, SpringAnimation.TRANSLATION_X);
              SpringForce springForce = new SpringForce();
              springForce.setFinalPosition(-200f);
              springForce.setStiffness(SpringForce.STIFFNESS_MEDIUM);
              springForce.setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY);
              springAnim.setSpring(springForce);
              springAnim.start();
          }
      });

  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
      //Inflate the menu; this adds items to the action bar if it is present.
      getMenuInflater().inflate(R.menu.menu_main, menu);
      return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
      //Handle action bar item clicks here. The action bar will
      //automatically handle clicks on the Home/Up button, so long
      //as you specify a parent activity in AndroidManifest.xml.
      int id = item.getItemId();

      //noinspection SimplifiableIfStatement
      if (id == R.id.action_settings) {
          return true;
      }

      return super.onOptionsItemSelected(item);
  }

  private void imageViewDragSpringAnimation() {

      imageView = findViewById(R.id.imageView);
      imageView.getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener);
      imageView.setOnTouchListener(touchListener);
  }

  private ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
      @Override
      public void onGlobalLayout() {
          xAnimation = createSpringAnimation(imageView, SpringAnimation.X, imageView.getX(),
                  SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
          yAnimation = createSpringAnimation(imageView, SpringAnimation.Y, imageView.getY(),
                  SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
      }
  };

  private View.OnTouchListener touchListener = new View.OnTouchListener() {
      @Override
      public boolean onTouch(View v, MotionEvent event) {
          switch (event.getActionMasked()) {
              case MotionEvent.ACTION_DOWN:
                  //capture the difference between view's top left corner and touch point
                  dX = v.getX() - event.getRawX();
                  dY = v.getY() - event.getRawY();
                  //cancel animations
                  xAnimation.cancel();
                  yAnimation.cancel();
                  break;
              case MotionEvent.ACTION_MOVE:
                  // a different approach would be to change the view's LayoutParams.
                  imageView.animate()
                          .x(event.getRawX() + dX)
                          .y(event.getRawY() + dY)
                          .setDuration(0)
                          .start();
                  break;
              case MotionEvent.ACTION_UP:
                  xAnimation.start();
                  yAnimation.start();
                  break;
          }
          return true;
      }
  };

  public SpringAnimation createSpringAnimation(View view,
                                               DynamicAnimation.ViewProperty property,
                                               float finalPosition,
                                               float stiffness,
                                               float dampingRatio) {
      SpringAnimation animation = new SpringAnimation(view, property);
      SpringForce springForce = new SpringForce(finalPosition);
      springForce.setStiffness(stiffness);
      springForce.setDampingRatio(dampingRatio);
      animation.setSpring(springForce);
      return animation;
  }

  public SpringAnimation createSpringAnimation(View view,
                                               DynamicAnimation.ViewProperty property,
                                               float stiffness,
                                               float dampingRatio) {
      SpringAnimation animation = new SpringAnimation(view, property);
      SpringForce springForce = new SpringForce();
      springForce.setStiffness(stiffness);
      springForce.setDampingRatio(dampingRatio);
      animation.setSpring(springForce);
      return animation;
  }

  private void chainedSpringAnimation() {

      final FloatingActionButton fab4 = findViewById(R.id.fab4);
      final FloatingActionButton fab5 = findViewById(R.id.fab5);
      final FloatingActionButton fab6 = findViewById(R.id.fab6);

      final SpringAnimation firstXAnim = createSpringAnimation(fab5, DynamicAnimation.X, SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
      final SpringAnimation firstYAnim = createSpringAnimation(fab5, DynamicAnimation.Y, SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
      final SpringAnimation secondXAnim = createSpringAnimation(fab6, DynamicAnimation.X, SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
      final SpringAnimation secondYAnim = createSpringAnimation(fab6, DynamicAnimation.Y, SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_HIGH_BOUNCY);

      final ViewGroup.MarginLayoutParams fab5Params = (ViewGroup.MarginLayoutParams) fab5.getLayoutParams();
      final ViewGroup.MarginLayoutParams fab6Params = (ViewGroup.MarginLayoutParams) fab6.getLayoutParams();

      firstXAnim.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() {
          @Override
          public void onAnimationUpdate(DynamicAnimation dynamicAnimation, float v, float v1) {

              secondXAnim.animateToFinalPosition(v + ((fab5.getWidth() 
                      fab6.getWidth())/2));

          }
      });

      firstYAnim.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() {
          @Override
          public void onAnimationUpdate(DynamicAnimation dynamicAnimation, float v, float v1) {
              secondYAnim.animateToFinalPosition(v + fab5.getHeight() +
                      fab6Params.topMargin);
          }
      });

      fab4.setOnTouchListener(new View.OnTouchListener() {
          @Override
          public boolean onTouch(View view, MotionEvent motionEvent) {
              switch (motionEvent.getActionMasked()) {
                  case MotionEvent.ACTION_DOWN:
                      dX = view.getX() - motionEvent.getRawX();
                      dY = view.getY() - motionEvent.getRawY();
                      break;
                  case MotionEvent.ACTION_MOVE:

                      float newX = motionEvent.getRawX() + dX;
                      float newY = motionEvent.getRawY() + dY;

                      view.animate().x(newX).y(newY).setDuration(0).start();
                      firstXAnim.animateToFinalPosition(newX + ((fab4.getWidth() 
                              fab5.getWidth())/2));
                      firstYAnim.animateToFinalPosition(newY + fab4.getHeight() +
                              fab5Params.topMargin);

                      break;
              }
              return true;
          }
      });

  }
}

第一种类型的Simple Spring Animation是在fab,fab2和fab3上执行的。
其中之一可以水平平移。

正如我们之前所讨论的," imageViewDragSpringAnimation()"会触发第二种动画类型。

chainedSpringAnimation()方法触发第三个类型。

在chainedSpringAnimation中,我们拖动第一个fab,即fab4,它将触发X和Y Spring动画的动画更新侦听器。
在每个这些侦听器中,我们触发其他两个"浮动动作"按钮的"弹簧动画",从而将它们链接起来。