Android开源:想送你一款小清新的加载等待 控件

本文详细介绍了如何在Android应用中实现一款可爱且设计感十足的加载等待自定义View控件Kawaii_LoadingView,包括开源、应用场景、特点(如样式清新、使用简单和二次开发成本低)、实现步骤和源码分析。
摘要由CSDN通过智能技术生成
  • 本文将手把手教你做 一款 可爱 & 小资风格的加载等待Android自定义View控件,希望你们会喜欢。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

已在Github开源:Kawaii_LoadingView,欢迎 Star


目录

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


1. 简介

一款 可爱 、清新 & 小资风格的 Android自定义View控件

已在Github开源:Kawaii_LoadingView,欢迎 Star

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


2. 应用场景

App 长时间加载等待时,用于提示用户进度 & 缓解用户情绪


3. 特点

对比市面上的加载等待自定义控件,该控件Kawaii_LoadingView 的特点是:

3.1 样式清新

  • 对比市面上 各种酷炫、眼花缭乱的加载等待自定义控件,该款 Kawaii_LoadingView清新 & 小资风格 简直是一股清流
  • 同时,可根据您的App定位 & 主色进行颜色调整,使得控件更加符合App的形象。具体如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3.2 使用简单

仅需要3步骤 & 配置简单。

具体请看文章:Android开源控件:一款你不可错过的可爱 & 小资风格的加载等待自定义View

3.3 二次开发成本低

  • 本项目已在 Github上开源:Kawaii_LoadingView
  • 详细的源码分析文档:具体请看本文的第6节

所以,在其上做二次开发 & 定制化成本非常低。


4. 具体使用

具体请看文章:Android开源控件:一款你不可错过的可爱 & 小资风格的加载等待自定义View


5. 完整Demo地址

Carson_Ho的Github地址:Kawaii_LoadingView_TestDemo

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


6. 源码分析

下面,我将手把手教你如何实现这款 可爱 & 小资风格的加载等待Android自定义View控件

6.1 准备说明

  • 方格排列说明

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 方块类型说明

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

6.2 动画原理

  • 隐藏固定的2个方块 & 移动方块继承其中1个的位置

注:只有外部方块运动

  • 通过 属性动画 (平移 + 旋转 = 组合动画)改变移动方块的位置 & 旋转角度
  • 通过调用 invalidate() 重新绘制,从而实现动态的动画效果
  • 具体原理图如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

6.3 实现步骤

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

下面我将详细介绍每个步骤:

步骤1:初始化动画属性

  • 属性说明:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 具体属性设置

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 添加属性文件

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
  • 具体源码分析

private void initAttrs(Context context, AttributeSet attrs) {

// 控件资源名称
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Kawaii_LoadingView);

// 一行的数量(最少3行)
lineNumber = typedArray.getInteger(R.styleable.Kawaii_LoadingView_lineNumber, 3);
if (lineNumber < 3) {
lineNumber = 3;
}

// 半个方块的宽度(dp)
half_BlockWidth = typedArray.getDimension(R.styleable.Kawaii_LoadingView_half_BlockWidth, 30);
// 方块间隔宽度(dp)
blockInterval = typedArray.getDimension(R.styleable.Kawaii_LoadingView_blockInterval, 10);

// 移动方块的圆角半径
moveBlock_Angle = typedArray.getFloat(R.styleable.Kawaii_LoadingView_moveBlock_Angle, 10);
// 固定方块的圆角半径
fixBlock_Angle = typedArray.getFloat(R.styleable.Kawaii_LoadingView_fixBlock_Angle, 30);
// 通过设置两个方块的圆角半径使得二者不同可以得到更好的动画效果哦

// 方块颜色(使用十六进制代码,如#333、#8e8e8e)
int defaultColor = context.getResources().getColor(R.color.colorAccent); // 默认颜色
blockColor = typedArray.getColor(R.styleable.Kawaii_LoadingView_blockColor, defaultColor);

// 移动方块的初始位置(即空白位置)
initPosition = typedArray.getInteger(R.styleable.Kawaii_LoadingView_initPosition, 0);

// 由于移动方块只能是外部方块,所以这里需要判断方块是否属于外部方块 -->关注1
if (isInsideTheRect(initPosition, lineNumber)) {
initPosition = 0;
}
// 动画方向是否 = 顺时针旋转
isClock_Wise = typedArray.getBoolean(R.styleable.Kawaii_LoadingView_isClock_Wise, true);

// 移动方块的移动速度
// 注:不建议使用者将速度调得过快
// 因为会导致ValueAnimator动画对象频繁重复的创建,存在内存抖动
moveSpeed = typedArray.getInteger(R.styleable.Kawaii_LoadingView_moveSpeed, 250);

// 设置移动方块动画的插值器
int move_InterpolatorResId = typedArray.getResourceId(R.styleable.Kawaii_LoadingView_move_Interpolator,
android.R.anim.linear_interpolator);
move_Interpolator = AnimationUtils.loadInterpolator(context, move_InterpolatorResId);

// 当方块移动后,需要实时更新的空白方块的位置
mCurrEmptyPosition = initPosition;

// 释放资源
typedArray.recycle();
}

// 此步骤结束

/**

  • 关注1:判断方块是否在内部
    */

private boolean isInsideTheRect(int pos, int lineCount) {
// 判断方块是否在第1行
if (pos < lineCount) {
return false;
// 是否在最后1行
} else if (pos > (lineCount * lineCount - 1 - lineCount)) {
return false;
// 是否在最后1行
} else if ((pos + 1) % lineCount == 0) {
return false;
// 是否在第1行
} else if (pos % lineCount == 0) {
return false;
}
// 若不在4边,则在内部
return true;
}
// 回到原处

步骤2:初始化方块对象 & 之间的关系

private void init() {
// 初始化画笔
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(blockColor);

// 初始化方块对象 & 关系 ->>关注1
initBlocks(initPosition);

}

/**

  • 关注1
  • 初始化方块对象、之间的关系
  • 参数说明:initPosition = 移动方块的初始位置
    */
    private void initBlocks(int initPosition) {

// 1. 创建总方块的数量(固定方块) = lineNumber * lineNumber
// lineNumber = 方块的行数
// fixedBlock = 固定方块 类 ->>关注2
mfixedBlocks = new fixedBlock[lineNumber * lineNumber];

// 2. 创建方块
for (int i = 0; i < mfixedBlocks.length; i++) {

// 创建固定方块 & 保存到数组中
mfixedBlocks[i] = new fixedBlock();

// 对固定方块对象里的变量进行赋值
mfixedBlocks[i].index = i;
// 对方块是否显示进行判断
// 若该方块的位置 = 移动方块的初始位置,则隐藏;否则显示
mfixedBlocks[i].isShow = initPosition == i ? false : true;
mfixedBlocks[i].rectF = new RectF();
}

// 3. 创建移动的方块(1个) ->>关注3
mMoveBlock = new MoveBlock();
mMoveBlock.rectF = new RectF();
mMoveBlock.isShow = false;

// 4. 关联外部方块的位置
// 因为外部的方块序号 ≠ 0、1、2…排列,通过 next变量(指定其下一个),一个接一个连接 外部方块 成圈
// ->>关注4
relate_OuterBlock(mfixedBlocks, isClock_Wise);

}
// 此步骤结束

/**

  • 关注2:固定方块 类(内部类)
    */
    private class fixedBlock {

// 存储方块的坐标位置参数
RectF rectF;

// 方块对应序号
int index;

// 标志位:判断是否需要绘制
boolean isShow;

// 指向下一个需要移动的位置
fixedBlock next;
// 外部的方块序号 ≠ 0、1、2…排列,通过 next变量(指定其下一个),一个接一个连接 外部方块 成圈

}
// 请回到原处

/**

  • 关注3
    *:移动方块类(内部类)
    */
    private class MoveBlock {
    // 存储方块的坐标位置参数
    RectF rectF;

// 方块对应序号
int index;

// 标志位:判断是否需要绘制
boolean isShow;

// 旋转中心坐标
// 移动时的旋转中心(X,Y)
float cx;
float cy;
}
// 请回到原处

/**

  • 关注4:将外部方块的位置关联起来
  • 算法思想: 按照第1行、最后1行、第1列 & 最后1列的顺序,分别让每个外部方块的next属性 == 下一个外部方块的位置,最终对整个外部方块的位置进行关联
  • 注:需要考虑移动方向变量isClockwise( 顺 Or 逆时针)
    */

private void relate_OuterBlock(fixedBlock[] fixedBlocks, boolean isClockwise) {
int lineCount = (int) Math.sqrt(fixedBlocks.length);

// 情况1:关联第1行
for (int i = 0; i < lineCount; i++) {
// 位于最左边
if (i % lineCount == 0) {
fixedBlocks[i].next = isClockwise ? fixedBlocks[i + lineCount] : fixedBlocks[i + 1];
// 位于最右边
} else if ((i + 1) % lineCount == 0) {
fixedBlocks[i].next = isClockwise ? fixedBlocks[i - 1] : fixedBlocks[i + lineCount];
// 中间
} else {
fixedBlocks[i].next = isClockwise ? fixedBlocks[i - 1] : fixedBlocks[i + 1];
}
}
// 情况2:关联最后1行
for (int i = (lineCount - 1) * lineCount; i < lineCount * lineCount; i++) {
// 位于最左边
if (i % lineCount == 0) {
fixedBlocks[i].next = isClockwise ? fixedBlocks[i + 1] : fixedBlocks[i - lineCount];
// 位于最右边
} else if ((i + 1) % lineCount == 0) {
fixedBlocks[i].next = isClockwise ? fixedBlocks[i - lineCount] : fixedBlocks[i - 1];
// 中间
} else {
fixedBlocks[i].next = isClockwise ? fixedBlocks[i + 1] : fixedBlocks[i - 1];
}
}

// 情况3:关联第1列
for (int i = 1 * lineCount; i <= (lineCount - 1) * lineCount; i += lineCount) {
// 若是第1列最后1个
if (i == (lineCount - 1) * lineCount) {
fixedBlocks[i].next = isClockwise ? fixedBlocks[i + 1] : fixedBlocks[i - lineCount];
continue;
}
fixedBlocks[i].next = isClockwise ? fixedBlocks[i + lineCount] : fixedBlocks[i - lineCount];
}

// 情况4:关联最后1列
for (int i = 2 * lineCount - 1; i <= lineCount * lineCount - 1; i += lineCount) {
// 若是最后1列最后1个
if (i == lineCount * lineCount - 1) {
fixedBlocks[i].next = isClockwise ? fixedBlocks[i - lineCount] : fixedBlocks[i - 1];
continue;
}
fixedBlocks[i].next = isClockwise ? fixedBlocks[i - lineCount] : fixedBlocks[i + lineCount];
}
}
// 请回到原处


步骤3:设置方块初始位置

// 该步骤写在onSizeChanged()
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// 调用时刻:onCreate之后onDraw之前调用;view的大小发生改变就会调用该方法
// 使用场景:用于屏幕的大小改变时,需要根据屏幕宽高来决定的其他变量可以在这里进行初始化操作
super.onSizeChanged(w, h, oldw, oldh);

int measuredWidth = getMeasuredWidth();
int measuredHeight = getMeasuredHeight();

// 1. 设置移动方块的旋转中心坐标
int cx = measuredWidth / 2;
int cy = measuredHeight / 2;

// 2. 设置固定方块的位置 ->>关注1
fixedBlockPosition(mfixedBlocks, cx, cy, blockInterval, half_BlockWidth);
// 3. 设置移动方块的位置 ->>关注2
MoveBlockPosition(mfixedBlocks, mMoveBlock, initPosition, isClock_Wise);
}

// 此步骤结束

/**

  • 关注1:设置 固定方块位置
    */
    private void fixedBlockPosition(fixedBlock[] fixedBlocks, int cx, int cy, float dividerWidth, float halfSquareWidth) {

// 1. 确定第1个方块的位置
// 分为2种情况:行数 = 偶 / 奇数时
// 主要是是数学知识,此处不作过多描述
float squareWidth = halfSquareWidth * 2;
int lineCount = (int) Math.sqrt(fixedBlocks.length);
float firstRectLeft = 0;
float firstRectTop = 0;

// 情况1:当行数 = 偶数时
if (lineCount % 2 == 0) {
int squareCountInAline = lineCount / 2;
int diviCountInAline = squareCountInAline - 1;
float firstRectLeftTopFromCenter = squareCountInAline * squareWidth

  • diviCountInAline * dividerWidth
  • dividerWidth / 2;
    firstRectLeft = cx - firstRectLeftTopFromCenter;
    firstRectTop = cy - firstRectLeftTopFromCenter;

// 情况2:当行数 = 奇数时
} else {
int squareCountInAline = lineCount / 2;
int diviCountInAline = squareCountInAline;
float firstRectLeftTopFromCenter = squareCountInAline * squareWidth

  • diviCountInAline * dividerWidth
  • halfSquareWidth;
    firstRectLeft = cx - firstRectLeftTopFromCenter;
    firstRectTop = cy - firstRectLeftTopFromCenter;
    firstRectLeft = cx - firstRectLeftTopFromCenter;
    firstRectTop = cy - firstRectLeftTopFromCenter;
    }

// 2. 确定剩下的方块位置
// 思想:把第一行方块位置往下移动即可
// 通过for循环确定:第一个for循环 = 行,第二个 = 列
for (int i = 0; i < lineCount; i++) {//行
for (int j = 0; j < lineCount; j++) {//列
if (i == 0) {
if (j == 0) {
fixedBlocks[0].rectF.set(firstRectLeft, firstRectTop,
firstRectLeft + squareWidth, firstRectTop + squareWidth);
} else {
int currIndex = i * lineCount + j;
fixedBlocks[currIndex].rectF.set(fixedBlocks[currIndex - 1].rectF);
fixedBlocks[currIndex].rectF.offset(dividerWidth + squareWidth, 0);
}
} else {
int currIndex = i * lineCount + j;
fixedBlocks[currIndex].rectF.set(fixedBlocks[currIndex - lineCount].rectF);
fixedBlocks[currIndex].rectF.offset(0, dividerWidth + squareWidth);
}
}
}
}

// 回到原处

/**

  • 关注2:设置移动方块的位置
    */
    private void MoveBlockPosition(fixedBlock[] fixedBlocks,
    MoveBlock moveBlock, int initPosition, boolean isClockwise) {

// 移动方块位置 = 设置初始的空出位置 的下一个位置(next)
// 下一个位置 通过 连接的外部方块位置确定
fixedBlock fixedBlock = fixedBlocks[initPosition];
moveBlock.rectF.set(fixedBlock.next.rectF);
}
// 回到原处


步骤4:绘制方块

// 此步骤写到onDraw()中
@Override
protected void onDraw(Canvas canvas) {

// 1. 绘制内部方块(固定的)
for (int i = 0; i < mfixedBlocks.length; i++) {
// 根据标志位判断是否需要绘制
if (mfixedBlocks[i].isShow) {
// 传入方块位置参数、圆角 & 画笔属性
canvas.drawRoundRect(mfixedBlocks[i].rectF, fixBlock_Angle, fixBlock_Angle, mPaint);
}
}
// 2. 绘制移动的方块
if (mMoveBlock.isShow) {
canvas.rotate(isClock_Wise ? mRotateDegree : -mRotateDegree, mMoveBlock.cx, mMoveBlock.cy);
canvas.drawRoundRect(mMoveBlock.rectF, moveBlock_Angle, moveBlock_Angle, mPaint);
}

}

步骤5:设置动画

实现该动画的步骤包括:设置平移动画、旋转动画 & 组合动画。

1.设置平移动画

private ValueAnimator createTranslateValueAnimator(fixedBlock currEmptyfixedBlock,
fixedBlock moveBlock) {
float startAnimValue = 0;
float endAnimValue = 0;
PropertyValuesHolder left = null;
PropertyValuesHolder top = null;

// 1. 设置移动速度
ValueAnimator valueAnimator = new ValueAnimator().setDuration(moveSpeed);

// 2. 设置移动方向
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后看一下学习需要的所有知识点的思维导图。在刚刚那份学习笔记里包含了下面知识点所有内容!文章里已经展示了部分!如果你正愁这块不知道如何学习或者想提升学习这块知识的学习效率,那么这份学习笔记绝对是你的秘密武器!

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

YHPu1QW-1712562782132)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后看一下学习需要的所有知识点的思维导图。在刚刚那份学习笔记里包含了下面知识点所有内容!文章里已经展示了部分!如果你正愁这块不知道如何学习或者想提升学习这块知识的学习效率,那么这份学习笔记绝对是你的秘密武器!

[外链图片转存中…(img-D5zMlB86-1712562782132)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值