按照惯例,反手就是一个超连接: github地址git
本文要实现的View效果以下图:程序员
从效果图容易看出,图中的功能主要分为两个部分:github
不难发现左侧动画效果主要由三部分组成:canvas
拇指的缩放各位客观想必也是心中有数的,无非就是两种方式:api
// 处理拇指缩放效果
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
move = event.getY();
animate().scaleY(0.8f).scaleX(0.8f).start();
break;
case MotionEvent.ACTION_UP:
getHandler().postDelayed(new Runnable() {
@Override
public void run() {
animate().cancel();
setScaleX(1);
setScaleY(1);
}
}, 300);
...
// 省略无关代码
break;
}
return super.onTouchEvent(event);
}
复制代码
#### 3.1.1 圆圈扩散 没错,就是画圈圈。一样,仔细的同志应该已经发现了些什么,冥冥之中彷佛有些什么不可告人的秘密。 是的,这里有两个须要注意的地方:bash
// 测量View宽高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
switch (widthSpecMode) {
...
case MeasureSpec.AT_MOST:
widthMeasureSpec = mDrawable.getIntrinsicWidth();
break;
...
}
switch (heightSpecMode) {
...
// wrap_content
case MeasureSpec.AT_MOST:
heightMeasureSpec = mDrawable.getIntrinsicHeight();
break;
...
}
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
initDrawable(mDrawable, widthMeasureSpec, heightMeasureSpec);
initPointFs(1.3f);
}
// drawable的大小为view的0.6
private void initDrawable(Drawable drawable, int width, int height) {
mCircleCenter.x = width / 2f;
mCircleCenter.y = height / 2;
mDrawable = drawable;
// drawable的边长为view的0.6
float diameter = (float) ((width > height ? height : width) * 0.6);
int left = (int) ((width - diameter)/2);
int top = (int)(height - diameter)/2;
int right = (int) (left + diameter);
int bottom = (int) (top + diameter);
Rect drawableRect = new Rect(left, top, right, bottom);
mDrawable.setBounds(drawableRect);
requestLayout();
}
复制代码
由此计算出了view和drawable的大小,从而能够去画他了。这样咱们就确认了圈圈该画在哪里,接下来的扩散效果,只须要控制圈圈的半径便可,依旧看代码:ide
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mDrawable.draw(canvas);
drawEffect(canvas);
}
private void drawEffect(Canvas canvas) {
// 画圆
if (mRadius > 0)
canvas.drawCircle(mCircleCenter.x, mCircleCenter.y, mRadius, mPaint);
if (drawLines == 1) {
// 划线
...
}
public void animation() {
final float radius = getInitRadius(mDrawable);
ObjectAnimator animator = ObjectAnimator.ofFloat(this, "radius", radius, radius * 1.5f, radius * 3.0f);
animator.setInterpolator(new AnticipateInterpolator());
animator.setDuration(500);
// 画线
// ...
set.start();
}
复制代码
至此咱们完成了拇指的缩放和波纹效果,内心美滋滋有木有 #### 3.1.2 线段效果 线段怎么去画呢?中小学老师告诉咱们,两点确认一条线段。问题随之转换:布局
/**
* 用于计算 线条的长度
* @param scale 外圆半径为内圆半径的scale倍数
*/
private void initPointFs(float scale) {
mPointList.clear();
float radius = getInitRadius(mDrawable);
int base = -60;
int factor = -20;
for (int i = 0; i < 4; i++) {
int result = base + factor * i;
// 点p1为mDrawable外接圆上的点
PointF p1 = new PointF(
mCircleCenter.x + (float) (radius * Math.cos(Math.toRadians(result))),
mCircleCenter.y + (float) (radius * Math.sin(Math.toRadians(result)))
);
// 点p1为mDrawable外接圆scale倍上的点
PointF p2 = new PointF(
mCircleCenter.x + (float) (scale * radius * Math.cos(Math.toRadians(result))),
mCircleCenter.y + (float) (scale * radius * Math.sin(Math.toRadians(result)))
);
mPointList.add(p1);
mPointList.add(p2);
}
}
复制代码
经过代码注解不难发现,这里咱们巧妙的利用同心圆和角度的方式来肯定了4条线段,8个点集合的值(豆豆不由感叹,数学对程序员的重要性)。这样作的好处就是足够灵活,不管View大小如何变,线段的间隔和长短都是适宜的。 至此左侧的拇指动画效果,算是告一段落了。post
右边的数字翻牌效果,乍看起来很简单,无非就是drawText()累加以后从新drawText();原理上是这样的没错,不过值得注意的是:动画
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
···
switch (widthSpecMode) {
···
case MeasureSpec.AT_MOST:
int width = (int) mPaint.measureText("0", 0, 1) * mCurrentString.length();
widthMeasureSpec = width;
break;
···
}
switch (heightSpecMode) {
···
case MeasureSpec.AT_MOST:
mTextHeight = mPaint.getFontSpacing();
heightMeasureSpec = (int) (mTextHeight * 3);
break;
case MeasureSpec.EXACTLY:
mPaint.setTextSize(heightSpecSize / 4);
mTextHeight = (int) mPaint.getFontSpacing();
heightMeasureSpec = heightSpecSize;
break;
}
pointY = 2 * mTextHeight;
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
复制代码
在测量出View的宽高以后,便要着手去画view的内容了,而内容很简单,就是一系列的String值。到这里都比较容易实现,而难点则是,肯定上一个和下一个值,以及他们的位置。 细心的朋友可能已经发如今measure的时候,咱们有一个mTextHeigh记录了文字的高度,pointY记录了两倍文字的高度,没错这里就是利用mTextHeight来控制三个可能要画出来的string值的位置的。
这里有必要提一下的是,drawText(@NonNull String text, float x, float y, @NonNull Paint paint)这个方法中的float y对应的是baseLine的y值,简单的理解的话就是一串String的bottom的位置,画出来的内容是在bottom之上的。这也是为何咱们要用pointY = 2 * mTextHeight的理由。至此不难想到,咱们的lastNum, currentNum, NextNum画的位置,分别对应mTextHeight, 2 * mTextHeight和3 * mTextHeight。至此三个值的位置便算是肯定好了。
先看加1的处理,上代码:
public void addOne() {
mCurrentString = String.valueOf(mCurrentNum);
mCurrentNum++;
mNextString = String.valueOf(mCurrentNum);
mStatus = ADD;
// 数字位数进1
if (mCurrentString.length() < mNextString.length()) {
mCurrentString = " " + mCurrentString;
requestLayout();
}
ObjectAnimator animator = ObjectAnimator.ofFloat(this, "pointY", 2 * mTextHeight, mTextHeight);
ObjectAnimator alphaAnim = ObjectAnimator.ofInt(this, "paintAlpha", 255, 0);
AnimatorSet set = new AnimatorSet();
set.playTogether(alphaAnim, animator);
set.start();
}
复制代码
代码比较简单,无非是作了移动和透明度的动画效果,这里便解决了“上下翻动时前一个数字会渐渐隐掉”的需求,须要注意的点是,数字位进1时的利用空格占位的处理,不作该处理,当数字进位后,动画效果会差强人意,有兴趣的朋友能够去试试看。 结合onDraw方法再来看看:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mStatus == NONE) {
canvas.drawText(mCurrentString, 0, pointY, mPaint);
} else if (mStatus == ADD) {
for (int i = mNextString.length() - 1; i >= 0; i--) {
String next = String.valueOf(mNextString.charAt(i));
String current = String.valueOf(mCurrentString.charAt(i));
// i位置须要改变
if (!next.equals(current)) {
mPaint.setAlpha(mPaintAlpha);
canvas.drawText(current, mPaint.measureText("0", 0, 1) * i, pointY, mPaint);
// mPaintAlpha : 255 - 0 递减
mPaint.setAlpha(255 - mPaintAlpha);
canvas.drawText(next, mPaint.measureText("0", 0, 1) * i, mTextHeight + pointY, mPaint);
// i位置不须要改变
} else {
mPaint.setAlpha(255);
canvas.drawText(current, mPaint.measureText("0", 0, 1) * i, mTextHeight * 2, mPaint);
}
}
} else if (mStatus == REDUCE) {
// pointY是累加的,所以有个往下滑动效果
for (int i = mCurrentString.length() - 1; i >= 0; i--) {
String last = String.valueOf(mLastString.charAt(i));
String current = String.valueOf(mCurrentString.charAt(i));
// i位置须要改变
if (!last.equals(current)) {
mPaint.setAlpha(mPaintAlpha);
canvas.drawText(current, mPaint.measureText("0", 0, 1) * i, mTextHeight + pointY, mPaint);
// mPaintAlpha : 255 - 0 递减
mPaint.setAlpha(255 - mPaintAlpha);
canvas.drawText(last, mPaint.measureText("0", 0, 1) * i, pointY, mPaint);
// i位置不须要改变
} else {
mPaint.setAlpha(255);
canvas.drawText(current, mPaint.measureText("0", 0, 1) * i, mTextHeight * 2, mPaint);
}
}
}
}
复制代码
这里即是核心所在了:如何无需变化的数位上的值不会被翻动? onDraw方法中给出了咱们答案,思路很简单:
至此gif图中的两部分效果都已经实现
以上是分开独立的两个view,为了更方便的使用这个效果,咱们须要将两个view的功能整合在一块儿,起到一个联动效果,也就须要引入一个ViewGroup去肯定这两个view(PraiseView和RecordView)的布局,这部分主要涉及到layout,以及viewgroup测绘的时候,使用的是match_parent宽高时,如何控制子view的显示,有兴趣的朋友能够直接去看代码,这里暂不作赘述了。
行文至此,我不由点了根黄鹤楼,望着那袅袅的烟,一抬手摸着了天... 天边飘来一个: github地址
附赠优惠礼包自取: 阿里云飞机票