android案例-波浪调频刻度尺

知识点

  1. 自定义view绘制的运用
  2. 对于图像转函数的一些应用
  3. 属性动画的使用

效果图

myloopscaleview.gif

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,其函数图像如下:

image.png

由于游标的静止高度是固定的,那么可以先计算出来并保存到数组中

    //游标高亮位置突出的刻度的信息,(绘制位置,绘制颜色)
    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里的源码吧,跑一跑可能就明白了


手动狗头.png

源码地址

DemoList

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容

  • 一 基础: 自定义View实现跟随手指滚动的刻度尺,实现了类似SeekBar的滑动选中效果。项目地址,欢迎star...
    _那个人阅读 3,598评论 0 2
  • 【Android 动画】 动画分类补间动画(Tween动画)帧动画(Frame 动画)属性动画(Property ...
    Rtia阅读 6,144评论 1 38
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 5,110评论 5 13
  • The string "PAYPALISHIRING" is written in a zigzag patter...
    nafoahnaw阅读 213评论 0 0
  • 在安逸的环境待久了,我们就容易产生惰性,慢慢失去了思考的能力。前几天,在坦桑尼亚任教的好友突然发信息给我。 她说:...
    漓沫晓儿阅读 643评论 3 11