知识点
- 自定义view绘制的运用
- 对于图像转函数的一些应用
- 属性动画的使用
效果图
ps:由于我采用的是上部分不可转动,下部分刻度转动的方式,在实现选择对应刻度,回弹功能的时候要考虑比较多的情况,所以这里就不细说具体的一些计算方式了,我想传递的是一些自定义view的思路,如果有其他需要具体参考下源码,有不懂的可以留言讨论
一、view的绘制
在我看来,自定义view中的动画效果,都是基于静态画面上所实现的,所以首先要做的就是先绘制出一个静态图,而从效果图中看出,静态图可以分为三个部分上方刻度
,游标刻度
,下方刻度数值
,绘制思路以及代码如下:
首先是一些初始参数:
/**
* 初始化默认值
*/
private void initPosAndLocation() {
//maxValue多加的数值是了保证scaleItemCount为10的倍数
if (isFM) {
oneItemValue = 1;
maxValue = (int) (1080+spaceScaleCount*oneItemValue);
minValue = 875;
} else {
oneItemValue = 9;
maxValue = (int) (1602+oneItemValue);
minValue = 531;
}
//一个小刻度的宽度
scaleDistance = getMeasuredWidth() / (showItemSize * spaceScaleCount);
//总的刻度数量
scaleItemCount = (int) (((maxValue - minValue) / oneItemValue));
Log.d(TAG,"scaleItemCount="+scaleItemCount+"---showItemSize="+(showItemSize*spaceScaleCount));
//尺子长度=总的个数*一个的宽度
viewWidth = scaleItemCount * scaleDistance;
//回弹目标位置
currsorTargetPos = (showItemSize/2) * spaceScaleCount;
//更新当前选中的位置,以及选中位置附近特定刻度的范围
updatecurrsorPos(currsorTargetPos);
//游标坐标位置
currsorLocation = currsorTargetPos*scaleDistance;
valueLocation=0;
//游标偏移量
currsorPosDiff=0;
//游标点的偏移量
pointLocationDiff=0;
//更新当前游标指向的数值
updateCurrsorValue();
}
其次绘制的是下方刻度数值
,因为要先得到数值的高度,后面才可以根据数值的高度以及数值上方间隙大小来绘制上方的刻度,另外需要注意的是,由于数值是可以循环滚动的,所以需要绘制多一次来实现循环滚动的效果,参考自自定义 View 循环滚动刻度控件
...
...
//绘制下方刻度值,每2个大刻度绘制一次数值
for (int i = 0; i < scaleItemCount; i += spaceScaleCount*2) {
drawScaleValue(canvas, i, -1);
}
for (int i = 0; i < scaleItemCount; i += spaceScaleCount*2) {
drawScaleValue(canvas, i, 1);
}
...
...
/**
* 绘制刻度值
*
* @param canvas 画布
* @param index 刻度值位置
* @param type 正向绘制还是逆向绘制
*/
private void drawScaleValue(Canvas canvas, int index, int type) {
float location = -valueLocation + index * scaleDistance * type;
float textValue;
paintText.setColor(scaleTextColor);
paintText.setTextSize(scaleTextSize);
if (type < 0) {
textValue = (maxValue / oneItemValue - index) * oneItemValue;
// textValue =(index+spaceScaleCount) * oneItemValue + minValue;
} else {
if(index>=scaleItemCount){
index-=scaleItemCount;
}
textValue = index * oneItemValue + minValue;
}
if (textValue >= maxValue) {
textValue = minValue;
}
String drawStr;
if(isFM){
drawStr = String.valueOf(textValue/10f);
}else {
drawStr = String.valueOf((int)textValue);
}
paintText.getTextBounds(drawStr, 0, drawStr.length(), textRect);
canvas.drawText(drawStr, location - textRect.width() * 1f / 2, viewHeight - getPaddingBottom(), paintText);
}
现在有了下方数值的高度,可以绘制上方的刻度
了
//...
//绘制上方刻度,只绘制可视范围
for (int i = 0; i < showItemSize * spaceScaleCount; i++) {
drawScale(canvas, i);
}
//...
private void drawScale(Canvas canvas, int index) {
float location = index * scaleDistance;
float drawBottom = viewHeight - scaleTextSpaceHeight - getPaddingBottom() - textRect.height();
//...中间省略了游标的位置,后面会说
if ((index - currsorPosDiff) >= 0 && (index - currsorPosDiff) % spaceScaleCount == 0) {
paint.setColor(scaleHighlightColor);
canvas.drawLine(location, drawBottom - scaleHeight, location, drawBottom, paint);
} else {
paint.setColor(scaleColor);
canvas.drawLine(location, drawBottom - scaleHeight, location, drawBottom, paint);
}
}
最后就剩下游标的绘制
了,如果不做平滑滑动的效果,游标绘制其实和普通刻度绘制没什么区别,只是绘制的位置变化了一下,波浪游标的高度计算我使用了sa函数即sin(x)/x
,其函数图像如下:
由于游标的静止高度是固定的,那么可以先计算出来并保存到数组中
//游标高亮位置突出的刻度的信息,(绘制位置,绘制颜色)
private float[][] currsorGradientScaleInfo;
//游标中心与最左/右的距离
private int currsorGradientSize=6;
...
...
//计算游标位置突出的各个高度
currsorGradientScaleInfo=new float[currsorGradientSize+1][3];
for (int i = 0; i < currsorGradientScaleInfo.length; i++) {
if(currsorGradientSize==i){
currsorGradientScaleInfo[i][0] = scaleHeight * 3.2f;
currsorGradientScaleInfo[i][1] = scaleHeight * 2;
currsorGradientScaleInfo[i][2] = getLinearColor(currsorStartColor,currsorEndColor,1);
}else {
float rate=(float) (Math.sin((i - currsorGradientSize)) / ((i - currsorGradientSize)));
currsorGradientScaleInfo[i][0] = scaleHeight * rate * 3;
currsorGradientScaleInfo[i][1] = scaleHeight * rate * 2;
currsorGradientScaleInfo[i][2] = getLinearColor(currsorStartColor,currsorEndColor,rate);
}
Log.d(TAG,"currsorGradientScaleInfo top="+ currsorGradientScaleInfo[i][0]);
}
既然绘制位置有了,那么就可以在绘制上方刻度的时候加入游标位置的绘制
private void drawScale(Canvas canvas, int index) {
float location = index * scaleDistance;
float drawBottom = viewHeight - scaleTextSpaceHeight - getPaddingBottom() - textRect.height();
//当次绘制的与普通scale的高度差值
float tempTopDiff = 0;
float tempBottomDiff = 0;
//与前一个的高度差值
float tempLastTopDiff = 0;
float tempLastBottomDiff = 0;
if (index == currsorPos) {
tempTopDiff=currsorGradientScaleInfo[currsorGradientSize][0];
tempBottomDiff=currsorGradientScaleInfo[currsorGradientSize][1];
float lastTop =currsorGradientScaleInfo[currsorGradientSize-1][0];
float lastBottom =currsorGradientScaleInfo[currsorGradientSize-1][1];
tempLastTopDiff = (tempTopDiff - lastTop) * (pointLocationDiff / scaleDistance);
tempLastBottomDiff = (tempBottomDiff - lastBottom) * (pointLocationDiff / scaleDistance);
paint.setColor((int) currsorGradientScaleInfo[currsorGradientSize][2]);
canvas.drawLine(location, drawBottom -scaleHeight- tempTopDiff + tempLastTopDiff, location, drawBottom - tempBottomDiff + tempLastBottomDiff, paint);
} else if (index >= gradientLeftPos && index <= currsorPos) {
tempTopDiff=currsorGradientScaleInfo[index-currsorPos+currsorGradientSize][0];
tempBottomDiff=currsorGradientScaleInfo[index-currsorPos+currsorGradientSize][1];
float lastTop = 0;
float lastBottom = 0;
if (index > gradientLeftPos) {
lastTop=currsorGradientScaleInfo[index-currsorPos+currsorGradientSize-1][0];
lastBottom=currsorGradientScaleInfo[index-currsorPos+currsorGradientSize-1][1];
}
tempLastTopDiff = (tempTopDiff - lastTop) * (pointLocationDiff / scaleDistance);
tempLastBottomDiff = (tempBottomDiff - lastBottom) * (pointLocationDiff / scaleDistance);
paint.setColor((int) currsorGradientScaleInfo[index-currsorPos+currsorGradientSize][2]);
canvas.drawLine(location, drawBottom - scaleHeight - tempTopDiff + tempLastTopDiff, location, drawBottom - tempBottomDiff + tempLastBottomDiff, paint);
} else if (index > currsorPos && index <= gradientRightPos) {
tempTopDiff=currsorGradientScaleInfo[currsorGradientSize-(index - currsorPos)][0];
tempBottomDiff=currsorGradientScaleInfo[currsorGradientSize-(index - currsorPos)][1];
float lastTop = 0;
float lastBottom = 0;
if(index==currsorPos+1){
lastTop = currsorGradientScaleInfo[currsorGradientSize][0];
lastBottom =currsorGradientScaleInfo[currsorGradientSize][1];
}else {
lastTop = currsorGradientScaleInfo[currsorGradientSize-(index - currsorPos)+1][0];
lastBottom = currsorGradientScaleInfo[currsorGradientSize-(index - currsorPos)+1][1];
}
tempLastTopDiff = (lastTop - tempTopDiff) * (pointLocationDiff / scaleDistance);
tempLastBottomDiff = (lastBottom - tempBottomDiff) * (pointLocationDiff / scaleDistance);
paint.setColor((int) currsorGradientScaleInfo[currsorGradientSize-(index - currsorPos)][2]);
canvas.drawLine(location, drawBottom - scaleHeight - tempTopDiff - tempLastTopDiff, location, drawBottom - tempBottomDiff - tempLastBottomDiff, paint);
} else {
if ((index - currsorPosDiff) >= 0 && (index - currsorPosDiff) % spaceScaleCount == 0) {
paint.setColor(scaleHighlightColor);
canvas.drawLine(location, drawBottom - scaleHeight, location, drawBottom, paint);
} else {
paint.setColor(scaleColor);
canvas.drawLine(location, drawBottom - scaleHeight, location, drawBottom, paint);
}
}
}
注意:我这里还增加了一个动态高度的计算,目的就是为了在拖动游标的时候,让游标有一个波浪形的滑动效果
//根据当前滑动的位置在两个相邻刻度间的占比,再计算出一个动态位置
tempLastTopDiff = (tempTopDiff - lastTop) * (pointLocationDiff / scaleDistance);
tempLastBottomDiff = (tempBottomDiff - lastBottom) * (pointLocationDiff / scaleDistance);
二、动画效果的实现
效果图中的动画效果主要有两种,onTouch事件
,属性动画
对于onTouch事件,这里使用了手势监听GestureDetector
来接管处理,只是处理了滑动事件
...
...
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (!isPlayAnimation) {
scrollCurrsor(-distanceX);
}
return true;
}
...
...
/**
* 处理游标滑动的距离
* @param x 当次的滑动距离
*/
private void scrollCurrsor(float x) {
currsorLocation+=x;
if(currsorLocation<0){
currsorLocation=0;
}else if(currsorLocation>showItemSize*spaceScaleCount*scaleDistance){
currsorLocation=showItemSize*spaceScaleCount*scaleDistance;
}
float realPos = currsorLocation / scaleDistance;
currsorPos = (int) realPos;
if (currsorPos > showItemSize * spaceScaleCount) {
currsorPos = showItemSize * spaceScaleCount - 1;
} else if (currsorPos < 1) {
currsorPos = 1;
}
//计算点与游标中心的偏移,用于平滑过渡
pointLocationDiff = Math.abs(currsorLocation - currsorPos * scaleDistance);
updatecurrsorPos(currsorPos);
if (lastCurrsorPos != currsorPos) {
if (currsorPos >=currosrEdgeRight ) {
Log.d(TAG, "slide to the right edge ");
sendEdgeMessage(EDGE_RIGHT);
} else if (currsorPos <= currosrEdgeLeft) {
Log.d(TAG, "slide to the left edge");
sendEdgeMessage(EDGE_LEFT);
} else {
loopScaleHandler.removeMessages(HANDLER_FLAG_CHECK_EDG);
}
}
lastCurrsorPos = currsorPos;
invalidate();
}
可以看出,前面绘制上方刻度的一个动态位置计算的数值就是从滑动这边计算出来的,而滑动最主要的就是更新当前游标的中心位置,以达到游标跟随手指一动
对于属性动画,也就是效果图中的回弹效果,这个比较简单,使用valueAnimator
更新位置坐标就好
/**
* 滑动回目标位置
*/
private void scrollTargetCurrsor() {
loopScaleHandler.removeCallbacksAndMessages(null);
stopAnimator();
final float tempOld = valueLocation;
final float target = valueLocation - (currsorTargetPos - currsorPos) * scaleDistance;
if (currsorPosDiff != 0 && (currsorTargetPos - currsorPos) % spaceScaleCount == 0) {
} else {
currsorPosDiff =((currsorTargetPos - currsorPos) % spaceScaleCount + currsorPosDiff) % spaceScaleCount;
}
Log.d(TAG, "scrollTargetCurrsor currsorPosDiff=" + currsorPosDiff);
ValueAnimator valueAnimator = ValueAnimator.ofFloat(currsorLocation, currsorTargetPos*scaleDistance);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
scrollCurrsor(value-currsorLocation);
}
});
ValueAnimator valueAnimator1 = ValueAnimator.ofFloat(tempOld, target);
valueAnimator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//调整下方数值的绘制位置,已达到循环滚动效果
adjustValueLocationByAnimator((float) animation.getAnimatedValue());
}
});
final AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(valueAnimator, valueAnimator1);
animatorSet.setInterpolator(new OvershootInterpolator());
//根据拖动位置距离目标位置的大小来设置回弹时间
int time = (int) (Math.abs(currsorTargetPos - currsorPos) * 1f / (showItemSize * spaceScaleCount) * animatDurationResilience);
animatorSet.setDuration(time);
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
updateCurrsorValue(true);
isPlayAnimation = false;
}
@Override
public void onAnimationStart(Animator animation) {
animatorPlayingList.add(animatorSet);
isPlayAnimation = true;
}
});
animatorSet.start();
}
总结
由于完善的时候要考虑的情况比较多,比如大刻度与数值位置要对齐,多个动画同时播放的导致位置出错等等,而且贴出来代码是我完善后的,所以代码可能比较多,但是代码量不是重点,我只希望大家理解思路就好,其实如果单纯实现波浪效果核心思路就下面两点
1. 通过函数来计算游标突出的位置(学会用函数来表达图像很重要)
2. 通过滑动偏移量来计算游标浮动的大小
ps:由于这种计算比较多控件,在写文章想表达自己完整思路的时候比较困难(或许是我表达能力不行),所以文章看不懂的推荐大家直接看demo里的源码吧,跑一跑可能就明白了
源码地址
DemoList