已在Github开源:Kawaii_LoadingView,欢迎 Star !
一款 可爱 、清新 & 小资风格的 Android自定义View控件
已在Github开源:Kawaii_LoadingView,欢迎 Star !
App 长时间加载等待时,用于提示用户进度 & 缓解用户情绪
对比市面上的加载等待自定义控件,该控件Kawaii_LoadingView 的特点是:
仅需要3步骤 & 配置简单。
具体请看文章:Android开源控件:一款你不可错过的可爱 & 小资风格的加载等待自定义View
所以,在其上做二次开发 & 定制化成本非常低。
具体请看文章:http://blog.csdn.net/carson_ho/article/details/77711952
Carson_Ho的Github地址:Kawaii_LoadingView_TestDemo
下面,我将手把手教你如何实现这款 可爱 & 小资风格的加载等待android自定义View控件
方块类型说明
注:只有外部方块运动
通过 属性动画 (平移 + 旋转 = 组合动画)改变移动方块的位置 & 旋转角度通过调用 invalidate() 重新绘制,从而实现动态的动画效果具体原理图如下:
下面我将详细介绍每个步骤:
具体属性设置
添加属性文件
attrs.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="Kawaii_LoadingView"> <attr name="half_BlockWidth" format="dimension" /> <attr name="blockInterval" format="dimension" /> <attr name="initPosition" format="integer" /> <attr name="isClock_Wise" format="boolean" /> <attr name="lineNumber" format="integer" /> <attr name="moveSpeed" format="integer" /> <attr name="blockColor" format="color" /> <attr name="moveBlock_Angle" format="float" /> <attr name="fixBlock_Angle" format="float" /> <attr name="move_Interpolator" format="reference" /> </declare-styleable> </resources> 123456789101112131415 123456789101112131415 具体源码分析 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; } // 回到原处 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778实现该动画的步骤包括:设置平移动画、旋转动画 & 组合动画。
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; } } // 回到原处 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421432. 设置旋转动画
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; } // 此步骤完毕 1234567891011121314151617181920 12345678910111213141516171819203. 设置组合动画
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); } // 回到原处 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657Github开源地址:Kawaii_LoadingView
已在Github上开源:Kawaii_LoadingView,欢迎 Star !
原文地址:http://blog.csdn.net/carson_ho/article/details/77712072 原文作者:Carson_Ho