WeiPeiYang阅读笔记(1)GPA2模块自定义View

第一次写这种东西emmm写错的地方求轻diss

GPA2 自定义View

GPALineChartView

整体布局:

看上去很ok!
注释里可大致知道paddingLeft,paddingRight,maxScroreExtended,minScoreExtended等参数的比例,作为接下来的参考。

各种参数:

companion object {
        // fixed constant
        const val LINE_STROKE = 16F
        const val POINT_RADIUS = 24F
        const val SELECTED_POINT_RADIUS = 28F
        const val SELECTED_POINT_STROKE_WIDTH = 20F
        const val SELECTED_POINT_STROKE_COLOR = Color.WHITE
        const val POPUP_BOX_COLOR = Color.WHITE
        const val POPUP_BOX_TRI_WIDTH = 80F
        const val POPUP_BOX_TRI_HEIGHT = 40F
        const val POPUP_BOX_RECT_WIDTH = 320F
        const val POPUP_BOX_RECT_HEIGHT = 320F
        const val POPUP_BOX_RECT_ROUND_RADIUS = 16F
        const val POPUP_BOX_MARGIN = 40F
        const val POPUP_BOX_PADDING = 40F
        const val DETAILS_TEXT_SIZE = 36F
        const val SHADOW_RADIUS = 16F
        const val SHADOW_COLOR = 0x66666666

        // default constant for attrs
        const val DEFAULT_LINE_COLOR = 0xFFEC826A.toInt()
        const val DEFAULT_FILL_COLOR = 0xFFF3AB9B.toInt()
        const val DEFAULT_POINT_COLOR = 0xFFEC826A.toInt()
    }
好处(个人理解)

1.将各种参数存为常量,在后面统一使用常量,如果要更改,只需改动一处,便于维护。

2.实现某些比例关系(如下)

Paint准备

设置各种各样的画笔,在需要的时候调用它们

    private val linePaint = Paint().apply {
        style = Paint.Style.STROKE
        strokeWidth = LINE_STROKE
        setShadowLayer(SHADOW_RADIUS, 0F, 0F, SHADOW_COLOR)
        isAntiAlias = true
    }

以linePaint为例,设置画笔的参数分别为:

style = Paint.Style.STROKE //style设置为勾边模式
strokeWidth = LINE_STROKE //线条的宽度设置为LINE_STROKE
setShadowLayer(SHADOW_RADIUS, 0F, 0F, SHADOW_COLOR) //设置阴影层
isAntiAlias = true //开启抗锯齿模式

顺便也学习到了kotlin的语法糖——apply函数
就比如上面的代码,用Paint().apply直接生成了一个参数为(此处省去n字)的Paint对象,比Java简洁。而且因为它再一个代码块里,所以更有紧凑感(词穷...)。阅读性很棒。

3个Color&&context.obtainStyledAttributes

    var lineColor
        get() = linePaint.color
        set(value) {
            linePaint.color = value
        }

    var fillColor
        get() = fillPaint.color
        set(value) {
            fillPaint.color = value
        }

    var pointColor
        get() = pointPaint.color
        set(value) {
            pointPaint.color = value
            selectedPointPaint.color = value
        }
context.obtainStyledAttributes(attrs, R.styleable.GpaLineChartView, defStyle, 0).apply {

            lineColor = getColor(R.styleable.GpaLineChartView_lineColor, DEFAULT_LINE_COLOR)

            fillColor = getColor(R.styleable.GpaLineChartView_fillColor, DEFAULT_FILL_COLOR)

            pointColor = getColor(R.styleable.GpaLineChartView_pointColor, DEFAULT_POINT_COLOR)

            recycle()
        }

这两端代码看的我一脸懵逼,看完网上的一些教程也有点摸不着头脑
模糊的理解是为这个自定义View设置属性值,而lineColor、fillColor、pointColor对应着R.styleable.GpaLineChartView中的三个Color。。。

但是我觉得自己这个解释对于apply好像并不行得通。。。于是对于这里我有一个问题:
context.obtainStyledAttributes是返回了一个TypedArray,但是TypedArray里面并没有lineColor、fillColor、pointColor这三个参数,也没有set方法,那代码块里面的东西有什么作用呢???求解答

接收的数据类和一些参数

 data class DataWithDetail(val data: Double, val detail: String)

数据类,包括一个Double和一个String。
//语法糖:data class:自动生成setter和getter方法的类,可用于接受网络请求GET到的数据,舒服。

    var dataWithDetail: List<DataWithDetail> = emptyList()
        set(value) {
            field = value
            selectedIndex = selectedIndex // ha?
            invalidate()
        }

    var selectedIndex = 0
        set(value) {
            field = value.coerceIn(if (dataWithDetail.isNotEmpty()) dataWithDetail.indices else 0..0)
            invalidate()
        }

两个参数,一个是返回数据的一个list,一个是选中的参数。
还有一个invalidate()函数,当检测到数据变化的时候,可以调用此函数来重绘自定义View,个人感觉有点像notifiedDataChanged()

//又是语法糖hhh,这回还不止一个
1)在参数下直接写set(value)={}或get()={}来为类中的属性值写setter和getter方法,
在set(value)中,可直接用field来代表属性值。
2)coerceIn:
@return this value if it's in the [range], or range.start if this value is less than range.start, or range.endInclusive if this value is greater than range.endInclusive.
这是源码的注解,嗯大概的意思是对于一个给定的范围,在范围内的会保持原来的值,比最大的大就返回最大值,比最小的小就返回最小值,可以有效的防止越界。

Path准备和计算

path的准备工作

 private val linePath = Path()
    private val fillPath = Path()
    private val pointPath = Path()
    private val selectedPointPath = Path()
    private val popupBoxPath = Path()

嗯,总共五个Path,分别是曲线的Path、填充颜色的Path、GPA分数点的Path、被选中点的Path和点击分数以后弹出的Box的Path,在后面的computePath函数中进行计算。


        val contentWidth = width - paddingLeft - paddingRight
        val contentHeight = height - paddingTop - paddingBottom
        val widthStep = contentWidth.toFloat() / (dataWithDetail.size + 1)

        val minData = dataWithDetail.minBy(DataWithDetail::data)?.data ?: 0.0
        val maxData = dataWithDetail.maxBy(DataWithDetail::data)?.data ?: 1.0
        val dataSpan = if (maxData != minData) maxData - minData else 1.0
        val minDataExtended = minData - dataSpan / 4F
        val maxDataExtended = maxData + dataSpan / 4F
        val dataSpanExtended = maxDataExtended - minDataExtended

        (0 until dataWithDetail.size).mapTo(pointsX.apply { clear() }) {
            paddingLeft + widthStep * (it + 1)
        }

        dataWithDetail.mapTo(pointsY.apply { clear() }) {
            paddingTop + ((1 - ((it.data - minDataExtended) / dataSpanExtended)) * contentHeight).toFloat()
        }

嗯这是一些参数,但是mapTo是干什么的有点没看懂...留着待解答

path的计算

linePath:
    linePath.apply {
            reset()
            var py = (paddingTop + contentHeight).toFloat()
            moveTo(0F, py)
            (0 until dataWithDetail.size).forEach {
                val cx = pointsX[it] - widthStep / 2F
                cubicTo(cx, py, cx, pointsY[it], pointsX[it], pointsY[it])
                py = pointsY[it]           }
            val cx = width - widthStep / 2F
            cubicTo(cx, py, cx, paddingTop.toFloat(), width.toFloat(), paddingTop.toFloat())
        }

使用moveTo函数将起点移到(0f,py)这个点(左边边界上的一个点),对于dataWithDetail,没有数据就调用一次cubicTo函数画出一条曲线;如果只有一个数据,以(0f,py)为起点,使用cubicTo函数传入计算后的控制点和终点绘画贝塞尔曲线;如果多个数据,第一个数据如上,其余每个数据以前一个数据绘制后的终点作为起点,再调用cubicTo函数进行绘制。所有数据绘制完毕后,再计算两个控制点和终点,绘制一个贝塞尔曲线收尾。

Kotlin语法糖:

(0 until dataWithDetail.size).foreach{
       // do something with "it"
}

//对范围(左开右闭)内的(Int类型数字)进行迭代。

fillPath
  fillPath.apply {
            reset()
            addPath(linePath)
            lineTo(width.toFloat(), height.toFloat())
            lineTo(0F, height.toFloat())
            close()
        }

使用addPath把计算好的linePath加进来,
用两个lineTo和close把linePath完善为一个以linePath为曲边的曲边梯形。

pointPath&&selectedPiontPath

pointPath:

 pointPath.apply {
            reset()
            if (dataWithDetail.isEmpty())
                return@apply 
            (0 until dataWithDetail.size)//
                    .filter { it != selectedIndex }
                    .forEach {
                        addCircle(
                                pointsX[it] - LINE_STROKE / 4F,
                                pointsY[it] - LINE_STROKE / 4F,
                                POINT_RADIUS,
                                Path.Direction.CCW
                        )
                    }
        }

如果dataWithDetail为空,证明没有成绩点,无需画点
对于所有的数据,用.filter进行筛选,选出没被选中的点,对这些点进行一个实现圆心效果的操作:

.forEach {
addCircle(//加一个比圆点小一号的圆
pointsX[it] - LINE_STROKE / 4F,
pointsY[it] - LINE_STROKE / 4F,
POINT_RADIUS,
Path.Direction.CCW
)
}

selectedPointPath:

  selectedPointPath.apply {
        reset()
        if (dataWithDetail.isEmpty())
            return@apply // no need to draw
        addCircle(
                pointsX[selectedIndex] - LINE_STROKE / 4F,
                pointsY[selectedIndex] - LINE_STROKE / 4F,
                SELECTED_POINT_RADIUS,
                Path.Direction.CCW
        )
    }

和ponitPath实现效果的操作基本一样,Circle的半径较大。

popupBoxPath
     popupBoxPath.apply {
            reset()

            if (dataWithDetail.isEmpty())
                return@apply // no need to draw

            val triCenter = pointsX[selectedIndex]
            val triTop = pointsY[selectedIndex] + SELECTED_POINT_RADIUS + POPUP_BOX_MARGIN

            moveTo(triCenter, triTop)
            lineTo(triCenter - POPUP_BOX_TRI_WIDTH / 2F, triTop + POPUP_BOX_TRI_HEIGHT)
            lineTo(triCenter + POPUP_BOX_TRI_WIDTH / 2F, triTop + POPUP_BOX_TRI_HEIGHT)
            close()


            val rectCenter =
                    when {
                        triCenter - POPUP_BOX_RECT_WIDTH / 2F < POPUP_BOX_MARGIN -> POPUP_BOX_MARGIN + POPUP_BOX_RECT_WIDTH / 2F
                        triCenter + POPUP_BOX_RECT_WIDTH / 2F > width - POPUP_BOX_MARGIN -> width - POPUP_BOX_MARGIN - POPUP_BOX_RECT_WIDTH / 2F
                        else -> triCenter
                    }

            val rectTop = triTop + POPUP_BOX_TRI_HEIGHT

            detailTextLayout = StaticLayout(
                    dataWithDetail[selectedIndex].detail,
                    detailsTextPaint,
                    (POPUP_BOX_RECT_WIDTH - POPUP_BOX_PADDING * 2).toInt(),
                    Layout.Alignment.ALIGN_NORMAL,
                    1.75F,
                    0F,
                    true
            ).also {
                detailTextLeft = rectCenter - it.width / 2F
                detailTextTop = rectTop + POPUP_BOX_PADDING
            }

            val rectHeight = detailTextLayout?.height?.toFloat() ?: POPUP_BOX_RECT_HEIGHT

            addRoundRect(
                    RectF(rectCenter - POPUP_BOX_RECT_WIDTH / 2F,
                            rectTop,
                            rectCenter + POPUP_BOX_RECT_WIDTH / 2F,
                            rectTop + rectHeight + POPUP_BOX_PADDING * 2F),
                    POPUP_BOX_RECT_ROUND_RADIUS,
                    POPUP_BOX_RECT_ROUND_RADIUS,
                    Path.Direction.CCW
            )
        }
    }

好长的一段代码啊!让我们把它来分成三部分:
1.数据判空:

    if (dataWithDetail.isEmpty())
                return@apply // no need to draw

如果没有数据就不需要进行绘制了

2.对话框的实现(自定义view部分):
1)对话框的小三角(等腰):

            val triCenter = pointsX[selectedIndex]
            val triTop = pointsY[selectedIndex] + SELECTED_POINT_RADIUS + POPUP_BOX_MARGIN
            moveTo(triCenter, triTop)
            lineTo(triCenter - POPUP_BOX_TRI_WIDTH / 2F, triTop + POPUP_BOX_TRI_HEIGHT)
            lineTo(triCenter + POPUP_BOX_TRI_WIDTH / 2F, triTop + POPUP_BOX_TRI_HEIGHT)
            close()

计算出了这个三角的顶点、高度和底边宽度,用lineto画出两条线再调用close()封闭成一个三角形。
2)圆角矩形的绘制:

    addRoundRect(
                RectF(rectCenter - POPUP_BOX_RECT_WIDTH / 2F,
                        rectTop,
                        rectCenter + POPUP_BOX_RECT_WIDTH / 2F,
                        rectTop + rectHeight + POPUP_BOX_PADDING * 2F),
                POPUP_BOX_RECT_ROUND_RADIUS,
                POPUP_BOX_RECT_ROUND_RADIUS,
                Path.Direction.CCW
        )

emmmm,就是一个addRoundRect函数,里面传进去了计算好的参数。

3.对话框的实现(StaticLayout部分)
这个说实话有点出乎我的预料。我原以为会用Paint.drawText之类的方法但是并没有,这是我没有接触过的船新版本,挤需三分钟(扯淡)我就爱上了节款Layout

 detailTextLayout = StaticLayout(
                dataWithDetail[selectedIndex].detail,
                detailsTextPaint,
                (POPUP_BOX_RECT_WIDTH - POPUP_BOX_PADDING * 2).toInt(),
                Layout.Alignment.ALIGN_NORMAL,
                1.75F,
                0F,
                true
        ).also {
            detailTextLeft = rectCenter - it.width / 2F
            detailTextTop = rectTop + POPUP_BOX_PADDING
        }

具体的原因呢emmmm,查了一下是因为drawText不能自动换行,所以要用StaticLayout来实现(涨姿势)

//语法糖:when

         val rectCenter =
                 when {
                        triCenter - POPUP_BOX_RECT_WIDTH / 2F < POPUP_BOX_MARGIN -> POPUP_BOX_MARGIN + POPUP_BOX_RECT_WIDTH / 2F
                        triCenter + POPUP_BOX_RECT_WIDTH / 2F > width - POPUP_BOX_MARGIN -> width - POPUP_BOX_MARGIN - POPUP_BOX_RECT_WIDTH / 2F
                        else -> triCenter
                    }

这个语法糖真的是让我爱不释手,配合lambda表达式,去除了冗杂的else if语句,代码简洁而不失可读性,简直爽的不行。

绘制

 override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        computePath()
        canvas.apply {
            // disable hardware acceleration for a perfect display of shadows
            if (isHardwareAccelerated) setLayerType(View.LAYER_TYPE_SOFTWARE, null)
            drawPath(linePath, linePaint)
            drawPath(fillPath, fillPaint)
            drawPath(pointPath, pointPaint)
            drawPath(selectedPointPath, selectedPointStrokePaint)
            drawPath(selectedPointPath, selectedPointPaint)
            drawPath(popupBoxPath, popupBoxPaint)
            save()
            translate(detailTextLeft, detailTextTop)
            detailTextLayout?.draw(canvas)
            restore()
        }
    }

重写OnDraw方法,调用cumputePath()函数,然后用canvas.apply进行如下操作——

if (isHardwareAccelerated) setLayerType(View.LAYER_TYPE_SOFTWARE, null)//关闭硬件加速

嗯翻译一遍注释hhh:为了完美的阴影效果,我们要添加这行代码来关闭硬件加速

drawPath(linePath, linePaint)
drawPath(fillPath, fillPaint)
drawPath(pointPath, pointPaint)
drawPath(selectedPointPath, selectedPointStrokePaint)
drawPath(selectedPointPath, selectedPointPaint)
drawPath(popupBoxPath, popupBoxPaint)

咳咳,最重要的时刻了!!!养兵千日用兵一时,用我们备好的point和path一一对应然后进行绘制!

save()
translate(detailTextLeft, detailTextTop)
detailTextLayout?.draw(canvas)
restore()

完善对话框。

结束语

自定义View可以使我们突破Android自带控件的限制,更精确的还原设计人员所提需求。可以说自定义VIew绘制最重要的是以下三点:
1.对于不同的需求设置不同参数的Paint(画笔)
可以用Paint().aplly{
//参数设置
}生成

2.对于一些不规则的形状,要事先计算Path.
可以使用Path().apply{
//计算参数
//设置Path
}

3.重写onDraw方法,用drawXXX()方法来绘制自定义View

下期是GPA2模块中对网络请求的封装处理

喜欢就点个赞哦~23333

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

推荐阅读更多精彩内容

  • Android中使用图形处理引擎,2D部分是android SDK内部自己提供,3D部分是用Open GL ES ...
    温暖的外星阅读 3,155评论 2 10
  • 版权声明:本文为博主原创文章,未经博主允许不得转载 前言 Canvas 本意是画布的意思,然而将它理解为绘制工具一...
    cc荣宣阅读 41,525评论 1 47
  • 【Android 自定义View之绘图】 基础图形的绘制 一、Paint与Canvas 绘图需要两个工具,笔和纸。...
    Rtia阅读 11,643评论 5 34
  • 18- UIBezierPath官方API中文翻译(待校对) ----------------- 华丽的分割线 -...
    醉卧栏杆听雨声阅读 1,048评论 1 1
  • 今天下班回来晚了点没接上儿子。回到家他已经站在门口等我了,我很抱歉地说了声:对不起妈妈回来晚了,儿子没有像以前那样...
    心似大海阅读 156评论 0 1