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

步骤1:初始化动画属性

属性说明:

具体属性设置

添加属性文件

attrs.xml

具体源码分析

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 * dividerWidthdividerWidth / 2; firstRectLeft = cx - firstRectLeftTopFromCenter; firstRectTop = cy - firstRectLeftTopFromCenter;

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

diviCountInAline * dividerWidthhalfSquareWidth; 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. 设置移动方向 // 情况分为:4种,分别是移动方块向左、右移动 和 上、下移动 // 注:需考虑 旋转方向(isClock_Wise),即顺逆时针 ->>关注1 if (isNextRollLeftOrRight(currEmptyfixedBlock, moveBlock)) {

// 情况1:顺时针且在第一行 / 逆时针且在最后一行时,移动方块向右移动 if (isClock_Wise && currEmptyfixedBlock.index > moveBlock.index || !isClock_Wise && currEmptyfixedBlock.index > moveBlock.index) {

startAnimValue = moveBlock.rectF.left; endAnimValue = moveBlock.rectF.left + blockInterval;

// 情况2:顺时针且在最后一行 / 逆时针且在第一行,移动方块向左移动 } else if (isClock_Wise && currEmptyfixedBlock.index < moveBlock.index || !isClock_Wise && currEmptyfixedBlock.index < moveBlock.index) {

startAnimValue = moveBlock.rectF.left; endAnimValue = moveBlock.rectF.left - blockInterval; }

// 设置属性值 left = PropertyValuesHolder.ofFloat(“left”, startAnimValue, endAnimValue); valueAnimator.setValues(left);

} else { // 情况3:顺时针且在最左列 / 逆时针且在最右列,移动方块向上移动 if (isClock_Wise && currEmptyfixedBlock.index < moveBlock.index || !isClock_Wise && currEmptyfixedBlock.index < moveBlock.index) {

startAnimValue = moveBlock.rectF.top; endAnimValue = moveBlock.rectF.top - blockInterval;

// 情况4:顺时针且在最右列 / 逆时针且在最左列,移动方块向下移动 } else if (isClock_Wise && currEmptyfixedBlock.index > moveBlock.index || !isClock_Wise && currEmptyfixedBlock.index > moveBlock.index) { startAnimValue = moveBlock.rectF.top; endAnimValue = moveBlock.rectF.top + blockInterval; }

// 设置属性值 top = PropertyValuesHolder.ofFloat(“top”, startAnimValue, endAnimValue); valueAnimator.setValues(top); }

// 3. 通过监听器更新属性值 valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { Object left = animation.getAnimatedValue(“left”); Object top = animation.getAnimatedValue(“top”); if (left != null) { mMoveBlock.rectF.offsetTo((Float) left, mMoveBlock.rectF.top); } if (top != null) { mMoveBlock.rectF.offsetTo(mMoveBlock.rectF.left, (Float) top); } // 实时更新旋转中心 ->>关注2 setMoveBlockRotateCenter(mMoveBlock, isClock_Wise);

// 更新绘制 invalidate(); } }); return valueAnimator; } // 此步骤分析完毕

/**

关注1:判断移动方向即上下 or 左右 */ private boolean isNextRollLeftOrRight(fixedBlock currEmptyfixedBlock, fixedBlock rollSquare) { 自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

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

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

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

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

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

尾声

如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

这里,笔者分享一份从架构哲学的层面来剖析的视频及资料给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。

Android进阶学习资料库

一共十个专题,包括了Android进阶所有学习资料,Android进阶视频,Flutter,java基础,kotlin,NDK模块,计算机网络,数据结构与算法,微信小程序,面试题解析,framework源码!

大厂面试真题

PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《2019-2021字节跳动Android面试历年真题解析》

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

阶学习资料库

一共十个专题,包括了Android进阶所有学习资料,Android进阶视频,Flutter,java基础,kotlin,NDK模块,计算机网络,数据结构与算法,微信小程序,面试题解析,framework源码!

[外链图片转存中…(img-bJFxdlEF-1711791548259)]

大厂面试真题

PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-CQOzjhhY-1711791548260)]

《2019-2021字节跳动Android面试历年真题解析》

[外链图片转存中…(img-GuJOR5xz-1711791548260)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

推荐文章

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: