这是早期的58同城的加载动画效果。说到加载动画效果,一般会用帧动画或者属性动画来实现,像京东和饿了么有一个小人一直在跑是用帧动画实现的,而我们今天写的这个则是用属性动画来实现。
加载动画效果图
loadingView.gifTips
通常有以下三种方式来实现自定义View
- 重写View来实现全新的控件
- 对现有View进行扩展
- 通过组合来实现新的控件
我们前面的三篇效果都是用第一种重写View来实现的,而本篇效果则是结合1&3来实现的组合控件
效果实现分析
效果图中控件摆放是从上到下的,我们自然想到组合控件继承LinearLayout实现。而控件主要由图中的三部分组成
- 第一部分:自定义一个可以轮询改变形状的View叫ShapeChangeView,对外提供变形的方法
- 第二部分:它作为第一部分的阴影展示,我们放一个椭圆形的View(ShadowView)即可
- 第三部分:放在控件最底层的TextView
- 分析动画效果:
- ShapeChangeView是上下的位移动画,而ShadowView是大小的缩放效果,这两个动画是同时执行的
- 两者关系是:当ShapeChangeView下落时,ShadowView放大;ShapeChangeView弹起时,ShadowView缩小
- 监听ShapeChangeView下落结束时,调用其改变形状的方法,弹起时让其改变形状
- 最后,在弹起和下落动画的结束时相互调用,便形成这种动画效果
接下来,我们就一步一步的实现这个效果
-
自定义轮询改变形状的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(); }
-
定义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>
-
继承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
判断是否还要继续执行动画。具体详见代码中实现。
网友评论