美文网首页Android实用知识程序员
自定义View系列四 仿58同城加载动画效果

自定义View系列四 仿58同城加载动画效果

作者: m1Ku | 来源:发表于2017-09-24 11:21 被阅读335次

    这是早期的58同城的加载动画效果。说到加载动画效果,一般会用帧动画或者属性动画来实现,像京东和饿了么有一个小人一直在跑是用帧动画实现的,而我们今天写的这个则是用属性动画来实现。

    加载动画效果图

    loadingView.gif

    Tips

    通常有以下三种方式来实现自定义View

    1. 重写View来实现全新的控件
    2. 对现有View进行扩展
    3. 通过组合来实现新的控件

    我们前面的三篇效果都是用第一种重写View来实现的,而本篇效果则是结合1&3来实现的组合控件

    效果实现分析

    效果图中控件摆放是从上到下的,我们自然想到组合控件继承LinearLayout实现。而控件主要由图中的三部分组成

    1. 第一部分:自定义一个可以轮询改变形状的View叫ShapeChangeView,对外提供变形的方法
    2. 第二部分:它作为第一部分的阴影展示,我们放一个椭圆形的View(ShadowView)即可
    3. 第三部分:放在控件最底层的TextView
    4. 分析动画效果:
      • ShapeChangeView是上下的位移动画,而ShadowView是大小的缩放效果,这两个动画是同时执行的
      • 两者关系是:当ShapeChangeView下落时,ShadowView放大;ShapeChangeView弹起时,ShadowView缩小
      • 监听ShapeChangeView下落结束时,调用其改变形状的方法,弹起时让其改变形状
      • 最后,在弹起和下落动画的结束时相互调用,便形成这种动画效果

    接下来,我们就一步一步的实现这个效果

    1. 自定义轮询改变形状的ShapeChangeView

      • 实现比较简单,主要是图形的轮询绘制
      public class ShapeChangeView extends View {
      
          //初始形状
          public Shape mCurShape = Shape.CIRCLE;
          private Paint mPaint;
          private Path mPath;
          /**
           * 定义形状的枚举类型
           */
          public enum Shape {
              CIRCLE,
              RECTANGLE,
              TRIANGLE,
          }
          public ShapeChangeView(Context context) {
              this(context, null);
          }
          public ShapeChangeView(Context context, @Nullable AttributeSet attrs) {
              this(context, attrs, 0);
          }
          public ShapeChangeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
              super(context, attrs, defStyleAttr);
      
              mPath = new Path();
              mPaint = new Paint();
              mPaint.setAntiAlias(true);
              mPaint.setStyle(Paint.Style.FILL);
          }
      
          /**
           * 获取当前View绘制的形状
           * @return
           */
          public Shape getCurShape() {
              return mCurShape;
          }
        
          @Override
          protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
              super.onMeasure(widthMeasureSpec, heightMeasureSpec);
              //指定View的宽高
              int width = MeasureSpec.getSize(widthMeasureSpec);
              int height = MeasureSpec.getSize(heightMeasureSpec);
              setMeasuredDimension(width>height?height:width, width>height?height:width);
          }
      
          @Override
          protected void onDraw(Canvas canvas) {
              super.onDraw(canvas);
      
              int center = getWidth() / 2;
              switch (mCurShape) {
                  case CIRCLE:
                      //画圆
                      mPaint.setColor(ContextCompat.getColor(getContext(),R.color.circle_color));
                      canvas.drawCircle(center, center, center, mPaint);
                      break;
                  case RECTANGLE:
                      //画正方形
                      mPaint.setColor(ContextCompat.getColor(getContext(),R.color.rect_color));
                      canvas.drawRect(0, 0, getRight(), getBottom(), mPaint);
                      break;
                  case TRIANGLE:
                      //用Path画三角形
                      mPaint.setColor(ContextCompat.getColor(getContext(),R.color.triangle_color));
                      //指定path的起点
                      mPath.moveTo(getWidth() / 2, 0);
                      mPath.lineTo(0, (float) (getWidth() / 2 * Math.sqrt(3)));
                      mPath.lineTo(getWidth(), (float) (getWidth() / 2 * Math.sqrt(3)));
                      canvas.drawPath(mPath, mPaint);
                      break;
              }
          }
          /**
           *轮询改变当前View绘制的形状
           */
          public void changeShape() {
              switch (mCurShape) {
                  case CIRCLE:
                      mCurShape = Shape.RECTANGLE;
                      break;
                  case RECTANGLE:
                      mCurShape = Shape.TRIANGLE;
                      break;
                  case TRIANGLE:
                      mCurShape = Shape.CIRCLE;
                      break;
              }
              invalidate();
          }
      
    2. 定义LoadingView组合控件的布局R.layout.layout_loading_view,摆放其三部分控件

      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:gravity="center"
          android:orientation="vertical">
      
          <com.m1Ku.progressview.view.view4.ShapeChangeView
              android:id="@+id/shapeChangeView"
              android:layout_width="30dp"
              android:layout_height="30dp"
              android:layout_marginBottom="90dp" />
      
          <View
              android:id="@+id/shadowView"
              android:layout_width="33dp"
              android:layout_height="6dp"
              android:background="@drawable/loading_shadow" />
      
          <TextView
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_marginTop="5dp"
              android:text="@string/on_loading" />
      </LinearLayout>
      
    3. 继承LinearLayout定义组合控件LoadingView,加载布局并开启动画

      /**
        *加载布局,初始化控件
        */
      private void initLayout() {
        
              inflate(getContext(), R.layout.layout_loading_view, this);
              mShapeChangeView = findViewById(R.id.shapeChangeView);
              mShadowView = findViewById(R.id.shadowView);
              //开启动画
              post(new Runnable() {
                  @Override
                  public void run() {
                      startFallAnimation();
                  }
              });
          }
          /**
           * 定义 下落动画 和 阴影缩小动画
           */
          private void startFallAnimation() {
              if (isStopAnimation) {
                  return;
              }
              //下落的动画
              ObjectAnimator translateAnimation = ObjectAnimator.ofFloat(mShapeChangeView, "translationY", 0, translateDistance);
              translateAnimation.setInterpolator(new AccelerateInterpolator());
              //阴影缩小动画
              ObjectAnimator scaleAnimation = ObjectAnimator.ofFloat(mShadowView, "scaleX", 1f, 0.4f);
              AnimatorSet animatorSet = new AnimatorSet();
              animatorSet.playTogether(translateAnimation, scaleAnimation);
              animatorSet.setDuration(animateTime);
              animatorSet.setInterpolator(new AccelerateInterpolator());
      
              animatorSet.addListener(new AnimatorListenerAdapter() {
                  @Override
                  public void onAnimationEnd(Animator animation) {
                      super.onAnimationEnd(animation);
                      mShapeChangeView.changeShape();
                      startUpAnimation();
                  }
              });
              animatorSet.start();
          }
         /**
           * 定义 弹起动画 和 阴影放大动画
           */
          private void startUpAnimation() {
              if (isStopAnimation) {
                  return;
              }
              //弹起的动画
              ObjectAnimator translateAnimation = ObjectAnimator.ofFloat(mShapeChangeView, "translationY", translateDistance, 0);
              //阴影放大的动画
              ObjectAnimator scaleAnimation = ObjectAnimator.ofFloat(mShadowView, "scaleX", 0.4f, 1f);
              AnimatorSet animatorSet = new AnimatorSet();
              animatorSet.playTogether(translateAnimation, scaleAnimation);
              animatorSet.setDuration(animateTime);
              animatorSet.setInterpolator(new DecelerateInterpolator());
      
              animatorSet.addListener(new AnimatorListenerAdapter() {
                  @Override
                  public void onAnimationStart(Animator animation) {
                      super.onAnimationStart(animation);
                      //弹起时旋转
                      startRotateAnimation();
                  }
      
                  @Override
                  public void onAnimationEnd(Animator animation) {
                      super.onAnimationEnd(animation);
                      startFallAnimation();
                  }
              });
              animatorSet.start();
      
          }
       //旋转动画
          private void startRotateAnimation() {
              if (isStopAnimation) {
                  return;
              }
              ObjectAnimator rotateAnimation = null;
              switch (mShapeChangeView.getCurShape()) {
                  case CIRCLE:
                  case RECTANGLE:
                      rotateAnimation = ObjectAnimator.ofFloat(mShapeChangeView, "rotation", 0, 180);
                      break;
                  case TRIANGLE:
                      rotateAnimation = ObjectAnimator.ofFloat(mShapeChangeView, "rotation", 0, -120);
                      break;
              }
              rotateAnimation.setDuration(animateTime);
              rotateAnimation.start();
          }
      
    项目Github地址:https://github.com/m1Koi/CustomViewPractice

    小结

    在实现效果的基础上,我们尽可能的也要考虑到性能优化。例如,上述效果中,在控件的setVisibility方法调用后,我们移除了控件中的View并将其本身从父布局移除,并且定义标志位isStopAnimation判断是否还要继续执行动画。具体详见代码中实现。

    相关文章

      网友评论

        本文标题:自定义View系列四 仿58同城加载动画效果

        本文链接:https://www.haomeiwen.com/subject/gblkextx.html