阅读 308

Android自定义-手势缩放折线图

一、手势带给折线图更好的体验

1.左右可滑动滑动的折线图。

好的自定义离不开手势,大家都看过K线图吧,见证过好几个技术群演变成日常股票分享群的过程。

看股票货币的朋友应该很熟悉。看到图中不仅可以左右移动还可以缩放。这交互性太炫酷了吧。接下来我们一步步来。手势滑动的计算至关重要但是没有超过初中生的计算分析能力,所以咋们也可以。先来个小案例。我们通过测量手势滑动距离来进行设置屏幕中间圆圈的位置:

首先创建简单的类绘制一个圆重写onTouchEvent事件:

class LHC_Scroll_distance_View @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyle) {
    //
    var viewxToY = 60f
     var maxXInit=0f
    override fun onDraw(canvas: Canvas) {
        maxXInit= measuredWidth.toFloat()
        drawCircle(canvas)
    }

    private fun drawCircle(canvas: Canvas) {
        val linePaint = Paint()
        linePaint.isAntiAlias = true
        linePaint.strokeWidth = 20f
        linePaint.strokeCap = Paint.Cap.ROUND
        linePaint.color = Color.RED
        linePaint.style = Paint.Style.FILL

       
        canvas.drawCircle(viewxToY, (measuredHeight/2).toFloat(), 60f, linePaint)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
            
            }
            MotionEvent.ACTION_MOVE -> {
               
            }
      
        }
        return true
    }
}

复制代码

运行之后

onTouchEvent可以根据屏幕MotionEvent信息进行计算...不清楚的可以看我的另一篇文章或者其他好文了解点View和ViewGroup的点击事件分发流程。

1.MotionEvent.ACTION_DOWN 字面明白当触发按下操作时候触发回调。
2.MotionEvent.ACTION_MOVE 返回按下和抬起中间的所有的点。
源码里面是这样定义的:
    /**
     * Constant for {@link #getActionMasked}: A change has happened during a
     * press gesture (between {@link #ACTION_DOWN} and {@link #ACTION_UP}).
     * The motion contains the most recent point, as well as any intermediate
     * points since the last down or move event.
     */
public static final int ACTION_MOVE             = 2;

复制代码

按下,滑动,抬起等都会收到屏幕点击MotionEvent(位置,时间...详情),那屏幕上按下的点所在的位置,可以很好的获取。那左右滑动如何获取每次滑动的距离呢?

按下之后到抬起之间在不断的通知回调onTouchEvent的MotionEvent.ACTION_MOVE类型事件,所以我们会在MOtionEvent.ACTION_MOVE下面不断收到屏幕MotionEvent信息。event.x在不断变化,同样对应的圆也应该不断的随着细微的变化而同步变化平移。这样即可实现了左右滑动。

我们的圆心x轴圆心坐标默认(viewxToY,measureHeight/2),滑动过程保证viewXToY随着滑动变化而同样的进行View刷新变化,那就实现了圆圈的平移!!

如上图,在DOWN和UP之间有其实有连连不断的MotionEvent.ACTION_MOVE通知,我们每次通知过来用event.x-上一次记录的event.x()就是每次滑动通知的距离小段。所有的滑动小段加起来就是滑动距离...说的是否明白。

  override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> startX = event.x
            MotionEvent.ACTION_MOVE -> {
                //每次通知计算滑动的一点点
                val dis = event.x - startX
                //记录这次移动结束的event.x就是下一次的滑动起始滑动的位置
                startX = event.x
                //将每次的滑动小段距离在当前距离的基础上叠加起来
                viewxToY=viewxToY+dis
                //通知刷新View
                invalidate()
            }
        }
        return true
    }

复制代码

迫不及待看看效果

我们看到基本和滑动同步,基本没啥大问题。我们来改造一波我们的代码看看折线图的。 先来个简单的平移一波x轴。慢慢来....嗯..原本搞个随机颜色的尴尬😅。

    //记录滑动距离
    private var viewxToY=0f
    
     //绘制x轴
    val pathx = Path()
    //手势滑动的距离加上
    pathx.moveTo(0f+viewxToY, 0f)
    pathx.lineTo(measuredWidth - 20f+viewxToY, 0f)
    
     var  startX=0f
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (event.action == MotionEvent.ACTION_DOWN) {
            clickDow = true
            startX=event.x
            return true
        }
        if (event.action==MotionEvent.ACTION_MOVE){
            //每次通知计算滑动的一点点
            val dis = event.x - startX
            //记录这次移动结束的event.x就是下一次的滑动起始滑动的位置
            startX = event.x
            //将每次的滑动小段距离在当前距离的基础上叠加起来
            viewxToY += dis
            invalidate()
        }
        ...
        


复制代码

x轴下方字体我们也修改一下对应的坐标。

 //绘制背景
 //背景和位子的left+right都+viewxToY即可
 canvas.drawRoundRect(-getTextWidth(xtitle_paint, "${index + 2}月月") / 2+viewxToY, -getTextHeight(xtitle_paint) / 2, getTextWidth(xtitle_paint, "${index + 2}月月") / 2+viewxToY, getTextHeight(xtitle_paint) / 2, 10f, 10f, getTextBackgroudPaint(40, 5, 0))
            //绘制文字
 canvas.drawText("${index + 2}月", 0, "${index + 2}月".length, -getTextWidth(xtitle_paint, "${index + 2}月") / 2+viewxToY, getTextHeight(xtitle_paint) / 3, xtitle_paint)
       

复制代码

\

到这里我们实现了随着手势计算水平方向相对的滑动位置。但是看着很别扭....有没有发现滑动是任意滑动。但是我们的滑动不应该是为了滑动而滑动。x轴方向的内容超出屏幕滑动才显得很有必要。所以需要手势滑动中做好限制。接下来我们继续分析:

  • 折线绘制的开始我们的却没有去结合业务或者正式的去规划每个细节。折线图的展示很多场景和需求都不能够完全在一个屏幕宽度内部呈现,而开始我们就没有考虑到这一点,我想这个对于大家很简单,水平方向我们继续添加月份和坐标,水平方向x轴的长度我们通过每格宽度*坐标个数-1即等于所有的宽度去绘制x轴即可。
1.我们必须知道X轴平移左右最大和最小距离。
2.因为x轴我们经过变换最大滑动距离为minXInit=0,代表如果在圆点向右边是滑动不了的。
3.而我们的x轴因为超出了屏幕宽度所以我们想看到屏幕右边的内容=x轴的宽度-屏幕的宽度。
不知道说的ok不。看下图清晰一下。

复制代码

代码如下:

 if (event.action==MotionEvent.ACTION_MOVE){
            //每次通知计算滑动的一点点
            val dis = event.x - startX
            //记录这次移动结束的event.x就是下一次的滑动起始滑动的位置
            startX = event.x
            //将每次的滑动小段距离在当前距离的基础上叠加起来
            minXInit=measuredWidth-xwidthMax
            if (viewxToY + dis < minXInit) {
                viewxToY = minXInit
            } else if (viewxToY + dis > maxXInit) {
                viewxToY = maxXInit
            } else {
                viewxToY += dis
            }
            invalidate()
        }
复制代码

有点迫不及待了,效果如下:

2.手势可缩放的折线图

ScaleGestureDetector作为android中的检测手势的通知工具类很少被我们使用,常见的场景也就图片和图表折线图这些会出现缩放。当然了onTouchEvent也可以通过多触点(勾股定理)进行计算缩放比例,哈哈偷偷看看ScaleGestureDetector就明白了。

1.通过onTouchEvent来计算缩放比例

1. onTouchEvent中我们自己测量?还是我们看看ScaleGestureDetector内部拿着event是如何测
量缩放的呢?我们从通知方法入手override fun onScale(detector: ScaleGestureDetector)
   public float getScaleFactor() {
        if (inAnchoredScaleMode()) {
            // Drag is moving up; the further away from the gesture
            // start, the smaller the span should be, the closer,
            // the larger the span, and therefore the larger the scale
            final boolean scaleUp =
                    (mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan < mPrevSpan)) ||
                    (!mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan > mPrevSpan));
            final float spanDiff = (Math.abs(1 - (mCurrSpan / mPrevSpan)) * SCALE_FACTOR);
            return mPrevSpan <= mSpanSlop ? 1 : scaleUp ? (1 + spanDiff) : (1 - spanDiff);
        }
        
        return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
    }
这里我们大概看到mPrevSpan,mCurrSpan这些变量通过各种判断和计算得到缩放因子。那我们顺着找
到mPrevSpan,mCurrSpan相关的附值地方。不用想就在onTouchEvent中,我们找到onTouchEvnet

 public boolean onTouchEvent(MotionEvent event) {
   ...
   //缩放滑动模式下通过股沟定理计算斜边。
   if (inAnchoredScaleMode()) {
            span = spanY;
        } else {
            span = (float) Math.hypot(spanX, spanY);
        }
     }
  //滑动过程给mCurrSpan负值   
  if (action == MotionEvent.ACTION_MOVE) {
            mCurrSpanX = spanX;
            mCurrSpanY = spanY;
            mCurrSpan = span;

            boolean updatePrev = true;

            if (mInProgress) {
                updatePrev = mListener.onScale(this);
            }

            if (updatePrev) {
                mPrevSpanX = mCurrSpanX;
                mPrevSpanY = mCurrSpanY;
                //记录这次结束时的斜边
                mPrevSpan = mCurrSpan;
                mPrevTime = mCurrTime;
            }
        }   
 
 }

上面我们知道可以通过双趾之间的距离进行前后相除得到实时的缩放比例。那我们接下来再onTouchEvent自己进行计算试试。

复制代码

接下来我们自己模仿一个进行测量缩放比例两趾之间的距离前后进行相除:

代码如下:

when (event.action and MotionEvent.ACTION_MASK) {
            MotionEvent.ACTION_DOWN -> {
                //1.表示单点事件
                eventModeType = 1f
            }
            MotionEvent.ACTION_POINTER_DOWN -> {
                //多点触控
                oriDis = distance(event)
                if (oriDis > 10f) {
                    //2.表示多点触碰类型
                    eventModeType = 2f
                }

            }
            MotionEvent.ACTION_MOVE -> {
                if (eventModeType == 2f) {
                        // 获取两个手指缩放时候的之间距离
                        val newDist = distance(event)
                        if (newDist > 10f) {
                            //通过当前的距离除以上一手指按下两趾头之间的距离就为实时的缩放 
                            curScale = newDist / oriDis
                        }
                }
                //通知刷新View
                invalidate()
            }
            MotionEvent.ACTION_UP->{
                eventModeType = 0f
            }
            MotionEvent.ACTION_POINTER_UP->{
                eventModeType = 0f
            }
        }
        return true
    }

复制代码

效果:

2.利用ScaleGestureDetector 监测缩放因子进行缩放

我们上面已经见过ScaleGestureDetector的部分代码了,有时间大家也可泛读研究。我们接下来利用 ScaleGestureDetector来进行缩放。首先初始化然后进行打印

//1.在onTouchEvnet里面进行设置event
 override fun onTouchEvent(event: MotionEvent): Boolean {
        mScaleGestureDetector!!.onTouchEvent(event)
        ..
 }       

private fun initScaleGestureDetector() {
        mScaleGestureDetector = ScaleGestureDetector(context, object : SimpleOnScaleGestureListener() {
            override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
                return true
            }

            override fun onScale(detector: ScaleGestureDetector): Boolean {
                Log.e(TAG, "onScale:" + detector.scaleFactor)
                return false
            }

            override fun onScaleEnd(detector: ScaleGestureDetector) {}
        })
    }
复制代码

detector.scaleFactor当手指间距由小到大放大操作时候->通过打印缩放因子看看:

放大过程
onScale: 1.0849714
onScale: 1.109393
onScale: 1.1507562
onScale: 1.1814013
onScale: 1.2101753
onScale: 1.2385316
onScale: 1.2752386
onScale: 1.3162968
onScale: 1.3745319
onScale: 1.0194724
onScale: 1.0061256
onScale: 1.0237223
onScale: 1.0157682
//缩小过程
 0.99745494
 0.99683857
 0.9947594
 0.98914015
 0.9827625
 0.9775298
 0.9719838
 0.96702707
 0.9618665
 0.9551136
 0.94867754
 0.94313
 0.9390567
 0.9344024
 0.93175083
 0.928374
 0.9219731
 0.9163468
 0.91060144
 0.90418917
 0.8970686
 0.8903642
 0.8834786
 0.879944
 0.87690777
 0.8743616
 0.87640566
 0.8807793
 0.88613236
 0.8932392
 0.89954007
 0.89262646
 0.9854972
 0.9857342
 0.9946703
 1.0082809
 0.99545985
 0.9920981
 0.9926125
 0.9973208
 1.0117983
 1.0115927
 1.002579
 0.9923982
 0.98811483
 0.98945755
 0.99297947
 1.0000068
 1.0023786
 1.00011
 0.994298
 0.9923681
 0.9915295
 0.99028814
 0.99649304
 1.0011351
 1.0022005
 0.9973822
 0.99489397
 0.98849684
 0.9891182
 0.9916089
 0.99125963
 0.991371
 0.99094766
 0.9904697
 0.9905223
 0.98756933
 0.9896349
 1.0016375
 1.0006968
 0.99415475
 0.98720026
 0.9908081
复制代码

这个缩放因子并和我们想要一样,逐渐增大,只是刚开始的一些数据体现着增大的趋势,因为看了源码知道,是当前的双趾间距除以上一个时刻的间距时间极短而双趾局部按压面积较大,接触屏幕的面积内所触摸的部分系列点中心的不同会导致结果不同。咋么说了例如:

上一个时刻双指在屏幕上之间距离是10,现在是11那么缩放因子=11/10=1.1 上一个时刻双指在屏幕上之间距离是15,现在是15.1 那么缩放因子=15.1/15=1.0000000...1 这里可能有点犯错,并不是手指之间的间距大那么缩放因子就大很关键。 ... 所以缩放因子因为屏幕按压的时间短,手指接触屏幕的点重心变化大导致这个因子是一个1和1.0..1.1..1.2..左右浮动的数字。

这一些列的数字我们如何和叠加增大和缩小一一映射?

正如我们平移不断的在进行相加么?这样只会导致无限放大...而这里我们对于1.0左右无法判断缩小还是放大的数据就不能用加法而应该用乘法。如下 1.01.0=1.0 1.0+1.0=2.0 而在手指缩放的一瞬间+能让这个数值变成及其大的数子。 1.00.5=0.5 1.0+0.5=1.5 乘法不仅在增大时候因为缩放因子都会变为大于1.0,缩小时候为零点几<1.0所以乘法恰好符合我们的缩放的规律。接下来我们进行前后叠加计算缩放。

  //在这里插入代码片
  private fun initScaleGestureDetector() {
        mScaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
            override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
                return true
            }

            override fun onScale(detector: ScaleGestureDetector): Boolean {
               //为了保持连续性->当前的伸缩值*之前的伸缩值
                curScale = detector.scaleFactor * preScale
                Log.e("ScaleGestureDetector", "onScale: "+ detector.scaleFactor )
                //当放大倍数大于2或者缩小倍数小于0.1倍 就不伸 
                if (curScale > 2 || curScale < 0.1) {
                    preScale = curScale;
                    return true
                }
                //保存上一次的伸缩值
                preScale = curScale;
                invalidate()
                return false
            }

            override fun onScaleEnd(detector: ScaleGestureDetector) {}
        })
    }
复制代码

接下来我们设置代码的x轴和折线的x,y坐标都乘curScale


        //绘制x轴
        val pathx = Path()
        //手势滑动的距离加上
        pathx.moveTo((0f + viewxToY)*curScale, 0f)
        pathx.lineTo((xwidthMax - marginXAndY + viewxToY)*curScale, 0f)

        val horizontalPath = Path()
        horizontalPath.moveTo((xwidthMax - arrowLength + viewxToY)*curScale, arrowLRHeight)
        horizontalPath.lineTo((xwidthMax - marginXAndY + viewxToY)*curScale, 0f)
        horizontalPath.lineTo((xwidthMax - arrowLength + viewxToY)*curScale, -arrowLRHeight)
        pathx.addPath(horizontalPath)
        //画x轴
        canvas.drawPath(pathx, x_paint)

复制代码

绘制折线图乘缩放比例


  //绘制折线图
    private fun drawLine(pointList: java.util.ArrayList<ViewPoint>, canvas: Canvas) {
        val linePaint = Paint()
        val path = Path()
        linePaint.style = Paint.Style.STROKE
        linePaint.color = Color.argb(255, 225, 225, 255)
        linePaint.strokeWidth = 10f


        val circle_paint = Paint()
        circle_paint.strokeWidth = 10f
        circle_paint.style = Paint.Style.FILL


        //连线
        path.moveTo(viewxToY*curScale, 0f)
        for (index in 0 until pointList.size) {
            path.lineTo((pointList[index].x + viewxToY)*curScale, pointList[index].y*curScale)
        }
        canvas.drawPath(path, linePaint)

        path.reset()
        //渐变色菜的填充
        for (index in 0 until pointList.size) {
            path.lineTo((pointList[index].x + viewxToY)*curScale, pointList[index].y*curScale)
        }
        val endIndex = pointList.size - 1
        path.lineTo((pointList[endIndex].x + viewxToY)*curScale, 0f)
        path.close()
        linePaint.style = Paint.Style.FILL
        linePaint.shader = getShader()
        linePaint.setShadowLayer(16f, 6f, -6f, Color.argb(100, 100, 255, 100))
        canvas.drawPath(path, linePaint)


        //画定点圆圈
        for (index in 0 until pointList.size) {
            circle_paint.shader = getShaders()
            canvas.drawCircle((pointList[index].x + viewxToY)*curScale, pointList[index].y*curScale, 16f, circle_paint)
        }


    }

复制代码

看看效果

在这里插入图片描述

那接下来的事就比较简单了,凡事能看到的东西想缩放的就乘这个缩放比例即可。

写到这里,大家就算不膜拜也可以用你金贵的手指点赞加评论交流呢?

你以为这就完事了?接下来我们才要干大事。有好友想要画曲线,一次性让你画个够好不好。

文章分类
Android
文章标签