本文目的
平时项目中看到的加载中的动效基本上就是转圈的形式,有点审美疲劳。网上看到一个有趣的动效,参照着通过ViewGroup做了一个简单的实现。主要知识点基于ViewGroup实现自定义组合视图
效果介绍
话不多说,直接上视频。实现了一个物体上升/下落的动效,并且可以自动切换形状和颜色。颜色好好配一下效果预估更佳
代码工程结构
本次实现动效因为引用了res资源,单独设置了一个名称为loading的module存放相应的代码和资源,然后在app主Module中使用
绘制形状的自定义View代码
绘制形状的代码主要逻辑包括测量尺寸,根据形状类型决定绘制三角形/正方形/圆形3种形状。内部设置一个供外部调用的更换形状的接口,整体逻辑较为简单,如下所示
/**
* 基于画图实现形状绘制
*/
public class ShapeView extends View {
private int width , height;
private int left , right , top ,bottom;
private Paint shapePaint;
//当前形状在全部形状中的序号
private int index = 0;
private Shape mShape = Shape.values()[index];
//可以变化的颜色组合,长度需要与Shape长度相同
private int[] colors = {R.color.purple_200 , R.color.purple_500 , R.color.purple_700};
public ShapeView(Context context) {
super(context);
init();
}
public ShapeView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public ShapeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public void setShape(Shape mShape) {
this.mShape = mShape;
}
/**
* 设置形状和颜色顺序切换
*/
public void changeShape(){
index++;
index%=Shape.values().length;
this.mShape = Shape.values()[index];
shapePaint.setColor(getResources().getColor(colors[index],null));
invalidate();
}
//测量应当绘制的形状的宽度,高度
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
left = getPaddingLeft();
right = left + width;
top = getPaddingTop();
bottom = top + height;
Log.d("ShapeView" , "width = "+width+",height = "
+height+",left = "+left+",right = "+right+",top = "+top+",bottom = "+bottom);
}
//绘制形状
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
switch (mShape){
case CIRCLE:
drawCircle(canvas);
break;
case TRIANGLE:
drawTriangle(canvas);
break;
case RECTANGLE:
default:
drawRectangle(canvas);
break;
}
}
//绘制三角形
private void drawTriangle(Canvas canvas){
Path path = new Path();
int topPoint = top + height - (int)(height*Math.sqrt(3)/2);
path.moveTo(left/2+right/2 , topPoint);
path.lineTo(right , bottom);
path.lineTo(left , bottom);
path.lineTo(left/2+right/2 , topPoint);
path.close();
canvas.drawPath(path , shapePaint);
}
//绘制正方形
private void drawRectangle(Canvas canvas){
int rectWidth = Math.min(width , height) - 20;
int rectLeft = left/2 + right/2 - rectWidth/2;
int rectRight = left/2 + right/2 + rectWidth/2;
int rectTop = top/2 + bottom/2 - rectWidth/2;
int rectBottom = top/2 + bottom/2 + rectWidth/2;
RectF rect = new RectF(rectLeft , rectTop , rectRight , rectBottom);
canvas.drawRoundRect(rect,5,5,shapePaint);
}
//绘制圆形
private void drawCircle(Canvas canvas){
canvas.drawCircle(left/2+right/2 , top/2+bottom/2 ,
Math.min(width,height)/2 - 5 , shapePaint);
}
//初始化画笔
private void init(){
shapePaint = new Paint();
shapePaint.setAntiAlias(true);
shapePaint.setColor(getResources().getColor(colors[index],null));
shapePaint.setStyle(Paint.Style.FILL_AND_STROKE);
}
//绘制的形状组合
public enum Shape{
CIRCLE,
TRIANGLE,
RECTANGLE
}
}
自定义ViewGroup实现动效
整体的动效由提示文字,地面的倒影,上升/下降的形状物体3个部分构成,地面倒影是通过设置椭圆形状进行横向缩放实现,上升/下降的形状物体则是基于自定义View的物体增加上升和下降的动效。详细代码如下
/**
* 加载中动效布局
*/
public class LoadingView extends LinearLayout {
//运动小球
private ShapeView shapeView;
//地面倒影
private ImageView imageViewReflection;
//移动距离
private int shiftDistance = 0;
private AnimatorSet upAnimatorSet , downAnimatorSet;
public LoadingView(Context context) {
super(context);
}
public LoadingView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context,attrs);
}
public LoadingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
public LoadingView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
private void init(Context context , AttributeSet attrs){
setOrientation(VERTICAL);
LayoutInflater.from(context).inflate(R.layout.layout_loading_view,this , true);
shapeView = findViewById(R.id.shape_view);
imageViewReflection = findViewById(R.id.imageview_reflect);
shiftDistance = dp2px(60);
}
/**
* 在视图加载完毕时开始执行动效
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if(getVisibility() == VISIBLE){
startAnim();
}
}
/**
* 视图被移除时停止动效
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
stopAnim();
}
private void startAnim(){
toBottom();
}
private void stopAnim(){
if(null != upAnimatorSet && upAnimatorSet.isRunning()){
upAnimatorSet.cancel();
upAnimatorSet.removeAllListeners();
}
if(null != downAnimatorSet && downAnimatorSet.isRunning()){
downAnimatorSet.cancel();
downAnimatorSet.removeAllListeners();
}
}
/**
* /减速上升
*/
private void toTop(){
shapeView.changeShape();
ObjectAnimator upShiftAnimator = ObjectAnimator.ofFloat(shapeView,"translationY",
shiftDistance,0);
ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(imageViewReflection,"scaleX",1f,0.2f);
ObjectAnimator rotateAnimator = ObjectAnimator.ofFloat(shapeView , "rotation",0,360);
upAnimatorSet = new AnimatorSet();
upAnimatorSet.playTogether(upShiftAnimator , scaleAnimator , rotateAnimator);
upAnimatorSet.setDuration(1000);
upAnimatorSet.setInterpolator(new DecelerateInterpolator());
upAnimatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
toBottom();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
upAnimatorSet.start();
}
/**
* 加速下降
*/
private void toBottom(){
ObjectAnimator bottomShiftAnimator = ObjectAnimator.ofFloat(shapeView,"translationY",
0,shiftDistance);
ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(imageViewReflection,"scaleX",0.2f,1f);
ObjectAnimator rotateAnimator = ObjectAnimator.ofFloat(shapeView , "rotation",0,360);
downAnimatorSet = new AnimatorSet();
downAnimatorSet.playTogether(bottomShiftAnimator , scaleAnimator , rotateAnimator);
downAnimatorSet.setDuration(1000);
downAnimatorSet.setInterpolator(new AccelerateInterpolator());
downAnimatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
toTop();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
downAnimatorSet.start();
}
/**
* 将dp值转为像素
* @param dp 输入dp类型尺寸
* @return
*/
private int dp2px(int dp){
float dpi = getContext().getResources().getDisplayMetrics().density;
return (int)(dp*dpi + 0.5f);
}
}
动效使用
在主Module的build.gradle文件中dependencies添加依赖
//引用加载中动效的module
implementation project(path:':loading')
在需要引用的布局文件中引用相应的类,如下示例
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
<!--引入加载中动效-->
<com.guo.loading.LoadingView
android:layout_width="200dp"
android:layout_height="200dp"/>
</LinearLayout>
源码位置
最后要感谢CSDN提供的设计效果和实现参照,只是我暂时没有找到链接注明,代码已共享之Gitee
https://gitee.com/com_mailanglidegezhe/loading-view.git