// 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. 设置移动方向
// 情况分为: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) {
    if (currEmptyfixedBlock.rectF.left - rollSquare.rectF.left == 0) {
    return false;
    } else {
    return true;
    }
    }
    // 回到原处

/**

  • 关注2:实时更新移动方块的旋转中心
  • 因为方块在平移旋转过程中,旋转中心也会跟着改变,因此需要改变MoveBlock的旋转中心(cx,cy)
    */

private void setMoveBlockRotateCenter(MoveBlock moveBlock, boolean isClockwise) {

// 情况1:以移动方块的左上角为旋转中心
if (moveBlock.index == 0) {
moveBlock.cx = moveBlock.rectF.right;
moveBlock.cy = moveBlock.rectF.bottom;

// 情况2:以移动方块的右下角为旋转中心
} else if (moveBlock.index == lineNumber * lineNumber - 1) {
moveBlock.cx = moveBlock.rectF.left;
moveBlock.cy = moveBlock.rectF.top;

// 情况3:以移动方块的左下角为旋转中心
} else if (moveBlock.index == lineNumber * (lineNumber - 1)) {
moveBlock.cx = moveBlock.rectF.right;
moveBlock.cy = moveBlock.rectF.top;

// 情况4:以移动方块的右上角为旋转中心
} else if (moveBlock.index == lineNumber - 1) {
moveBlock.cx = moveBlock.rectF.left;
moveBlock.cy = moveBlock.rectF.bottom;
}

//以下判断与旋转方向有关:即顺 or 逆顺时针

// 情况1:左边
else if (moveBlock.index % lineNumber == 0) {
moveBlock.cx = moveBlock.rectF.right;
moveBlock.cy = isClockwise ? moveBlock.rectF.top : moveBlock.rectF.bottom;

// 情况2:上边
} else if (moveBlock.index < lineNumber) {
moveBlock.cx = isClockwise ? moveBlock.rectF.right : moveBlock.rectF.left;
moveBlock.cy = moveBlock.rectF.bottom;

// 情况3:右边
} else if ((moveBlock.index + 1) % lineNumber == 0) {
moveBlock.cx = moveBlock.rectF.left;
moveBlock.cy = isClockwise ? moveBlock.rectF.bottom : moveBlock.rectF.top;

// 情况4:下边
} else if (moveBlock.index > (lineNumber - 1) * lineNumber) {
moveBlock.cx = isClockwise ? moveBlock.rectF.left : moveBlock.rectF.right;
moveBlock.cy = moveBlock.rectF.top;
}
}
// 回到原处

2. 设置旋转动画

private ValueAnimator createMoveValueAnimator() {

// 通过属性动画进行设置
ValueAnimator moveAnim = ValueAnimator.ofFloat(0, 90).setDuration(moveSpeed);

moveAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Object animatedValue = animation.getAnimatedValue();

// 赋值
mRotateDegree = (float) animatedValue;

// 更新视图
invalidate();
}
});
return moveAnim;
}
// 此步骤完毕

3. 设置组合动画

private void setAnimation() {

// 1. 获取固定方块当前的空位置,即移动方块当前位置
fixedBlock currEmptyfixedBlock = mfixedBlocks[mCurrEmptyPosition];
// 2. 获取移动方块的到达位置,即固定方块当前空位置的下1个位置
fixedBlock movedBlock = currEmptyfixedBlock.next;

// 3. 设置动画变化的插值器
mAnimatorSet.setInterpolator(move_Interpolator);
mAnimatorSet.playTogether(translateConrtroller, moveConrtroller);
mAnimatorSet.addListener(new AnimatorListenerAdapter() {

// 4. 动画开始时进行一些设置
@Override
public void onAnimationStart(Animator animation) {

// 每次动画开始前都需要更新移动方块的位置 ->>关注1
updateMoveBlock();

// 让移动方块的初始位置的下个位置也隐藏 = 两个隐藏的方块
mfixedBlocks[mCurrEmptyPosition].next.isShow = false;

// 通过标志位将移动的方块显示出来
mMoveBlock.isShow = true;
}

// 5. 结束时进行一些设置
@Override
public void onAnimationEnd(Animator animation) {
isMoving = false;
mfixedBlocks[mCurrEmptyPosition].isShow = true;
mCurrEmptyPosition = mfixedBlocks[mCurrEmptyPosition].next.index;

// 将移动的方块隐藏
mMoveBlock.isShow = false;

// 通过标志位判断动画是否要循环播放
if (mAllowRoll) {
startMoving();
}
}
});

// 此步骤分析完毕

/**

  • 关注1:更新移动方块的位置
    */

private void updateMoveBlock() {

mMoveBlock.rectF.set(mfixedBlocks[mCurrEmptyPosition].next.rectF);
mMoveBlock.index = mfixedBlocks[mCurrEmptyPosition].next.index;
setMoveBlockRotateCenter(mMoveBlock, isClock_Wise);
}
// 回到原处


步骤6:启动动画

public void startMoving() {

// 1. 根据标志位 & 视图是否可见确定是否需要启动动画
// 此处设置是为了方便手动 & 自动停止动画
if (isMoving || getVisibility() != View.VISIBLE ) {
return;
}

// 2. 设置标记位:以便是否停止动画
isMoving = true;
mAllowRoll = true;

// 3. 启动动画
mAnimatorSet.start();

// 停止动画
public void stopMoving() {
// 通过标记位来设置
mAllowRoll = false;
}

  • 至此,该款小清新加载等待的自定义控件源码分析完毕
  • 完整源码地址:https://github.com/Carson-Ho/Kawaii_LoadingView

7. 贡献代码

  • 希望你们能和我一起完善这款清新 & 小资风格的自定义控件,具体请看:贡献代码说明
  • 关于该开源项目的意见 & 建议可在Issue上提出。欢迎 Star

Github开源地址:Kawaii_LoadingView


8. 总结

  • 相信你一定会喜欢上 这款小清新的加载等待自定义控件

已在Github上开源:Kawaii_LoadingView,欢迎 Star

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

a. 手把手教你实现一个简单好用的搜索框(含历史搜索记录)
b. 你需要一款简单实用的SuperEditText(一键删除&自定义样式))
c. Android 自定义View实战系列 :时间轴


请点赞!因为你的鼓励是我写作的最大动力!

  • 参考文章

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

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

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

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

img
img

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V:vip204888 备注Android获取(资料价值较高,非无偿)
img

文末

今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如Handler机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。

最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【算法合集】

【延伸Android必备知识点】

【Android部分高级架构视频学习资源】

**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

[外链图片转存中…(img-YN2iHZx4-1711551370432)]

【算法合集】

[外链图片转存中…(img-hLgAvjs9-1711551370432)]

【延伸Android必备知识点】

[外链图片转存中…(img-8Ke35WEj-1711551370433)]

【Android部分高级架构视频学习资源】

**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐