Android自定义View基础篇

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android自定义View基础篇相关的知识,希望对你有一定的参考价值。

参考技术A 在我们自定义View,尤其是制作一些复杂炫酷的效果的时候,实际上是将一些简单的东西通过数学上精密的计算组合到一起形成的效果。这其中可能会涉及到画布的相关操作(旋转),以及一些正余弦函数的计算等,这些内容就会用到一些角度、弧度相关的知识。

圆一周对应的角度为360度(角度),对应的弧度为2π弧度。

故得等价关系:360(角度) = 2π(弧度) ==> 180(角度) = π(弧度)

几种创建或使用颜色的方式

android自定义属性可分为以下几步:

2.自定义View中获取属性

3.在布局中使用

4.属性值的类型归纳

android 自定义view: 矩形图表

本系列自定义View全部采用kt

系统mac

android studio: 4.1.3

kotlin version1.5.0

gradle: gradle-6.5-bin.zip

本篇效果:

tips: 本篇是在上一篇的基础上来绘制的,背景表格,和左侧文字都是上一篇的东西, 如果不清楚可以先学习上一篇!

绘制网格和文字

这是上一篇的代码,我把它封装成了一下,如果需要请下载看看吧 !

class E2RectChartBlogView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : BaseChartView(context, attrs, defStyleAttr) 

    init 
        // 初始化数据
        originList = arrayListOf(
            148, 120, 21, 60, 10,
            70, 80, 100, 222, 74,
        )
    

    override fun onDraw(canvas: Canvas) 
        canvas.scale(0.8f, 0.8f, width / 2f, width / 2f)
        // 绘制网格
        super.drawGrid(canvas)
        // 绘制文字
        super.drawLeftText(canvas)
    

绘制矩形

首先绘制矩形,先得确定要绘制到什么地方:

如图:

我们需要将矩形绘制到每一格的正中心位置,

假设我们需要绘制矩形高度为 90

我们知道

  • 每一格的宽度 eachWidth

  • 需要绘制的矩形宽度(rectWidth) = eachWidth * 0.6

  • 每一格的矩形高度 = View#height / 数组最大值

  • 对应的高度 = 每一个的矩形高度 * 90

那么最终矩形的坐标为

  • left = (eachWidth / 2f ) - (eachWidth * 0.6) / 2f
  • right = left + rectWidth
  • top = 每一个的矩形高度 * 高度对应的值
  • bottom = height

来看看代码:

 // 每一格矩形的宽
    private val eachWidth: Float
        get() 
            return width / verticalCount.toFloat()
         

override fun onDraw(canvas: Canvas) 
   
   // 每一格的矩形宽度
   val eachWidth = eachWidth
   // 矩形的宽度
   val rectWidth = eachWidth * 0.6f

   // 计算出每一格的高度
   val eachGrid = height.toFloat() / originMax

   // 绘制矩形
   drawRect(canvas, eachWidth, rectWidth, eachGrid)
 

 /*
  * 作者:史大拿
	* 创建时间: 9/26/22 3:28 PM
	* @param eachWidth: 每一格的矩形宽度
	* @param rectWidth: 每一个矩形的宽
  * @param eachGridHeight: 每一格矩形的高度
	*/
private fun drawRect(
  canvas: Canvas,
  eachWidth: Float,
  rectWidth: Float,
  eachGridHeight: Float
) 
  var tempLeft = eachWidth / 2f - rectWidth / 2f
  originList.forEach 

    val left = tempLeft
    val top = height - eachGridHeight * it
    val right = left + rectWidth
    val bottom = height.toFloat()

    canvas.drawRoundRect(left, top, right, bottom, 0f, 0f, paint)
    tempLeft += eachWidth
  

可以看出,绘制成功了,但是绘制出表格外面了,这种情况是不允许的,

和上一篇的套路一样,只保留屏幕内的数据(裁剪一下)

override fun onDraw(canvas: Canvas) 
   canvas.withSave 
     // 裁剪
     canvas.clipRect(0, 0, width, height)
     // 绘制矩形
     drawRect(canvas, eachWidth, rectWidth, eachGrid)
   

手势监听

手势监听还是用到了 GestureDetectorCompat

如果手势监听使用有点模糊 建议看这一篇 android 图解 PhotoView,从‘百草园’到‘三味书屋’!

首先先让他滚动起来

private var offsetX = 0f
private var downX = 0f
private var originX = 0f

private inner class E2RectGesture : GestureDetector.OnGestureListener 
    override fun onDown(event: MotionEvent): Boolean 
				// 记录按下位置
        downX = event.x
        // 记录上一次偏移量
        originX = offsetX
        return true
    
    override fun onScroll(
        e1: MotionEvent,// down按下的位置
        event: MotionEvent, // 当前滑动的事件
        distanceX: Float, // event - e1 的X距离
        distanceY: Float // event - e2 的Y距离
    ): Boolean 
        when (event.action) 
            MotionEvent.ACTION_MOVE -> 
                // 当前偏移位置 = 当前位置 - 按压位置 + 原始偏移量
                offsetX = event.x - downX + originX

                // 修复X轴距离 保证offsetX 不会滑出屏幕
                repairOffSetX()
            
        

        invalidate()
        return false
    

  ....


private fun repairOffSetX() 
  // 限制滑动距离
  if (offsetX > 0) 
    offsetX = 0f
  

  if (offsetX <= -(originList.size * eachWidth - width)) 
    	offsetX = -(originList.size * eachWidth - width)
  

使用GestureDetectorCompat

// 手势监听
private val gestureDetectorCompat = GestureDetectorCompat(context, E2RectBlogGesture())

@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean 
    gestureDetectorCompat.onTouchEvent(event)
    return true

记得设置offsetX 用来滑动

/*
     * 作者:史大拿
     * 创建时间: 9/26/22 3:28 PM
     * @param eachWidth: 每一格的矩形宽度
     * @param rectWidth: 每一个矩形的宽
     * @param eachGridHeight: 每一格矩形的高度
     */
    private fun drawRect(
        canvas: Canvas,
        eachWidth: Float,
        rectWidth: Float,
        eachGridHeight: Float
    ) 
        var tempLeft = eachWidth / 2f - rectWidth / 2f
        originList.forEach 
            val left = tempLeft + offsetX // TODO 设置偏移量
            val top = height - eachGridHeight * it
            val right = left + rectWidth
            val bottom = height.toFloat()

            /// 绘制矩形
           ....
        
    

设置fling事件

因为只可以左右滑动,所以只需要关心x轴即可

fling事件还是用到了OverScroller

private inner class E2RectGesture : GestureDetector.OnGestureListener 
		...
  	
  	 override fun onFling(
            e1: MotionEvent?,
            e2: MotionEvent?,
            velocityX: Float,
            velocityY: Float
        ): Boolean 
            val startX = offsetX.toInt() // 只需要关心x轴即可
            val startY = 0

            val vX = velocityX.toInt()
            val vY = 0

            val minX = -(originList.size * eachWidth - width).toInt()
            val maxX = 0

            val minY = 0
            val maxY = 0

            overScroll.fling(
                startX, startY,
                vX, vY,
                minX, maxX,
                minY, maxY,
                0, 0
            )

            postOnAnimation(this@E2RectChartView)
            return true
        


 override fun run() 
   // 计算偏移量 返回是否完成
   val isOver = overScroll.computeScrollOffset()
   if (isOver) 
     offsetX = overScroll.currX.toFloat()
     repairOffSetX() // 修复X轴距离
     invalidate()
     postOnAnimation(this) //
   
 

这里代码也比较简单,就不过多赘述了.

设置动画

动画其实就是在2秒内一直的变换高度即可

变换进度从0到1变换

只需要高度 * 变换进度即可

先来定义一个动画

private var currentFraction = 0f

private val admissionAnimation by lazy 
    val animator = ObjectAnimator.ofFloat(0f, 1f)
    animator.duration = 2000 // 动画时间
    animator.addUpdateListener 
        currentFraction = it.animatedValue as Float
        invalidate()
    
    animator

currentFraction 会在两秒内,从0 变换到1

现在只需要将currentFraction设置给矩形的高度即可

private fun drawRect(
        canvas: Canvas,
        eachWidth: Float,
        rectWidth: Float,
        eachGridHeight: Float
    ) 
        var tempLeft = eachWidth / 2f - rectWidth / 2f
        originList.forEach 

            val left = tempLeft + offsetX // 偏移量
            val top = height - eachGridHeight * it * currentFraction // 将当前进度设置给top
            val right = left + rectWidth
            val bottom = height.toFloat()

            canvas.drawRoundRect(left, top, right, bottom, 0f, 0f, paint)
            tempLeft += eachWidth
        
    

添加文字

文字的x,y位置和矩形的top位置是一样的

 override fun onDraw(canvas: Canvas) 
   
   canvas.withSave 
     // 裁剪
     canvas.clipRect(0, 0, width, height)
     // 绘制矩形
     drawRect(canvas, eachWidth, rectWidth, eachGrid)
   
		// 绘制文字 
   drawText(canvas, eachWidth, eachGrid)
 

 /*
	* 作者:史大拿
	* 创建时间: 9/26/22 4:04 PM
	* @param eachWidth: 每一格的矩形宽度
	* @param eachGridHeight: 每一格矩形的高度
	*/
private fun drawText(canvas: Canvas, eachWidth: Float, eachGridHeight: Float) 
        var tempX = eachWidth / 2f
        originList.forEachIndexed  _, value ->
            val text = "$value"

            // 文字的宽
            val textWidth = paint.measureText(text)

            val x = tempX - textWidth / 2f + offsetX
            val y = (height - eachGridHeight * value) - paint.fontMetrics.bottom

            // 如果在范围内才绘制
            if (x >= 0 && x <= width) 
                canvas.drawText(text, x, y, paint)
            

            tempX += eachWidth
        
    

可以看出,文字绘制成功了,但是我们想要的是和矩形一样一直变换

那么肯定和currentFraction有关系

同样的套路,y轴 * currentFraction即可

  private fun drawText(canvas: Canvas, eachWidth: Float, eachGridHeight: Float) 
        var tempX = eachWidth / 2f
        originList.forEachIndexed  _, value ->
            val text = String.format("%.0f", value * currentFraction)

            // 文字的宽
            val textWidth = paint.measureText(text)

            val x = tempX - textWidth / 2f + offsetX
            val y = (height - eachGridHeight * value * currentFraction) - paint.fontMetrics.bottom


            // 如果在范围内才绘制
            if (x >= 0 && x <= width) 
                canvas.drawText(text, x, y, paint)
            

            tempX += eachWidth
        
    

完整代码

原创不易,您的点赞就是对我最大的帮助!

以上是关于Android自定义View基础篇的主要内容,如果未能解决你的问题,请参考以下文章

Android自定义View学习一---基础

Android自定义View基础篇

Android 手把手教您自定义ViewGroup

Android 自定义View 基础

Android自定义View

Android自定义View