写点什么

Android 开源:想送你一款小清新的加载等待 控件,源码 + 原理 + 手写框架

用户头像
Android架构
关注
发布于: 刚刚

int index;


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


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


/**


  • 关注 4:将外部方块的位置关联起来

  • 算法思想: 按照第 1 行、最后 1 行、第 1 列 & 最后 1 列的顺序,分别让每个外部方块的 next 属性 == 下一个外部方块的位置,最终对整个外部方


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


块的位置进行关联


  • 注:需要考虑移动方向变量 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()@Overrideprotected 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. 设置固定方块的位置 ->>关注 1fixedBlockPosition(mfixedBlocks, cx, cy, blockInterval, half_BlockWidth);// 3. 设置移动方块的位置 ->>关注 2MoveBlockPosition(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()中 @Overrideprotected 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),即顺逆时针 ->>关注 1if (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() {@Overridepublic 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);}// 实时更新旋转中心 ->>关注 2setMoveBlockRotateCenter(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() {@Overridepublic 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. 动画开始时进行一些设置 @Overridepublic void onAnimationStart(Animator animation) {


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


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


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


// 5. 结束时进行一些设置 @Overridepublic 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



用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android开源:想送你一款小清新的加载等待 控件,源码+原理+手写框架