0%


又是更新blog的一天,最近项目中遇到一个需求,绘制一个渐变色的仪表盘去展示某个行情当前的热度,为用户提供当前市场热度更直观的一个大概参考,需要用到自定义View的相关知识,在这里做一个总结,欢迎一起学习和讨论~


最终效果图:
效果图

变量声明

  • 声明画笔,颜色,刻度数,属性动画,进度条宽度,表盘矩形区域,文本大小,必要的偏移量,表盘的开始和过渡角度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    var progressColor = intArrayOf() // 渐变色开始颜色
    var progressBackgroundColor = R.color.emphasis16 // 进度条背景颜色
    var textColor = R.color.primary // 文字颜色
    var tickScaleColor = R.color.emphasis8 // 普通刻度线颜色
    var groupScaleColor = R.color.emphasis38 // 分组刻度线颜色
    var progressStrokeWidth = 24f // 进度条宽度
    var paintProgressBackground = Paint() // 进度条背景画笔
    private var paintProgress = Paint() // 进度条画笔
    var paintText = Paint() // 文字画笔
    private var paintNum = Paint() // 刻度画笔
    var rect = RectF() // 表盘矩形区域
    private var viewWidth = 0 // 宽度
    private var viewHeight = 0 // 高度
    var percent = 0f // 百分比
    var oldPercent = 0f // 过去的百分比
    var textSize = 100f // 文本大小
    private var valueAnimator: ValueAnimator? = null // 属性动画
    var animatorDuration = 0L // 动画时长
    private var groupNum = 5 // 分组数
    private var ticksNum = 6 // 每组刻度数
    var pointerWidth = 15f // 指针的宽度
    private var ticksCount = groupNum * ticksNum + 1// 总刻度数

    companion object {
    var OFFSET = 30f // 偏移量
    var START_ARC = 150f // 开始角度
    var DURING_ARC = 240f // 过渡调度
    }

初始化逻辑

  • 获取xml的配置进行变量初始化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    init {
    setLayerType(LAYER_TYPE_SOFTWARE, null)
    val typedArray = context.obtainStyledAttributes(attrs, R.styleable.dashboard)
    progressBackgroundColor = typedArray.getColor(
    R.styleable.dashboard_progressBackgroundColor,
    ContextCompat.getColor(context, progressBackgroundColor)
    )
    progressStrokeWidth = typedArray.getDimension(
    R.styleable.dashboard_progressStrokeWidth,
    progressStrokeWidth
    )

    textSize = typedArray.getDimension(
    R.styleable.dashboard_textSize,
    textSize
    )
    textColor = typedArray.getColor(R.styleable.dashboard_textColor, textColor)
    tickScaleColor =
    typedArray.getColor(R.styleable.dashboard_tickScaleColor, tickScaleColor)
    groupScaleColor =
    typedArray.getColor(R.styleable.dashboard_groupScaleColor, groupScaleColor)
    groupNum = typedArray.getInt(R.styleable.dashboard_groupNum, groupNum)
    ticksNum = typedArray.getInt(R.styleable.dashboard_ticksNum, ticksNum)
    pointerWidth =
    typedArray.getDimension(R.styleable.dashboard_pointerWidth, pointerWidth)
    val colorsId = typedArray.getResourceId(R.styleable.dashboard_progressColors, 0)
    progressColor =typedArray.resources.getIntArray(colorsId)
    ticksCount = groupNum * ticksNum + 1
    typedArray.recycle()
    OFFSET = progressStrokeWidth + 10f
    initPaint()
    }

    /**
    * 初始化画笔
    */
    private fun initPaint() {
    paintProgressBackground.isAntiAlias = true
    paintProgressBackground.strokeWidth = progressStrokeWidth
    paintProgressBackground.style = Paint.Style.STROKE
    paintProgressBackground.color = progressBackgroundColor
    paintProgressBackground.isDither = true
    paintProgress.isAntiAlias = true
    paintProgress.strokeWidth = progressStrokeWidth
    paintProgress.style = Paint.Style.STROKE
    paintProgress.isDither = true
    paintText.isAntiAlias = true
    paintText.color = textColor
    paintText.strokeWidth = 1F
    paintText.style = Paint.Style.FILL
    paintText.isDither = true
    paintNum.isAntiAlias = true
    paintNum.strokeWidth = 3f
    paintNum.style = Paint.Style.FILL
    paintNum.isDither = true
    }

    声明xml配置属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <declare-styleable name="dashboard">
    <attr name="progressStrokeWidth" format="dimension" />
    <attr name="progressBackgroundColor" format="color" />
    <attr name="textColor" format="color" />
    <attr name="tickScaleColor" format="color" />
    <attr name="groupScaleColor" format="color" />
    <attr name="groupNum" format="integer" />
    <attr name="ticksNum" format="integer" />
    <attr name="pointerWidth" format="dimension" />
    <attr name="textSize" format="dimension" />
    <attr name="progressColors" format="reference"/>
    </declare-styleable>

    获取自定义View宽高,初始化渐变进度条

    回顾一下View的生命周期

    1
    activity.onCreate -> onFinishInflate -> activity.onStart -> activity.onResume -> onAttachedToWindow -> onWindowVisibilityChanged -> onVisibilityChanged -> onMeasure -> onSizeChanged -> onLayout -> onDraw -> onWindowFocusChanged -> activity.onPause -> onWindowFocusChanged -> onWindowVisibilityChanged -> activity.onStop -> onVisibilityChanged -> activity.onDestroy -> onDetachedFromWindow
  • 重写onSizeChanged,此时自定义View已经完成测量可以拿到当前自定义View的宽高
  • 初始化指针宽度,当前变盘的矩形区域,以0,0点为原点,左边上角的坐标,x为-(View宽度 / 2)+ 偏移量 + paddingLeft,y为- (view高度 / 2) + 偏移量 + paddingTop,右下角的坐标,x为(View宽度 / 2)- 偏移量 - paddingRight,y为(view高度 / 2) - 偏移量 - paddingBottom,渐变色采用以原点为中心的90度渐变,初始化Shader的子类SweepGradient并设置一个以原点为中心的90度旋转操作后的矩阵,再设置这个Shader为画笔的Shader
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    viewWidth = width
    viewHeight = height
    pointerWidth = (viewWidth / 40).toFloat()
    initShader()
    }

    /**
    * 初始化渐变颜色
    */
    private fun initShader() {
    rect.set(
    (-viewWidth / 2) + OFFSET + paddingLeft, paddingTop - (viewHeight / 2) + OFFSET,
    (viewWidth / 2) - paddingRight - OFFSET,
    (viewWidth / 2) - paddingBottom - OFFSET
    )
    val shader = SweepGradient(
    0f,
    0f,
    progressColor,
    null
    )
    val rotate = 90f
    val gradientMatrix = Matrix()
    gradientMatrix.preRotate(rotate, 0f, 0f)
    shader.setLocalMatrix(gradientMatrix)
    paintProgress.shader = shader
    }

    测量View的宽高

  • 根据当前的模式和大小决定View大小,如果是明确指明宽高的大小就用当前声明的值,否则就设置一个固定值使得宽高一致防止自定义View形变
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    val realWidth = startMeasure(widthMeasureSpec)
    val realHeight = startMeasure(heightMeasureSpec)
    setMeasuredDimension(realWidth, realHeight)
    }

    fun startMeasure(msSpec: Int): Int {
    val mode = MeasureSpec.getMode(msSpec)
    val size = MeasureSpec.getSize(msSpec)
    return if (mode == MeasureSpec.EXACTLY) {
    size
    } else {
    Util.dp2px(200)
    }
    }

    重写绘制方法

  • 当前的仪表盘的绘制逻辑分为绘制表盘刻度,绘制进度条,绘制文本,绘制指针,canvas的原点为左上角,为了方便计算在绘制前需要把原点移动到canvas的中心,所以x方向平移(View宽度 / 2),y方向平移(View高度 / 2)
    1
    2
    3
    4
    5
    6
    7
    8
    override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    canvas?.translate((viewWidth / 2).toFloat(), (viewHeight / 2).toFloat())
    drawPanel(canvas)
    drawProgress(canvas, percent)
    drawText(canvas, percent)
    drawPointer(canvas, percent)
    }

    绘制表盘刻度

  • 以0,0点为圆心,绘制起始点逆时针旋转-120度,计算刻度的y坐标为 -(View高度 / 2) + 偏移量 + 进度条宽度,计算每个刻度的旋转角度为过渡角度 / (总刻度数 - 1),再以总刻度数进行循环绘制刻度线条,判断当前下标是否为新一组开始刻度的位置,区分普通刻度和分割刻度并设置不同的颜色
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /**
    * 绘制刻度
    * @param canvas Canvas?
    */
    fun drawPanel(canvas: Canvas?) {
    canvas?.save()
    canvas?.rotate(-(180 - START_ARC + 90), 0f, 0f)
    val numY = -viewHeight / 2 + OFFSET + progressStrokeWidth
    val angle = DURING_ARC / ((ticksCount - 1) * 1.0f)
    for (i in 0 until ticksCount) {
    canvas?.save()
    canvas?.rotate(angle * i, 0f, 0f)
    if (i == 0 || i % groupNum == 0) {
    paintNum.color = groupScaleColor
    } else {
    paintNum.color = tickScaleColor
    }
    canvas?.drawLine(0f, numY + 2, 0f, numY + (pointerWidth * 2) + 5, paintNum)
    canvas?.restore()
    }
    canvas?.restore()
    }

    绘制进度条

  • 先绘制背景进度条,绘制一个圆弧,绘制区域为表盘矩形区域,设置开始角度和过渡角度
  • 绘制进度条,判断当前的进度百分比如果大于1则为1,过滤掉其他异常情况,如果百分比大于0绘制一个渐变色圆弧,根据当前百分比和过渡角度计算当前需要绘制的渐变色过渡角度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /**
    * 绘制进度
    * @param canvas Canvas?
    * @param percent Float
    */
    private fun drawProgress(canvas: Canvas?, percent: Float) {
    canvas?.drawArc(rect, START_ARC, DURING_ARC, false, paintProgressBackground)
    var curPercent = percent
    if (curPercent > 1.0f) {
    curPercent = 1.0f
    }
    if (curPercent > 0.0f) {
    canvas?.drawArc(rect, START_ARC, percent * DURING_ARC, false, paintProgress)
    }
    }

    绘制文本

  • 根据当前的进度百分比设置文本的内容和颜色,根据当前的View宽度计算一个Y方向的偏移量,如果百分比小于0.2展示“Very Cold”文本,渐变色数组中取第一个颜色,绘制文本的x坐标为文本宽度/2,第一个词y坐标为1.4倍偏移量,第二个词为2.4倍偏移量,如果百分比大于等于0.2并且小于0.4展示“Cold”文本,y坐标为2.2倍偏移量,如果百分比大于等于0.4并且小于0.6展示“Normal”文本,y坐标为2.2倍偏移量,如果百分比大于等于0.6并且小于0.8展示“Hot”文本,y坐标为2.2倍偏移量,如果是剩下的情况展示“Very Hot”文本,第一个词y坐标为1.4倍偏移量,第二个词为2.4倍偏移量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    /**
    * 绘制文本
    * @param canvas Canvas?
    * @param percent Float
    */
    @SuppressLint("RestrictedApi")
    fun drawText(canvas: Canvas?, percent: Float) {
    val offsetY = viewWidth / 8
    paintText.textSize = textSize
    if (percent < 0.2f) {
    val subTitle1 = "Very"
    paintText.color = progressColor[0]
    canvas?.drawText(
    subTitle1,
    -paintText.measureText(subTitle1) / 2,
    offsetY * 1.4f,
    paintText
    )

    val subTitle2 = "Cold"
    canvas?.drawText(
    subTitle2,
    -paintText.measureText(subTitle2) / 2,
    offsetY * 2.4f,
    paintText
    )
    } else if (percent >= 0.2f && percent < 0.4f) {
    val subTitle = "Cold"
    paintText.color = progressColor[0]
    canvas?.drawText(
    subTitle,
    -paintText.measureText(subTitle) / 2,
    offsetY * 2.2f,
    paintText
    )
    } else if (percent >= 0.4f && percent < 0.6f) {
    val subTitle = "Normal"
    paintText.color = progressColor[2]
    canvas?.drawText(
    subTitle,
    -paintText.measureText(subTitle) / 2,
    offsetY * 2.2f,
    paintText
    )
    } else if (percent >= 0.6f && percent < 0.8f) {
    val subTitle = "Hot"
    paintText.color = progressColor[4]
    canvas?.drawText(
    subTitle,
    -paintText.measureText(subTitle) / 2,
    offsetY * 2.2f,
    paintText
    )
    } else {
    val subTitle1 = "Very"
    paintText.color = progressColor[4]
    canvas?.drawText(
    subTitle1,
    -paintText.measureText(subTitle1) / 2,
    offsetY * 1.4f,
    paintText
    )

    val subTitle2 = "Hot"
    canvas?.drawText(
    subTitle2,
    -paintText.measureText(subTitle2) / 2,
    offsetY * 2.4f,
    paintText
    )
    }

    paintText.textSize = (textSize * 1.3).toFloat()
    val text = (percent * 100).toInt().toString()
    canvas?.drawText(
    text,
    -paintText.measureText(text) / 2,
    - textSize / 5f,
    paintText
    )
    }

    绘制指针

  • 绘制一个三角形的指针,根据当前的百分比计算指针所在位置的旋转角度,再计算指针所在位置需要绘制三角形的路径,从(0, view的高度/2 - 偏移量 - 进度条宽度)为起点,绘制一个倒三角形,绘制右边的线段和左边的线段再绘制顶部的线段,以垂直向下为0度,旋转范围是-300到-60并设置填充模式绘制这条路径
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    /**
    * 绘制指针
    * @param canvas Canvas?
    * @param percent Float
    */
    private fun drawPointer(canvas: Canvas?, percent: Float) {
    canvas?.save()
    val angle = DURING_ARC * (percent - 0.5f) - 180
    canvas?.rotate(angle, 0f, 0f)
    val pointer = Path()
    pointer.moveTo(0f, viewHeight / 2 - OFFSET - progressStrokeWidth)
    pointer.lineTo(
    pointerWidth,
    viewHeight / 2 - OFFSET - progressStrokeWidth - (pointerWidth * 2)
    )
    pointer.lineTo(
    -pointerWidth,
    viewHeight / 2 - OFFSET - progressStrokeWidth - (pointerWidth * 2)
    )
    pointer.lineTo(0f, viewHeight / 2 - OFFSET - progressStrokeWidth)
    pointer.close()
    pointer.fillType = Path.FillType.EVEN_ODD
    canvas?.drawPath(pointer, paintText)
    canvas?.restore()
    }

设置百分比和动画

  • 传入当前的百分比范围0~1,初始化属性动画设置属性值变化监听刷新当前百分比,根据上一次的百分比和当前百分比计算动画时间,设置动画开始结束的百分比数值,设置动画结束监听刷新上一次百分比数值并做边界判断。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    /**
    * 设置当前百分比
    * @param curPercent Float 范围0~1
    */
    fun setProgress(curPercent: Float) {
    if (valueAnimator?.isRunning == true) {
    valueAnimator?.cancel()
    }
    animatorDuration = (abs(curPercent - oldPercent) * 20).toLong()
    valueAnimator = ValueAnimator.ofFloat(oldPercent, curPercent).setDuration(animatorDuration)
    valueAnimator?.addUpdateListener {
    percent = it.animatedValue as Float
    invalidate()
    }
    valueAnimator?.interpolator = LinearInterpolator()
    valueAnimator?.addListener(onEnd = {
    oldPercent = curPercent
    if (percent < 0.0f) {
    percent = 0.0f
    invalidate()
    }
    if (percent > 1f) {
    percent = 1f
    invalidate()
    }
    })
    valueAnimator?.start()
    }

    使用

  • xml声明
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <com.example.testapp.DashboardView
    android:id="@+id/view_dashboard"
    android:layout_width="200dp"
    android:layout_height="200dp"
    app:layout_constraintTop_toTopOf="parent"
    android:layout_marginStart="6dp"
    android:layout_marginTop="8dp"
    app:groupNum="5"
    app:groupScaleColor="@color/emphasis38"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:pointerWidth="2dp"
    app:progressBackgroundColor="@color/emphasis8"
    app:progressStrokeWidth="6dp"
    app:progressColors="@array/dashboardColors"
    app:textSize="24sp"
    app:tickScaleColor="@color/emphasis8"
    app:ticksNum="6" />
  • 设置当前百分比
    1
    dashboardView.setProgress((float) i / 100)
    最后附上完整代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    package com.example.testapp

    import android.animation.ValueAnimator
    import android.annotation.SuppressLint
    import android.content.Context
    import android.graphics.*
    import android.util.AttributeSet
    import android.view.View
    import android.view.animation.LinearInterpolator
    import androidx.core.animation.addListener
    import androidx.core.content.ContextCompat
    import kotlin.math.abs

    @SuppressLint("CustomViewStyleable")
    class DashboardView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
    ) :
    View(context, attrs, defStyleAttr) {
    var progressColor = intArrayOf() // 渐变色开始颜色
    var progressBackgroundColor = R.color.emphasis16 // 进度条背景颜色
    var textColor = R.color.primary // 文字颜色
    var tickScaleColor = R.color.emphasis8 // 普通刻度线颜色
    var groupScaleColor = R.color.emphasis38 // 分组刻度线颜色
    var progressStrokeWidth = 24f // 进度条宽度
    var paintProgressBackground = Paint() // 进度条背景画笔
    private var paintProgress = Paint() // 进度条画笔
    var paintText = Paint() // 文字画笔
    private var paintNum = Paint() // 刻度画笔
    var rect = RectF() // 表盘矩形区域
    private var viewWidth = 0 // 宽度
    private var viewHeight = 0 // 高度
    var percent = 0f // 百分比
    var oldPercent = 0f // 过去的百分比
    var textSize = 100f // 文本大小
    private var valueAnimator: ValueAnimator? = null // 属性动画
    var animatorDuration = 0L // 动画时长
    private var groupNum = 5 // 分组数
    private var ticksNum = 6 // 每组刻度数
    var pointerWidth = 15f // 指针的宽度
    private var ticksCount = groupNum * ticksNum + 1// 总刻度数

    companion object {
    var OFFSET = 30f // 偏移量
    var START_ARC = 150f // 开始角度
    var DURING_ARC = 240f // 过渡调度
    }

    init {
    setLayerType(LAYER_TYPE_SOFTWARE, null)
    val typedArray = context.obtainStyledAttributes(attrs, R.styleable.dashboard)
    progressBackgroundColor = typedArray.getColor(
    R.styleable.dashboard_progressBackgroundColor,
    ContextCompat.getColor(context, progressBackgroundColor)
    )
    progressStrokeWidth = typedArray.getDimension(
    R.styleable.dashboard_progressStrokeWidth,
    progressStrokeWidth
    )

    textSize = typedArray.getDimension(
    R.styleable.dashboard_textSize,
    textSize
    )
    textColor = typedArray.getColor(R.styleable.dashboard_textColor, textColor)
    tickScaleColor =
    typedArray.getColor(R.styleable.dashboard_tickScaleColor, tickScaleColor)
    groupScaleColor =
    typedArray.getColor(R.styleable.dashboard_groupScaleColor, groupScaleColor)
    groupNum = typedArray.getInt(R.styleable.dashboard_groupNum, groupNum)
    ticksNum = typedArray.getInt(R.styleable.dashboard_ticksNum, ticksNum)
    pointerWidth =
    typedArray.getDimension(R.styleable.dashboard_pointerWidth, pointerWidth)
    val colorsId = typedArray.getResourceId(R.styleable.dashboard_progressColors, 0)
    progressColor =typedArray.resources.getIntArray(colorsId)
    ticksCount = groupNum * ticksNum + 1
    typedArray.recycle()
    OFFSET = progressStrokeWidth + 10f
    initPaint()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    val realWidth = startMeasure(widthMeasureSpec)
    val realHeight = startMeasure(heightMeasureSpec)
    setMeasuredDimension(realWidth, realHeight)
    }

    fun startMeasure(msSpec: Int): Int {
    val mode = MeasureSpec.getMode(msSpec)
    val size = MeasureSpec.getSize(msSpec)
    return if (mode == MeasureSpec.EXACTLY) {
    size
    } else {
    Util.dp2px(200)
    }
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    viewWidth = width
    viewHeight = height
    pointerWidth = (viewWidth / 40).toFloat()
    initShader()
    }

    override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    canvas?.translate((viewWidth / 2).toFloat(), (viewHeight / 2).toFloat())
    drawPanel(canvas)
    drawProgress(canvas, percent)
    drawText(canvas, percent)
    drawPointer(canvas, percent)
    }

    /**
    * 绘制刻度
    * @param canvas Canvas?
    */
    fun drawPanel(canvas: Canvas?) {
    canvas?.save()
    canvas?.rotate(-(180 - START_ARC + 90), 0f, 0f)
    val numY = -viewHeight / 2 + OFFSET + progressStrokeWidth
    val angle = DURING_ARC / ((ticksCount - 1) * 1.0f)
    for (i in 0 until ticksCount) {
    canvas?.save()
    canvas?.rotate(angle * i, 0f, 0f)
    if (i == 0 || i % groupNum == 0) {
    paintNum.color = groupScaleColor
    } else {
    paintNum.color = tickScaleColor
    }
    canvas?.drawLine(0f, numY + 2, 0f, numY + (pointerWidth * 2) + 5, paintNum)
    canvas?.restore()
    }
    canvas?.restore()
    }

    /**
    * 绘制进度
    * @param canvas Canvas?
    * @param percent Float
    */
    private fun drawProgress(canvas: Canvas?, percent: Float) {
    canvas?.drawArc(rect, START_ARC, DURING_ARC, false, paintProgressBackground)
    var curPercent = percent
    if (curPercent > 1.0f) {
    curPercent = 1.0f
    }
    if (curPercent > 0.0f) {
    canvas?.drawArc(rect, START_ARC, percent * DURING_ARC, false, paintProgress)
    }
    }

    /**
    * 绘制指针
    * @param canvas Canvas?
    * @param percent Float
    */
    private fun drawPointer(canvas: Canvas?, percent: Float) {
    canvas?.save()
    val angle = DURING_ARC * (percent - 0.5f) - 180
    canvas?.rotate(angle, 0f, 0f)
    val pointer = Path()
    pointer.moveTo(0f, viewHeight / 2 - OFFSET - progressStrokeWidth)
    pointer.lineTo(
    pointerWidth,
    viewHeight / 2 - OFFSET - progressStrokeWidth - (pointerWidth * 2)
    )
    pointer.lineTo(
    -pointerWidth,
    viewHeight / 2 - OFFSET - progressStrokeWidth - (pointerWidth * 2)
    )
    pointer.lineTo(0f, viewHeight / 2 - OFFSET - progressStrokeWidth)
    pointer.close()
    pointer.fillType = Path.FillType.EVEN_ODD
    canvas?.drawPath(pointer, paintText)
    canvas?.restore()
    }

    /**
    * 绘制文本
    * @param canvas Canvas?
    * @param percent Float
    */
    @SuppressLint("RestrictedApi")
    fun drawText(canvas: Canvas?, percent: Float) {
    val offsetY = viewWidth / 8
    paintText.textSize = textSize
    if (percent < 0.2f) {
    val subTitle1 = "Very"
    paintText.color = progressColor[0]
    canvas?.drawText(
    subTitle1,
    -paintText.measureText(subTitle1) / 2,
    offsetY * 1.4f,
    paintText
    )

    val subTitle2 = "Cold"
    canvas?.drawText(
    subTitle2,
    -paintText.measureText(subTitle2) / 2,
    offsetY * 2.4f,
    paintText
    )
    } else if (percent >= 0.2f && percent < 0.4f) {
    val subTitle = "Cold"
    paintText.color = progressColor[0]
    canvas?.drawText(
    subTitle,
    -paintText.measureText(subTitle) / 2,
    offsetY * 2.2f,
    paintText
    )
    } else if (percent >= 0.4f && percent < 0.6f) {
    val subTitle = "Normal"
    paintText.color = progressColor[2]
    canvas?.drawText(
    subTitle,
    -paintText.measureText(subTitle) / 2,
    offsetY * 2.2f,
    paintText
    )
    } else if (percent >= 0.6f && percent < 0.8f) {
    val subTitle = "Hot"
    paintText.color = progressColor[4]
    canvas?.drawText(
    subTitle,
    -paintText.measureText(subTitle) / 2,
    offsetY * 2.2f,
    paintText
    )
    } else {
    val subTitle1 = "Very"
    paintText.color = progressColor[4]
    canvas?.drawText(
    subTitle1,
    -paintText.measureText(subTitle1) / 2,
    offsetY * 1.4f,
    paintText
    )

    val subTitle2 = "Hot"
    canvas?.drawText(
    subTitle2,
    -paintText.measureText(subTitle2) / 2,
    offsetY * 2.4f,
    paintText
    )
    }

    paintText.textSize = (textSize * 1.3).toFloat()
    val text = (percent * 100).toInt().toString()
    canvas?.drawText(
    text,
    -paintText.measureText(text) / 2,
    - textSize / 5f,
    paintText
    )
    }

    /**
    * 设置当前百分比
    * @param curPercent Float 范围0~1
    */
    fun setProgress(curPercent: Float) {
    if (valueAnimator?.isRunning == true) {
    valueAnimator?.cancel()
    }
    animatorDuration = (abs(curPercent - oldPercent) * 20).toLong()
    valueAnimator = ValueAnimator.ofFloat(oldPercent, curPercent).setDuration(animatorDuration)
    valueAnimator?.addUpdateListener {
    percent = it.animatedValue as Float
    invalidate()
    }
    valueAnimator?.interpolator = LinearInterpolator()
    valueAnimator?.addListener(onEnd = {
    oldPercent = curPercent
    if (percent < 0.0f) {
    percent = 0.0f
    invalidate()
    }
    if (percent > 1f) {
    percent = 1f
    invalidate()
    }
    })
    valueAnimator?.start()
    }

    /**
    * 设置进度条宽度
    * @param dp Int
    */
    fun setProgressStroke(dp: Int) {
    progressStrokeWidth = Util.dp2px(dp).toFloat()
    paintProgress.strokeWidth = progressStrokeWidth
    paintProgressBackground.strokeWidth = progressStrokeWidth
    invalidate()
    }



    /**
    * 初始化画笔
    */
    private fun initPaint() {
    paintProgressBackground.isAntiAlias = true
    paintProgressBackground.strokeWidth = progressStrokeWidth
    paintProgressBackground.style = Paint.Style.STROKE
    paintProgressBackground.color = progressBackgroundColor
    paintProgressBackground.isDither = true
    paintProgress.isAntiAlias = true
    paintProgress.strokeWidth = progressStrokeWidth
    paintProgress.style = Paint.Style.STROKE
    paintProgress.isDither = true
    paintText.isAntiAlias = true
    paintText.color = textColor
    paintText.strokeWidth = 1F
    paintText.style = Paint.Style.FILL
    paintText.isDither = true
    paintNum.isAntiAlias = true
    paintNum.strokeWidth = 3f
    paintNum.style = Paint.Style.FILL
    paintNum.isDither = true
    }

    /**
    * 初始化渐变颜色
    */
    private fun initShader() {
    rect.set(
    (-viewWidth / 2) + OFFSET + paddingLeft, paddingTop - (viewHeight / 2) + OFFSET,
    (viewWidth / 2) - paddingRight - OFFSET,
    (viewWidth / 2) - paddingBottom - OFFSET
    )
    val shader = SweepGradient(
    0f,
    0f,
    progressColor,
    null
    )
    val rotate = 90f
    val gradientMatrix = Matrix()
    gradientMatrix.preRotate(rotate, 0f, 0f)
    shader.setLocalMatrix(gradientMatrix)
    paintProgress.shader = shader
    }
    }


最近工作比较忙,项目压力大,又是很长一段时间没有更新blog了。今天虽然是放五一长假,但是由于疫情仍然没有结束,所以没有出游的计划,正好可以写点东西总结一下最近项目中遇到的自定义View相关的内容,实现一个总量一定,支持和反对人数对比的指示条来反映某个问题下大多数人的看法,欢迎交流和讨论~


最终效果图:
效果图

变量声明

  • 声明支持和反对的绘制画笔,数量以及颜色,定义指示条的宽度,中间间隔的宽度,以及三角分割的宽度,圆角大小,指示条坐标对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private var downPaint = Paint()
    private var upPaint = Paint()
    private var upCount = 80
    private var downCount = 0
    private var downColor = ContextCompat.getColor(context, R.color.secondary)
    private var upColor = ContextCompat.getColor(context, R.color.primary)
    private var lineWidth = Util.dp2px(6)
    private var attachWidth = Util.dp2px(2)
    private var spanWidth = Util.dp2px(9)
    private var roundSize = lineWidth - Util.dp2px(2)
    private val indicator by lazy { Indicator() }

    指示条坐标对象

  • 整个指示条包含8个坐标,支持段和反对段各4个坐标,所以封装为一个Indicator对象,把支持和反对的绘制路径封装成两个IndicatorPath对象,每个IndicatorPath包含4个坐标,每个坐标包含x,y的数值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    data class Indicator(
    var upPath: IndicatorPath = IndicatorPath(),
    var downPath: IndicatorPath = IndicatorPath()
    )

    data class IndicatorPath(
    var startTop: Pos = Pos(),
    var startBottom: Pos = Pos(),
    var endTop: Pos = Pos(),
    var endBottom: Pos = Pos()
    )

    data class Pos(var x: Float = 0f, var y: Float = 0f)

    初始化逻辑

  • 获取xml的配置进行变量初始化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    init {
    setLayerType(View.LAYER_TYPE_SOFTWARE, null)
    val typedArray = context.obtainStyledAttributes(attrs, R.styleable.compareindicator)
    downColor = typedArray.getColor(R.styleable.compareindicator_down_color, downColor)
    upColor = typedArray.getColor(R.styleable.compareindicator_up_color, upColor)
    lineWidth = typedArray.getDimension(
    R.styleable.compareindicator_lineWidth,
    lineWidth.toFloat()
    ).toInt()
    attachWidth = typedArray.getDimension(
    R.styleable.compareindicator_attachWidth,
    lineWidth.toFloat()
    ).toInt()
    spanWidth = typedArray.getDimension(
    R.styleable.compareindicator_spanWidth,
    lineWidth.toFloat()
    ).toInt()
    textSize = typedArray.getDimension(
    R.styleable.dashboard_textSize,
    textSize
    )
    typedArray.recycle()
    upPaint = Paint()
    upPaint.color = upColor
    downPaint = Paint()
    downPaint.color = downColor
    }

    重写onDraw自定义绘制逻辑

  • 当支持和反对数量均不为0时,根据当前View宽度和支持数量百分比计算支持指示条的长度,根据当前View宽度和反对数量百分比计算反对指示条的长度,再计算支持和反对指示条的绘制路径。如果支持数不为0反对数为0则只绘制支持指示条,设置支持绘制画笔的圆角,直接绘制一根长度为View宽度的支持指示条,如果支持数为0反对数不为0则只绘制反对指示条,设置反对绘制画笔的圆角,直接绘制一根长度为View宽度的反对指示条。如果支持和反对数都为0,则支持和反对指示条长度各占View宽度的一半进行绘制
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    val upWidth: Int
    val downWidth: Int
    if (upCount != 0 && downCount != 0) {
    upWidth = width / (downCount + upCount) * upCount
    downWidth = width / (downCount + upCount) * downCount
    getPath(downWidth)
    drawIndicator(canvas)
    } else if (upCount != 0 && downCount == 0) {
    upWidth = width
    upPaint.strokeWidth = lineWidth.toFloat()
    upPaint.strokeCap = Paint.Cap.ROUND
    canvas?.drawLine(
    (paddingStart + roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    (upWidth - paddingEnd - roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    upPaint
    )
    } else if (upCount == 0 && downCount != 0) {
    downWidth = width
    downPaint.strokeWidth = lineWidth.toFloat()
    downPaint.strokeCap = Paint.Cap.ROUND
    canvas?.drawLine(
    (paddingStart + roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    (downWidth - paddingEnd - roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    downPaint
    )
    } else {
    upWidth = width / 2
    downWidth = width / 2
    getPath(downWidth)
    drawIndicator(canvas)
    }
    drawText(canvas)
    }

    计算绘制路径

  • 分别计算支持指示条和反对指示条的绘制路径,各自计算开始和结束的上下坐标,这里考虑到圆角,所以左边的反对指示条x开始坐标为View的paddingStart+圆角的大小,x结束坐标为View的paddingStart+圆角的大小+反对指示条的长度+三角形分割的宽度-间隔的宽度,y开始坐标为View的paddingTop,y结束坐标为View的paddingTop+指示条的宽度。右边的支持指示条x开始坐标为反对指示条的x坐标+间隔宽度,结束坐标为View的宽度-View的paddingEnd-圆角大小,y开始坐标为paddingTop,结束坐标为paddingTop+指示条的宽度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    private fun getPath(downWidth: Int) {
    val paddingSpanStart = paddingStart + roundSize
    // 反对指示条
    indicator.downPath.startTop.x = paddingSpanStart.toFloat()
    indicator.downPath.startTop.y = paddingTop.toFloat()
    indicator.downPath.startBottom.x = paddingSpanStart.toFloat()
    indicator.downPath.startBottom.y = (paddingTop + lineWidth).toFloat()
    indicator.downPath.endTop.x =
    (paddingSpanStart + downWidth + attachWidth - spanWidth).toFloat()
    indicator.downPath.endTop.y = paddingTop.toFloat()
    indicator.downPath.endBottom.x = (paddingSpanStart + downWidth - spanWidth).toFloat()
    indicator.downPath.endBottom.y = (paddingTop + lineWidth).toFloat()
    val paddingSpanEnd = paddingEnd + roundSize
    // 支持指示条
    indicator.upPath.startTop.x = indicator.downPath.endTop.x + spanWidth
    indicator.upPath.startTop.y = paddingTop.toFloat()
    indicator.upPath.startBottom.x = indicator.downPath.endBottom.x + spanWidth
    indicator.upPath.startBottom.y = (paddingTop + lineWidth).toFloat()
    indicator.upPath.endTop.x = (width - paddingSpanEnd).toFloat()
    indicator.upPath.endTop.y = paddingTop.toFloat()
    indicator.upPath.endBottom.x = (width - paddingSpanEnd).toFloat()
    indicator.upPath.endBottom.y = (paddingTop + lineWidth).toFloat()
    }

    绘制指示条逻辑

  • 设置支持和反对的绘制画笔为填充模式,绘制反对指示条的路径,从开始的上坐标到结束的上坐标,从结束的下坐标到开始的下坐标都是绘制直线,开始的下坐标到开始的上坐标是绘制圆滑的曲线即贝塞尔曲线,控制点为x的位置是开始的下坐标x-圆角大小,y是开始的下坐标y-指示线的宽度/2,结束点为开始的上坐标。同理,绘制支持指示条的路径,从结束的上坐标到开始的上坐标,从开始的下坐标到结束的下坐标都是绘制直线,结束的下坐标到结束的上坐标同样是绘制贝塞尔曲线,控制点为x的位置是结束的下坐标x-圆角大小,y是结束的下坐标y-指示线的宽度/2,结束点为结束的上坐标。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    private fun drawIndicator(canvas: Canvas?) {
    downPaint.strokeWidth = 1F
    upPaint.strokeWidth = 1F
    downPaint.strokeCap = Paint.Cap.BUTT
    upPaint.strokeCap = Paint.Cap.BUTT
    drawLine(canvas, indicator.downPath, downPaint, true)
    drawLine(canvas, indicator.upPath, upPaint, false)
    }

    private fun drawLine(
    canvas: Canvas?,
    indicatorPath: IndicatorPath,
    paint: Paint,
    down: Boolean
    ) {
    val path = Path()
    if (down) {
    path.moveTo(indicatorPath.startTop.x, indicatorPath.startTop.y)
    path.lineTo(indicatorPath.endTop.x, indicatorPath.endTop.y)
    path.lineTo(indicatorPath.endBottom.x, indicatorPath.endBottom.y)
    path.lineTo(indicatorPath.startBottom.x, indicatorPath.startBottom.y)
    path.quadTo(
    indicatorPath.startBottom.x - roundSize,
    indicatorPath.startBottom.y - (lineWidth / 2),
    indicatorPath.startTop.x,
    indicatorPath.startTop.y
    )
    } else {
    path.moveTo(indicatorPath.endTop.x, indicatorPath.endTop.y)
    path.lineTo(indicatorPath.startTop.x, indicatorPath.startTop.y)
    path.lineTo(indicatorPath.startBottom.x, indicatorPath.startBottom.y)
    path.lineTo(indicatorPath.endBottom.x, indicatorPath.endBottom.y)
    path.quadTo(
    indicatorPath.endBottom.x + roundSize,
    indicatorPath.endBottom.y - (lineWidth / 2),
    indicatorPath.endTop.x,
    indicatorPath.endTop.y
    )
    }
    path.close()
    paint.strokeWidth = 1f
    paint.style = Paint.Style.FILL
    path.fillType = Path.FillType.WINDING
    canvas?.drawPath(path, paint)
    }

    绘制文本逻辑

  • 左边是反对指示条在开始位置绘制Down的数量文本,x的坐标为反对指示条的开始x坐标,y坐标为反对指示条的开始下坐标y+间隔宽度+文字大小,右边是支持指示条在结束位置绘制Up的数量文本,x的坐标为支持指示条的结束下坐标的x-文本宽度,y坐标为支持指示条的结束下坐标y+间隔宽度+文字大小
    1
    2
    3
    4
    5
    6
    7
    8
    private fun drawText(canvas: Canvas?){
    val downText = "Down: $downCount"
    downPaint.textSize = textSize
    canvas?.drawText(downText, indicator.downPath.startBottom.x, indicator.downPath.startBottom.y + spanWidth + textSize, downPaint)
    val upText = "Up: $upCount"
    upPaint.textSize = textSize
    canvas?.drawText(upText, indicator.upPath.endBottom.x - upPaint.measureText(upText), indicator.upPath.endBottom.y + spanWidth + textSize, upPaint)
    }

    设置支持和反对数量

  • 根据设置的支持反对数量刷新视图
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    fun updateView(
    up: Int,
    down: Int,
    downColor: Int = ContextCompat.getColor(context, R.color.secondary),
    upColor: Int = ContextCompat.getColor(context, R.color.primary)
    ) {
    this.upCount = up
    this.downCount = down
    this.downColor = downColor
    downPaint.color = downColor
    this.upColor = upColor
    upPaint.color = upColor
    postInvalidate()
    }

    使用

  • xml中声明
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <com.example.testapp.CompareIndicator
    android:id="@+id/indicator_compare"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginStart="12dp"
    android:layout_marginTop="200dp"
    android:layout_marginEnd="12dp"
    app:lineWidth="6dp"
    app:textSize="12sp"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  • 设置支持反对数量
    1
    compareIndicator.updateView(80,20, ContextCompat.getColor(this, R.color.secondary), ContextCompat.getColor(this, R.color.primary))
    最后附上完整代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    package com.example.testapp

    import android.content.Context
    import android.graphics.Canvas
    import android.graphics.Paint
    import android.graphics.Path
    import android.util.AttributeSet
    import android.view.View
    import androidx.core.content.ContextCompat

    class CompareIndicator @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
    ) : View(context, attrs, defStyleAttr) {
    private var downPaint = Paint()
    private var upPaint = Paint()
    private var upCount = 80
    private var downCount = 0
    private var downColor = ContextCompat.getColor(context, R.color.secondary)
    private var upColor = ContextCompat.getColor(context, R.color.primary)
    private var lineWidth = Util.dp2px(6)
    private var attachWidth = Util.dp2px(2)
    private var spanWidth = Util.dp2px(9)
    private var labelSize = 40f
    private var roundSize = lineWidth - Util.dp2px(2)
    private val indicator by lazy { Indicator() }

    init {
    setLayerType(View.LAYER_TYPE_SOFTWARE, null)
    val typedArray = context.obtainStyledAttributes(attrs, R.styleable.compareindicator)
    downColor = typedArray.getColor(R.styleable.compareindicator_down_color, downColor)
    upColor = typedArray.getColor(R.styleable.compareindicator_up_color, upColor)
    lineWidth = typedArray.getDimension(
    R.styleable.compareindicator_lineWidth,
    lineWidth.toFloat()
    ).toInt()
    attachWidth = typedArray.getDimension(
    R.styleable.compareindicator_attachWidth,
    lineWidth.toFloat()
    ).toInt()
    spanWidth = typedArray.getDimension(
    R.styleable.compareindicator_spanWidth,
    lineWidth.toFloat()
    ).toInt()
    labelSize = typedArray.getDimension(
    R.styleable.dashboard_textSize,
    labelSize
    )
    typedArray.recycle()
    upPaint = Paint()
    upPaint.color = upColor
    downPaint = Paint()
    downPaint.color = downColor
    }

    fun updateView(
    up: Int,
    down: Int,
    downColor: Int = ContextCompat.getColor(context, R.color.secondary),
    upColor: Int = ContextCompat.getColor(context, R.color.primary)
    ) {
    this.upCount = up
    this.downCount = down
    this.downColor = downColor
    downPaint.color = downColor
    this.upColor = upColor
    upPaint.color = upColor
    postInvalidate()
    }

    override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    val upWidth: Int
    val downWidth: Int
    if (upCount != 0 && downCount != 0) {
    upWidth = width / (downCount + upCount) * upCount
    downWidth = width / (downCount + upCount) * downCount
    getPath(downWidth)
    drawIndicator(canvas)
    } else if (upCount != 0 && downCount == 0) {
    upWidth = width
    upPaint.strokeWidth = lineWidth.toFloat()
    upPaint.strokeCap = Paint.Cap.ROUND
    canvas?.drawLine(
    (paddingStart + roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    (upWidth - paddingEnd - roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    upPaint
    )
    } else if (upCount == 0 && downCount != 0) {
    downWidth = width
    downPaint.strokeWidth = lineWidth.toFloat()
    downPaint.strokeCap = Paint.Cap.ROUND
    canvas?.drawLine(
    (paddingStart + roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    (downWidth - paddingEnd - roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    downPaint
    )
    } else {
    upWidth = width / 2
    downWidth = width / 2
    getPath(downWidth)
    drawIndicator(canvas)
    }
    drawText(canvas)
    }

    private fun drawText(canvas: Canvas?){
    val downText = "Down: $downCount"
    downPaint.textSize = labelSize
    canvas?.drawText(downText, indicator.downPath.startBottom.x, indicator.downPath.startBottom.y + spanWidth + labelSize, downPaint)
    val upText = "Up: $upCount"
    upPaint.textSize = labelSize
    canvas?.drawText(upText, indicator.upPath.endBottom.x - upPaint.measureText(upText), indicator.upPath.endBottom.y + spanWidth + labelSize, upPaint)
    }

    private fun drawIndicator(canvas: Canvas?) {
    downPaint.strokeWidth = 1F
    upPaint.strokeWidth = 1F
    downPaint.strokeCap = Paint.Cap.BUTT
    upPaint.strokeCap = Paint.Cap.BUTT
    drawLine(canvas, indicator.downPath, downPaint, true)
    drawLine(canvas, indicator.upPath, upPaint, false)
    }

    private fun drawLine(
    canvas: Canvas?,
    indicatorPath: IndicatorPath,
    paint: Paint,
    down: Boolean
    ) {
    val path = Path()
    if (down) {
    path.moveTo(indicatorPath.startTop.x, indicatorPath.startTop.y)
    path.lineTo(indicatorPath.endTop.x, indicatorPath.endTop.y)
    path.lineTo(indicatorPath.endBottom.x, indicatorPath.endBottom.y)
    path.lineTo(indicatorPath.startBottom.x, indicatorPath.startBottom.y)
    path.quadTo(
    indicatorPath.startBottom.x - roundSize,
    indicatorPath.startBottom.y - (lineWidth / 2),
    indicatorPath.startTop.x,
    indicatorPath.startTop.y
    )
    } else {
    path.moveTo(indicatorPath.endTop.x, indicatorPath.endTop.y)
    path.lineTo(indicatorPath.startTop.x, indicatorPath.startTop.y)
    path.lineTo(indicatorPath.startBottom.x, indicatorPath.startBottom.y)
    path.lineTo(indicatorPath.endBottom.x, indicatorPath.endBottom.y)
    path.quadTo(
    indicatorPath.endBottom.x + roundSize,
    indicatorPath.endBottom.y - (lineWidth / 2),
    indicatorPath.endTop.x,
    indicatorPath.endTop.y
    )
    }
    path.close()
    paint.strokeWidth = 1f
    paint.style = Paint.Style.FILL
    path.fillType = Path.FillType.WINDING
    canvas?.drawPath(path, paint)
    }

    private fun getPath(downWidth: Int) {
    val paddingSpanStart = paddingStart + roundSize
    // 支持指示条
    indicator.downPath.startTop.x = paddingSpanStart.toFloat()
    indicator.downPath.startTop.y = paddingTop.toFloat()
    indicator.downPath.startBottom.x = paddingSpanStart.toFloat()
    indicator.downPath.startBottom.y = (paddingTop + lineWidth).toFloat()
    indicator.downPath.endTop.x =
    (paddingSpanStart + downWidth + attachWidth - spanWidth).toFloat()
    indicator.downPath.endTop.y = paddingTop.toFloat()
    indicator.downPath.endBottom.x = (paddingSpanStart + downWidth - spanWidth).toFloat()
    indicator.downPath.endBottom.y = (paddingTop + lineWidth).toFloat()
    val paddingSpanEnd = paddingEnd + roundSize
    // 反对指示条
    indicator.upPath.startTop.x = indicator.downPath.endTop.x + spanWidth
    indicator.upPath.startTop.y = paddingTop.toFloat()
    indicator.upPath.startBottom.x = indicator.downPath.endBottom.x + spanWidth
    indicator.upPath.startBottom.y = (paddingTop + lineWidth).toFloat()
    indicator.upPath.endTop.x = (width - paddingSpanEnd).toFloat()
    indicator.upPath.endTop.y = paddingTop.toFloat()
    indicator.upPath.endBottom.x = (width - paddingSpanEnd).toFloat()
    indicator.upPath.endBottom.y = (paddingTop + lineWidth).toFloat()
    }

    data class Indicator(
    var upPath: IndicatorPath = IndicatorPath(),
    var downPath: IndicatorPath = IndicatorPath()
    )

    data class IndicatorPath(
    var startTop: Pos = Pos(),
    var startBottom: Pos = Pos(),
    var endTop: Pos = Pos(),
    var endBottom: Pos = Pos()
    )

    data class Pos(var x: Float = 0f, var y: Float = 0f)
    }


裁剪是Android开发中一个常见的需求,虽然Android的ImageView的scaleType属性提供了多种图片的展示方式,其中包括一些裁剪方式,以及Glide自带的Transformation也提供了常见的圆角,圆形,居中裁剪,但有的时候仍然不能满足我们的需求,比如说为了版本迭代老版本apk能兼容新版本的图片资源,UI迭代的情况下产品会提出裁剪图片保留图片底部区域的规则,这时候ImageView和Glide自带的裁剪就不能满足需求了,需要我们自定义Transformation实现新的裁剪规则


裁剪类型

定义枚举:顶部裁剪,居中裁剪,底部裁剪

1
2
3
enum class CropType {
TOP, CENTER, BOTTOM
}

实线BitmapTransformation接口

默认居中裁剪,支持传入自定义的裁剪像素宽高,重写必要的方法,updateDiskCacheKey,transform,equals,hashCode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private val TAG: String = CropTransformation::class.java.name

class CropTransformation(var width: Int = 0, var height: Int = 0, var cropType: CropType = CropType.CENTER) : BitmapTransformation() {

override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(TAG.toByteArray(CHARSET))
}

override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
...
}

override fun equals(other: Any?): Boolean {
return other is CropTransformation && other.width == width && other.height == height && other.cropType == cropType
}

override fun hashCode(): Int {
return TAG.hashCode()
}
}

实现核心方法transform

  • transform的入参为,图片复用线程池pool,下载后要处理的图片toTransform,目标View的宽高

  • 当宽或高为0时默认使用图片的宽或高

  • 图片格式默认采用要处理图片的格式,否则使用ARGB_8888

  • 从当前图片缓存池中取得一张复用的图片进行处理,打开alpha通道

  • 计算裁剪的宽和图片的宽之比,以及高之比,取大的那个

  • 计算缩放后的宽和高

  • 计算绘制的开始位置,x和y坐标,x坐标为(当前裁剪的宽度 - 缩放后的宽度)/ 2,从水平居中区域开始绘制,y坐标根据当前的裁剪类型而定,TOP从0开始,CENTER为(当前裁剪的高度 - 缩放后的高度)/ 2,从垂直居中区域开始,BOTTOM直接裁剪底部区域,结束位置的x坐标为开始位置的x坐标 + 缩放后的宽度,y坐标为开始位置的y坐标 + 缩放后的高度,最后计算出绘制的矩形区域,设置从当前图片缓存池中取得的图片密度为要处理的图片toTransform的密度,利用复用的图片创建canvas,drawBitmap把要处理的图片toTransform绘制到canvas的目标矩形区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
width = if (width == 0) toTransform.width else width
height = if (height == 0) toTransform.height else height

val config = if (toTransform.config != null) toTransform.config else Bitmap.Config.ARGB_8888
val bitmap = pool[width, height, config]

bitmap.setHasAlpha(true)

val scaleX = width.toFloat() / toTransform.width
val scaleY = height.toFloat() / toTransform.height
val scale = max(scaleX, scaleY)

val scaledWidth = scale * toTransform.width
val scaledHeight = scale * toTransform.height
val left = (width - scaledWidth) / 2
val top: Float = getTop(scaledHeight)
val targetRect = RectF(left, top, left + scaledWidth, top + scaledHeight)

bitmap.density = toTransform.density
val canvas = Canvas(bitmap)
canvas.drawBitmap(toTransform, null, targetRect, null)

return bitmap
}

private fun getTop(scaledHeight: Float): Float {
return when (cropType) {
CropType.TOP -> 0f
CropType.CENTER -> (height - scaledHeight) / 2
CropType.BOTTOM -> height - scaledHeight
}
}

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.kubi.kucoin.utils

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.RectF
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import java.security.MessageDigest
import kotlin.math.max

/**
* @description: 图片裁剪
* @author: Jessie.Li
* @email: jessie.li@corp.kucoin.com
* @create: 2022-01-05 17:47
**/
private val TAG: String = CropTransformation::class.java.name

class CropTransformation(var width: Int = 0, var height: Int = 0, var cropType: CropType = CropType.CENTER) : BitmapTransformation() {
enum class CropType {
TOP, CENTER, BOTTOM
}

override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(TAG.toByteArray(CHARSET))
}

override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
width = if (width == 0) toTransform.width else width
height = if (height == 0) toTransform.height else height

val config = if (toTransform.config != null) toTransform.config else Bitmap.Config.ARGB_8888
val bitmap = pool[width, height, config]

bitmap.setHasAlpha(true)

val scaleX = width.toFloat() / toTransform.width
val scaleY = height.toFloat() / toTransform.height
val scale = max(scaleX, scaleY)

val scaledWidth = scale * toTransform.width
val scaledHeight = scale * toTransform.height
val left = (width - scaledWidth) / 2
val top: Float = getTop(scaledHeight)
val targetRect = RectF(left, top, left + scaledWidth, top + scaledHeight)

bitmap.density = toTransform.density
val canvas = Canvas(bitmap)
canvas.drawBitmap(toTransform, null, targetRect, null)

return bitmap
}

private fun getTop(scaledHeight: Float): Float {
return when (cropType) {
CropType.TOP -> 0f
CropType.CENTER -> (height - scaledHeight) / 2
CropType.BOTTOM -> height - scaledHeight
}
}

override fun equals(other: Any?): Boolean {
return other is CropTransformation && other.width == width && other.height == height && other.cropType == cropType
}

override fun hashCode(): Int {
return TAG.hashCode()
}
}


最近工作忙项目比较急,很少有时间更新blog了,不过好习惯还是应该保持,时间挤挤还是有的。项目中遇到一个需求,实现一个拉环动画,有一根拉伸可以往下拉动并在放手的时候回弹然后上下抖动,就跟我们生活中拖拽一根带了拉环的橡皮筋效果是一样的。最终我是用属性动画去实现这个需求,感觉不算复杂效果也还行,在此记录实现过程,欢迎相互交流~


最终效果图gif:
image

自定义View

实现AnimatorUpdateListener和Animator.AnimatorListener接口

1
2
3
4
5
6
7
8
9
class RingView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr), AnimatorUpdateListener, Animator.AnimatorListener{
override fun onAnimationUpdate(animation: ValueAnimator) {}
override fun onAnimationRepeat(animation: Animator) {}
override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationStart(animation: Animator) {}
override fun onAnimationEnd(animation: Animator) {}
}

初始化

初始化paint,animator,设置1秒后开始动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
init {
paint.color = resources.getColor(R.color.colorPrimary)
paint.isAntiAlias = true
postDelayed({ startAnimation() }, 1000)
}
private fun initAnimator(start: Int, end: Int) {
animator = ValueAnimator.ofInt(start, end)
animator?.duration = 1000
animator?.interpolator = accelerateInterpolator
animator?.addUpdateListener(this)
animator?.addListener(this)
}

private fun startAnimation() {
initAnimator(startY, endY)
isDown = true
animator?.start()
}

绘制

绘制拉环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private val bgRing = BitmapFactory.decodeResource(resources, R.mipmap.bg_ring)
// 拉环绘制尺寸
private val targetWidth = (bgRing.width * 1.4).toInt()
private val targetHeight = (bgRing.height * 1.4).toInt()
private val radius = targetWidth / 2
// 拉环坐标
private var xPos = 0
private var yPos = 0
private val srcRingRect = Rect(0, 0, bgRing.width, bgRing.height)
private val dstRingRect = Rect()

private fun drawRing(canvas: Canvas) {
dstRingRect.left = xPos
dstRingRect.top = yPos
dstRingRect.right = xPos + targetWidth
dstRingRect.bottom = yPos + targetHeight
canvas.drawBitmap(bgRing, srcRingRect, dstRingRect, paint)
}

绘制弹力绳

1
2
3
4
5
6
private val strokeWidth = 6f  // 绳子粗细
private var ratio = 0f // 弹性变化比例
private fun drawLine(canvas: Canvas) {
paint.strokeWidth = strokeWidth - 3 * ratio
canvas.drawLine((xPos + radius).toFloat(), 0f, (xPos + radius).toFloat(), (yPos + 2).toFloat(), paint)
}

重写onDraw

初始化上下轻微抖动的范围和最大拖拽长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 上下轻微抖动的范围
private var startY = 0
private var endY = 0
private var maxDragY = 0 // 最大拖拽长度
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (animator == null) {
startY = canvas.height / 2 - radius / 2
endY = canvas.height / 2
yPos = startY
maxDragY = endY + endY / 2
xPos = canvas.width / 2 - radius
}
drawLine(canvas)
drawRing(canvas)
}

下拉后自动回弹

利用加速插值器和减速插值器实现惯性向下运动,减速向上回弹,监听动画结束事件,更新运动方向标志位,下落时设置为加速插值器,回弹时设置为减速插值器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private var isDown = true
private var stopAnimate = false // 是否停止动画
private val accelerateInterpolator = AccelerateInterpolator()
private val decelerateInterpolator = DecelerateInterpolator()
override fun onAnimationEnd(animation: Animator) {
if (!stopAnimate) {
val vAnimation = animation as ValueAnimator
isDown = !isDown
if (isDown) {
vAnimation.setIntValues(startY, endY)
vAnimation.interpolator = accelerateInterpolator
} else {
vAnimation.setIntValues(endY, startY)
vAnimation.interpolator = decelerateInterpolator
}
vAnimation.start()
}
}

拖拽逻辑

重写onTouchEvent,监听ACTION_DOWN/ACTION_UP/ACTION_MOVE事件
手指落下时:判断当前触摸位置坐标是否在拉环矩形区域内,如果是则记录开始拖拽位置的Y坐标,停止当前动画,记录上一次手指移动的Y坐标
手指移动时:记录手指当前Y坐标,如果是拖拽状态且拖拽距离大于10,计算当前拖拽的插值,如果插值大于0则为向下拖拽,继续判断当前Y坐标是否小于最大拖拽Y坐标位置,如果是计算拉环当前运动的目标位置,计算当前拖拽位置到最大拖拽位置的百分比,修正拉环的Y坐标,实现拖拽时移动缓慢的阻尼效果,达到最大拖拽位置时百分比为1,更新拖拽状态为false,刷新视图,更新最后一次拖拽的Y坐标
手指抬起时:更新拖拽状态为false,更新最后一次拖拽的Y坐标为0,触发回弹动画,设置为减速插值器,刷新动画标志位,向下标志位为false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
isDrag = dstRingRect.plus(padding).contains(event.x.toInt(), event.y.toInt())
if (isDrag) {
lastDragY = event.y
if (animator != null && animator!!.isRunning) {
stopAnimate = true
animator?.cancel()
}
lastMoveY = (yPos + radius).toFloat()
}
}
MotionEvent.ACTION_UP -> {
// 弹回去
back()
lastMoveY = 0f
isDrag = false
}
MotionEvent.ACTION_MOVE -> {
curDragY = event.y
if (isDrag && curDragY - lastDragY > 10) {
val diffY = curDragY - lastMoveY
Log.i(TAG, "diffY: $diffY" )
if (diffY > 0){
if (curDragY < maxDragY){
val targetY = curDragY.toInt()
Log.i(TAG, "maxLength: ${maxDragY - lastDragY}" )
ratio = (targetY - lastDragY) / (maxDragY - lastDragY)
yPos = (yPos + diffY * (1 - ratio)).toInt()
Log.i(TAG, "add: ${diffY * (1 - ratio)}, diffY:${diffY} ratio:${(1 - ratio)}" )
} else {
ratio = 1f
isDrag = false
}

invalidate()
}
lastMoveY = curDragY
}
}
}
return true
}

private fun back() {
if (animator != null && !animator!!.isRunning) {
animator?.setIntValues(yPos, startY)
animator?.interpolator = decelerateInterpolator
stopAnimate = false
animator?.start()
isDown = false
}
}

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
class RingView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr), AnimatorUpdateListener, Animator.AnimatorListener {
private val bgRing = BitmapFactory.decodeResource(resources, R.mipmap.bg_ring)
// 拉环绘制尺寸
private val targetWidth = (bgRing.width * 1.4).toInt()
private val targetHeight = (bgRing.height * 1.4).toInt()
private val radius = targetWidth / 2
// 拉环坐标
private var xPos = 0
private var yPos = 0
private val strokeWidth = 6f // 绳子粗细
private val paint = Paint()
private var animator: ValueAnimator? = null
private var isDown = true

// 上下轻微抖动的范围
private var startY = 0
private var endY = 0
private var maxDragY = 0// 最大拖拽长度
private val accelerateInterpolator = AccelerateInterpolator()
private val decelerateInterpolator = DecelerateInterpolator()
private val srcRingRect = Rect(0, 0, bgRing.width, bgRing.height)
private val dstRingRect = Rect()
private var isDrag = false// 是否拖拽
private var curDragY = 0f // 当前拖拽Y坐标
private var lastDragY = 0f // 开始拖拽Y坐标
private var stopAnimate = false // 是否停止动画
private var lastMoveY = 0f // 上一次手指移动Y坐标
private var ratio = 0f // 弹性变化比例
private var padding = 20 // 触摸范围增量
init {
paint.color = resources.getColor(R.color.colorPrimary)
paint.isAntiAlias = true
postDelayed({ startAnimation() }, 1000)
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (animator == null) {
startY = canvas.height / 2 - radius / 2
endY = canvas.height / 2
yPos = startY
maxDragY = endY + endY / 2
xPos = canvas.width / 2 - radius
}
drawLine(canvas)
drawRing(canvas)
}

private fun drawRing(canvas: Canvas) {
dstRingRect.left = xPos
dstRingRect.top = yPos
dstRingRect.right = xPos + targetWidth
dstRingRect.bottom = yPos + targetHeight
canvas.drawBitmap(bgRing, srcRingRect, dstRingRect, paint)
}

override fun onAnimationUpdate(animation: ValueAnimator) {
yPos = animation.animatedValue as Int
ratio = (yPos - startY).toFloat() / (maxDragY - startY)
invalidate()
}

private fun drawLine(canvas: Canvas) {
paint.strokeWidth = strokeWidth - 3 * ratio
canvas.drawLine((xPos + radius).toFloat(), 0f, (xPos + radius).toFloat(), (yPos + 2).toFloat(), paint)
}

private fun initAnimator(start: Int, end: Int) {
animator = ValueAnimator.ofInt(start, end)
animator?.duration = 1000
animator?.interpolator = accelerateInterpolator
animator?.addUpdateListener(this)
animator?.addListener(this)
}

private fun startAnimation() {
initAnimator(startY, endY)
isDown = true
animator?.start()
}

override fun onAnimationRepeat(animation: Animator) {}
override fun onAnimationStart(animation: Animator) {}
override fun onAnimationEnd(animation: Animator) {
if (!stopAnimate) {
val vAnimation = animation as ValueAnimator
isDown = !isDown
if (isDown) {
vAnimation.setIntValues(startY, endY)
vAnimation.interpolator = accelerateInterpolator
} else {
vAnimation.setIntValues(endY, startY)
vAnimation.interpolator = decelerateInterpolator
}
vAnimation.start()
}
}

override fun onAnimationCancel(animation: Animator) {}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
isDrag = dstRingRect.plus(padding).contains(event.x.toInt(), event.y.toInt())
if (isDrag) {
lastDragY = event.y
if (animator != null && animator!!.isRunning) {
stopAnimate = true
animator?.cancel()
}
lastMoveY = (yPos + radius).toFloat()
}
}
MotionEvent.ACTION_UP -> {
// 弹回去
back()
lastMoveY = 0f
isDrag = false
}
MotionEvent.ACTION_MOVE -> {
curDragY = event.y
if (isDrag && curDragY - lastDragY > 10) {
val diffY = curDragY - lastMoveY
Log.i(TAG, "diffY: $diffY" )
if (diffY > 0){
if (curDragY < maxDragY){
val targetY = curDragY.toInt()
Log.i(TAG, "maxLength: ${maxDragY - lastDragY}" )
ratio = (targetY - lastDragY) / (maxDragY - lastDragY)
yPos = (yPos + diffY * (1 - ratio)).toInt()
Log.i(TAG, "add: ${diffY * (1 - ratio)}, diffY:${diffY} ratio:${(1 - ratio)}" )
} else {
ratio = 1f
isDrag = false
}

invalidate()
}
lastMoveY = curDragY
}
}
}
return true
}

private fun back() {
if (animator != null && !animator!!.isRunning) {
animator?.setIntValues(yPos, startY)
animator?.interpolator = decelerateInterpolator
stopAnimate = false
animator?.start()
isDown = false
}
}

}

xml中引入

1
2
3
4
<com.example.testapp.RingView
android:layout_width="match_parent"
android:layout_height="match_parent"
/>


PPTP协议在没有配置智能分流的情况下,上网需要来回切换略显麻烦,有没有什么办法可以和ssr客户端和V2ray客户端那样实现分流呢?解决这个问题以后又会发现一个新的问题PPTP用的是google的dns,解析到的ip地址未必是适合的ip导致某些网站访问速度很慢,如果一个账号能让任意设备共享网络,省去折腾各个终端安装和配置那就太好了,最近利用家里的树莓派实现了这个需求,下面是我的折腾记录,有更好的方法欢迎留言交流~


安装PPTP的linux客户端

  1. apt-get安装pptp-linux
    sudo apt-get install pptp-linux

  2. 修改pptp-linux的配置文件

    sudo vi /etc/ppp/peers/pptpconf

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    pty "你的服务端地址 --nolaunchpppd"
    name 账号
    password 密码
    remotename PPTP
    require-mppe-128
    require-mschap-v2
    refuse-eap
    refuse-pap
    refuse-chap
    refuse-mschap
    noauth
    persist
    maxfail 0
    defaultroute
    replacedefaultroute
    usepeerdns
  3. 启动/关闭PPTP
    sudo pon pptpconf
    开启后,如果连接正常,ifconfig可以看到PPTP的连接ppp0

  4. 设置开机启动服务 sudo vi /lib/systemd/system/pptp.service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    [Unit]
    Description=PPTP Service
    After=network.target

    [Service]
    Type=forking
    ExecStart=/usr/bin/pon pptpconf

    [Install]
    WantedBy=multi-user.target
  5. 刷新并启动服务

    1
    2
    3
    sudo systemctl daemon-reload
    sudo systemctl enable pptp
    sudo systemctl start pptp

    chnroutes分流策略

    根据请求ip分流,某些网站强制转发到ppp0使用PPTP访问,某些网站不使用,这里记录PPTP下的使用方法,其它协议的使用方法可以查看官网:https://github.com/fivesheep/chnroutes

1
2
3
git clone https://github.com/fivesheep/chnroutes.git
cd chnroutes
sudo python chnroutes.py -p linux; sudo chmod a+x ip-pre-up; sudo cp ip-pre-up /etc/ppp; sudo chmod a+x ip-down; sudo cp ip-down /etc/ppp/ip-down.d/

clash开启http/https/socks5代理

  1. 安装clash
  • 下载安装包

    sudo wget https://github.com/Dreamacro/clash/releases/download/v1.6.5/clash-linux-armv7-v1.6.5.gz

  • 解压

    sudo gunzip clash-linux-armv7-v1.6.5.gz

  • 移动到系统目录

    sudo mv clash-linux-armv7-v1.6.5 /usr/local/bin/clash

  • 设置可执行权限

    sudo chmod +x /usr/local/bin/clash

  1. 设置配置文件
    sudo vi ~/.config/clash/config.yaml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # port of HTTP
    port: 7890

    ## port of SOCKS5
    socks-port: 7891

    # `allow-lan` must be true in your config.yml
    allow-lan: true

    # set log level to stdout (default is info)
    # info / warning / error / debug / silent
    log-level: info

    # A RESTful API for clash
    #使用0.0.0.0可以使用局域网设备访问
    external-controller: 0.0.0.0:8080

    mode: Rule

    sudo clash启动,这样我们就拥有http/https/socks5的代理服务器了,任意设备只要配置树莓派的ip地址和对应协议的端口号即可代理请求,如果有公网ip或内网穿透,有域名+ddns解析服务器那么在外网也可以代理

  2. 设置开机启动服务

    sudo vi /etc/systemd/system/clash.service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [Unit]
    Description=Clash Service
    After=network.target

    [Service]
    Restart=on-abort
    LimitNOFILE=1048576
    ExecStart=/usr/local/bin/clash -d /home/pi/.config/clash

    [Install]
    WantedBy=multi-user.target
  3. 转发所有请求 sudo vi /etc/sysctl.conf

  • ipv4的请求,修改

    net.ipv4.ip_forward=1

  • ipv6的请求,修改

    net.ipv6.conf.all.forwarding = 1
    刷新设置

    sudo sysctl -p
    流量强制转发到ppp0
    sudo iptables -t nat -A POSTROUTING -o ppp0 -j MASQUERADE

    dnsmasq + dnsmasq-china-list 本地DNS分流策略

  1. apt-get安装dnsmasq

    sudo apt install dnsmasq

  2. 设置配置文件

    sudo vi /etc/dnsmasq.conf

    1
    2
    3
    no-resolv
    server=8.8.8.8
    server=8.8.4.4
  3. 用dnsmasq-china-list设置白名单,用运营商dns去解析,其它用google的dns解析,否则解析到的ip并不是访问速度快的最适合的ip,导致网站和App的访问速度太慢了,所以太干净的dns解析也不好

  • 运营商分配的DNS地址假设为223.5.5.5,切换到su用户,拉取不需要走PPTP的地址用运营商分配的DNS地址去解析
    1
    curl -s https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/master/accelerated-domains.china.conf|sed 's/114.114.114.114/223.5.5.5/g' >/etc/dnsmasq.d/accelerated-domains.china.223.5.5.5.conf
  1. 重启dnsmasq

    service dnsmasq restart

  2. 测试分流效果

  • 未使用本地dns解析
    dig google.com @223.5.5.5
  • 使用本地dns解析
    dig google.com @127.0.0.1
  1. 添加自定义域名到白名单
    echo 'server=/你需要的域名/223.5.5.5' >>/etc/dnsmasq.d/accelerated-domains.china.223.5.5.5.conf

    DDNS动态域名解析

    如果家里是公网IP,可以使用DDNS给域名绑定动态ip,no-ip是ddns解析服务,免费赠送域名无需备案,速度还不错,每个月需要点一次邮件续期,官网的ip更新方式在树莓派下更新失败,折腾一段时间后,发现用ddclient可以正常更新,但是注意如果当前状态是pptp开启的情况下,获取到的外网ip可能不是运营商的公网ip,在配置了智能路由的情况下需使用无需PPTP访问的网站获取外网ip
    1
    sudo apt-get install ddclient
    设置配置文件 /etc/ddclient.conf
    1
    2
    3
    4
    5
    6
    protocol=noip
    use=web, web=获取外网ip的网站
    server=dynupdate.no-ip.com
    login=用户名(邮箱)
    password='密码'
    你的域名


C语言是面向过程的,函数+结构体组成,C++是在C语言的基础上增加面向对象的能力,兼容C语言但是C语言不兼容C++,是Android中NDK开发的主要语言,对于学习NDK开发而言重要性是不言而喻的,接下来总结C++的学习,欢迎一起学习和交流~


打印日志

1
2
3
4
5
6
void print() {
// c的打印方式
printf("c++ 语言的学习!\n");
// c++的打印方式,endl == \n
cout << "c++ 语言的学习!" << endl;
}

交换两个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// c的交换方式
void numberChange(int *num1, int *num2){
int temp;
temp = *num1;
*num1 = *num2;
*num2 = temp;
}
// c++的交换方式
void numberChange2(int &number1,int &number2){
// 内存地址与外部一致
cout << "n1内存地址:" << &number1 << ", n2内存地址:" << &number2 <<endl;

int temp;
temp = number1;
number1 = number2;
number2 = temp;
}
int main(){
int num1 = 10;
int num2 = 20;
// numberChange(&num1, &num2);
// cout << "n1:" << num1 << ", n2:" << num2 << endl;
cout << "n1内存地址:" << &num1 << ", n2内存地址:" << &num2<<endl;
numberChange2(num1, num2);
cout << "n1:" << num1 << ", n2:" << num2 << endl;
return 0;
}

通过内存地址修改某个值

1
2
3
4
5
6
7
8
9
10
11
12
int n1 = 999;
int n2 = n1;
// n1 n2内存地址不同
cout << &n1 << "---" << &n2 << endl;

int n3 = 999;
int &n4 = n3;
// n3 n4内存地址相同
cout << &n3 << "---" << &n4 << endl;
n4 = 777;
// n3 n4都改为777
cout << n3 << "---" << n4 << endl;

定义结构体

1
2
3
4
5
6
7
8
typedef struct {
char name[20];
int age;
}Student;
int main(){
Student student = {"小明", 30};
return 0;
}

函数重载

1
2
3
4
5
6
7
8
9
10
11
12
int add(int number1,int number2 ){
return number1 + number2;
}

int add(int number1,int number2,int number3){
return number1 + number2 + number3;
}

// 支持默认形参,直接重载1-4个参数方法
//int add(int number1 = 1,int number2 = 2,int number3 = 3, int number4 = 4){
// return number1 + number2;
//}

类的定义

  • Teacher.h中声明
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    #include <iostream>
    using namespace std;
    class Person{
    private:
    char *name;
    int age;
    public:
    Person(char * name, int age):name(name){
    this -> age = age;
    cout << "Person 构造函数" << endl;
    }
    void print(){
    cout << this -> name << "," << this -> age << endl;
    }
    };

    // 默认私有继承,子类中可以访问父类的成员,类外不行
    // 公开继承,子类在类外也可以访问父类的成员 class Student:public Person
    class Student: Person{
    private:
    char * course;
    Student(char * name, int age, char * course): Person(name, age),course(course){
    cout << "Student 构造函数" << endl;
    }
    public:
    void test(){
    print();
    }
    // 构造函数顺序:Person,Student,析构函数顺序Student,Person
    ~Student(){
    cout << "Student 析构函数" << endl;
    }
    };
    class Teacher{
    private:
    char *name;
    int age;
    public:
    void setAge(int age);
    void setName(char *name);
    int getAge();
    char* getName();
    // 静态变量声明再实现
    static int id;
    // 友元函数可以访问/修改所有私有成员
    friend void updateAge(Teacher *teacher, int age);
    };
  • Teacher.cpp中定义
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    #include "Teacher.h"
    #include <string.h>
    using namespace std;

    public:
    int Teacher::id = 9
    // 不需要::
    void updateAge(Teacher *teacher, int age){
    teacher -> age = age;
    }
    void Teacher::setAge(int age){
    this->age = age;
    }
    void Teacher::setName(char *name){
    this -> name = name;
    }
    int Teacher::getAge(){
    return this -> age;
    }
    char* Teacher::getName(){
    return this -> name;
    }
    // 先调两个参数的构造函数,再调用一个参数的构造函数
    Teacher(char *name):Teacher(name, 87){
    this.name = name;
    }
    // 拷贝构造函数,被const修饰只能写在类中访问私有成员
    Teacher(const Teacher &teacher){
    this -> age = teacher.age;
    // 浅拷贝
    // this -> name = teacher.name;
    // 如果存在堆成员采用深拷贝
    this -> name = (char *)malloc(sizeof(char*) * 10);
    strcpy(this -> name, teacher.name);
    }
    Teacher(char *name, int age){
    // 堆区创建的name
    this -> name = (char *)malloc(sizeof(char*) * 10);
    strcpy(this -> name, name);
    this.age = age;
    }
    ~Teacher(){
    // 释放堆区创建的属性
    if (this -> name){
    free(this -> name);
    this -> name = NULL;
    }
    }


    int main(){
    // 栈空间分配内存
    Teacher teacher;
    teacher.setAge(99);
    teacher.setName("李华");
    cout << "name:" << teacher.getName() << ", age:" << teacher.getAge() << endl;

    // free 不会调析构函数,malloc不会调构造函数
    // 堆空间分配内存
    Teacher *teacher2 = new Teacher(); //堆区 手动释放
    teacher2 ->setAge(88);
    teacher2 ->setName("李华成");
    // 堆空间的内存需要手动释放
    if (teacher2){
    delete teacher2; // 析构函数一定执行
    teacher2 = NULL;
    }

    Teacher teacher1("张三",34); //栈区 弹栈自动释放
    // 不会调用拷贝构造函数
    // Teacher teacher2;
    // teacher2 = teacher1;
    Teacher teacher2 = teacher1; // 调拷贝构造函数
    Teacher *teacher1 = new Teacher("李四",35);
    Teacher *teacher2 = teacher1; // 不会调拷贝构造函数
    int number = 9;
    int number2 = 8;
    // 常量指针
    const int *numberP1 = &number;
    *numberP1 = 100; // 不允许修改常量指针存放地址对应的值
    numberP1 = &number2; // 允许修改常量指针存放的地址
    // 指针常量
    int* const numberP2 = &number;
    *numberP2 = 100; // 允许修改指针常量存放地址对应的值
    numberP2 = &number2; // 不允许修改指针常量存放的地址
    // 常量指针常量
    const int* const numberP3 = &number;
    *numberP3 = 100; // 不允许修改常量指针常量存放地址对应的值
    numberP3 = &number2; // 不允许修改常量指针常量存放的地址
    return 0;
    }

    自定义命名空间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    namespace MyNameSpace1{
    namespace MyNameSpace2{
    namespace MyNameSpace3{
    void out(){
    cout << "" << endl;
    }
    }
    }
    }
    int main(){
    // 第一种调用方式
    using namespace MyNameSpace1::MyNameSpace2::MyNameSpace3;
    out();
    // 第二种调用方式
    // MyNameSpace1::MyNameSpace2::MyNameSpace3::out();
    return 0;
    }

    可变参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include <stdarg.h> // 支持可变参数
    using namespace std;
    void sum(int count, ...){
    va_list vp; // 可变参数的动作
    // count 内部需要一个存储地址参考值,否则无法处理存放参数的信息,也用于循环遍历
    va_start(vp, count);
    int number = va_arg(vp, int);
    cout << number << endl;

    number = va_arg(vp, int);
    cout << number << endl;

    number = va_arg(vp, int);
    cout << number << endl;

    va_end(vp);
    }

    int main(){
    sum(54,6,7,8)
    return 0;
    }

    友元类

  • java反射的实现原理 native层利用友元类访问私有成员
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    # include<iostream>
    using namespace std;

    class ImageView{
    private int viewSize;
    friend class Class; // 声明Class为友元类
    };
    class Class{
    public ImageView imageView;
    void changeViewSize(int size){
    imageView.viewSize = size;
    }
    int getViewSize(){
    return imageView.viewSize
    }
    };

    int main(){
    Class imageViewClass;
    imageViewClass.changeViewSize(600);
    return 0;
    }

    运算符重载

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    class Position{
    private:
    int x, y;
    public:
    Position(int x, int y):x(x), y(y) {}
    void setX(int x){
    this -> x = x;
    }
    void setY(int y){
    this -> y = y;
    }
    int getX(){
    return this -> x;
    }
    int getY(){
    return this -> y;
    }

    // 如果没有&会创建副本,影响性能
    Position operator + (Position &position) {
    int x = this -> x + position.x; // 类里可以访问私有成员
    int y = this -> y + position.y;
    return Position(x, y);
    }

    void operator ++(){ //++对象
    this -> x = this -> x + 1;
    this -> y = this -> y + 1;
    }

    void operator ++ (int) { //对象++
    this -> x = this -> x + 1;
    this -> y = this -> y + 1;
    }

    friend void operator << (ostream &_START, Position position){
    _START << position.x << "," << position.y << endl;
    }

    friend ostream & operator >> (ostream &_START, Position position){
    _START << position.x << "," << position.y << endl;
    return _START; // 可多次打印
    }

    friend istream & operator >> (istream &_START, Position position){
    _START >> position.x;
    _START >> position.y;
    return _START; // 可多次打印
    }
    };

    class ArrayClass {
    private:
    int size = 0;
    int *arrayValue;
    public:
    void set(int index, int value){
    arrayValue[index] = value;
    size += 1;
    }
    int getSize(){
    return this -> size;
    }
    int operator[](int index){
    return this -> arrayValue[index];
    }
    void printfArrayClass(ArrayClass arrayClass){
    for (int i = 0; i < arrayClass.getSize(); ++i){
    cout << arrayClass[i] << endl;
    }
    }
    };


    int main(){
    Position position1(100, 200);
    Position position2(200, 300);
    Position res = position1 + position2;
    cout << res.getX() << "," << res.getY() << endl;
    Position pos(1, 2);
    pos ++;
    ++ pos;
    cout << pos.getX() << "," << pos.getY() << endl;
    cout >> position2 >> position2 >> position2;
    Position position;
    cin >> position;
    cout << position.getX() << "," << position.getY() << endl;

    ArrayClass arrayClass;
    arrayClass.set(0, 1000);
    arrayClass.set(1, 2000);
    arrayClass.set(2, 3000);
    printfArrayClass(arrayClass);
    return 0;
    }

    多继承

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    #include <iostream>
    using namespace std;
    class BaseActivity1{
    public:
    void onCreate(){
    cout << "BaseActivity1 onCreate" << endl;
    }
    void onStart(){
    cout << "BaseActivity1 onStart" << endl;
    }
    };
    class BaseActivity2{
    public:
    void onCreate(){
    cout << "BaseActivity2 onCreate" << endl;
    }
    void onStart(){
    cout << "BaseActivity2 onStart" << endl;
    }
    };
    class BaseActivity3{
    public:
    void onCreate(){
    cout << "BaseActivity3 onCreate" << endl;
    }
    void onStart(){
    cout << "BaseActivity3 onStart" << endl;
    }
    };
    class MainActivity1: public BaseActivity1,BaseActivity2,BaseActivity3{
    void onCreate(){
    cout << "MainActivity1 onCreate" << endl;
    }

    };
    int main(){
    MainActivity1 mainActivity1;
    mainActivity1.onCreate();
    // mainActivity1.onStart(); 有歧义,不知道调哪个父类的onStart,子类重写onStart就没问题
    mainActivity1.BaseActivity1::onStart();
    mainActivity1.BaseActivity2::onStart();
    mainActivity1.BaseActivity3::onStart();

    return 0;
    }

    class Object{
    public:
    int number;
    };

    // 虚继承解决歧义问题
    class BaseActivity1:virtual public Object{

    };

    // 虚继承解决歧义问题,原理是把多个变量化成一份
    class BaseActivity2:virtual public Object{

    };

    class Son :public BaseActivity1,public BaseActivity2{
    public:
    int number; // 覆盖父类number
    }
    int main(){
    Son son;
    son.BaseActivity1::number = 1;
    son.BaseActivity2::number = 1;
    son.number = 1;
    return 0;
    }

    多态

  • 重写和重载
  • 同一个方法有不同的实现,父类指向子类
  • 动态多态:程序在运行期间才确定调用哪个类的函数
  • C++默认关闭多态,在父类上给方法增加virtual关键字开启多态
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    #include <iostream>
    using namespace std;
    class BaseActivity{
    public:
    virtual void onStart(){
    cout << "BaseActivity onStart" << endl;
    }
    };
    class HomeActivity: public BaseActivity{
    public:
    void onStart(){
    cout << "HomeActivity onStart" << endl;
    }
    };
    class LoginActivity: public BaseActivity{
    public:
    void onStart(){
    cout << "LoginActivity onStart" << endl;
    }
    };
    void startToActivity(BaseActivity *baseActivity){
    baseActivity -> onStart();
    }

    void add(int number1, int number2){
    cout << number1 + number2 << endl;
    }

    void add(float number1, float number2){
    cout << number1 + number2 << endl;
    }

    void add(double number1, double number2){
    cout << number1 + number2 << endl;
    }
    int main(){
    BaseActivity *homeActivity = new HomeActivity();
    BaseActivity *loginActivity = new LoginActivity();
    startToActivity(homeActivity);
    startToActivity(loginActivity);
    if (homeActivity && loginActivity) delete homeActivity; delete loginActivity;

    add(100, 100);
    add(1.1f, 2.1f);
    add(23.2, 21.2);
    cout << endl;
    }

    纯虚函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    #include <iostream>
    using namespace std;
    class BaseActivity{
    private:
    void setContentView(String layoutId){
    cout << "解析布局文件,反射" << endl;
    }
    public :
    void onCreate(){
    setContentView(getLayoutId());
    initView();
    initData();
    initListener();
    }
    //纯虚函数就是抽象函数,必须实现,虚函数virtual String getLayoutId();不是必须实现的
    virtual String getLayoutId() = 0;
    virtual void initView() = 0;
    virtual void initData() = 0;
    virtual void initListener() = 0;
    };
    // 如果子类没有实现纯虚函数就是抽象类,不能实例化
    class MainActivity: public BaseActivity{
    String getLayoutId(){
    return "R.layout.activity_main";
    }
    void initView(){

    }
    void initData(){

    }
    void initListener(){

    }
    };
    int main(){
    MainActivity mainActivity;
    return 0;
    }

    全纯虚函数

  • C++没有接口,所有的函数都是纯虚函数就是C++的接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #include<iostream>
    using namespace std;
    Class Student{
    int _id;
    string name;
    int age;
    };
    // 定义接口
    class Student_DB{
    virtual void insertStudent(Student student) = 0;
    virtual void deleteStudent(int id) = 0;
    virtual void updateStudent(int id, Student student) = 0;
    virtual void queryStudent(Student student) = 0;
    }
    // 接口的实现类
    class Student_DBImpl: public Student_DB{
    public:
    void insertStudent(Student student){

    }
    void deleteStudent(int id){

    }
    void updateStudent(int id, Student student){

    }
    Student queryStudent(Student student){

    }
    }
    int main(){
    Student_DBImpl student_DBImpl;

    return 0;
    }

    回调

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    // 返回的数据对象
    class SuccessBean {
    public:
    String username;
    String userpwd;
    SuccessBean(String username, String userpwd):username(username),userpwd(userpwd){}
    }
    // 回调接口
    class ILoginResponse{
    public:
    virtual void loginSuccess(int code, string message, SuccessBean successBean) = 0;
    virtual void loginError(int code, string message) = 0;
    }
    // 登录操作
    void loginAction(String name, String pwd, ILoginResponse loginResponse){
    if (name.empty() || pwd.empty()){
    cout << "用户名或密码为空" << endl;
    return;
    }
    if ("admin" == name && "123" == pwd){
    loginResponse.loginSuccess(200, "登录成功", SuccessBean(name, "恭喜你成功登入"));
    loginResponse.loginError(404, "用户名或密码错误");
    }
    }
    // 接口的实现类
    class ILoginResponseImpl: public ILoginResponse{
    public:
    void loginSuccess(int code, string message, SuccessBean successBean){
    cout << "登录成功" << "code:" << code << "message" << message << "successBean" << successBean.username << "," << successBean.userpwd << endl;
    }
    void loginError(int code, string message){
    cout << "登录失败" << "code:" << code << "message" << endl;
    }
    }
    int main(){
    string username;
    cout << "请输入用户名.." << endl;
    cin >> username;
    string userpwd;
    cout << "请输入密码.." << endl;
    cin >> userpwd;
    ILoginResponseImpl iLoginResponseImpl;
    loginAction(username, userpwd, iLoginResponseImpl);
    return 0;
    }

    泛型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    template <typename TT>
    void addAction(TT n1, TT n2){
    cout << "模版函数:" << n1 + n2 << endl;
    }

    int main(){
    addAction(1, 2);
    addAction(10.2f, 20.3f);
    addAction(545.34, 324.3);
    return 0;
    }


随着工作年限的增长,越来越意识到C语言的重要性,Android的底层是C和linux内核,Android中为提高安全性,防止反编译,防止二次打包,提升程序的执行效率都是用C去实现的,作为Android开发者,掌握C语言才能进行NDK开发,提高自己的核心竞争力,拓宽职业道路


指针与地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(){
int i = 100;
double d = 200;
printf("i的值是:%d\n", i); // 100
printf("d的值是:%lf\n", d); // 200

printf("i的值是:%d\n", *(&i)); // 100 取该地址的值
printf("d的值是:%lf\n", *(&d)); // 200 取该地址的值

int *intP = &i;
double *doubleP = &d;

*intP = 220; // 修改内存地址对应的值为220
printf("i的值是:%d\n", *intP); // 220 取该地址的值
printf("d的值是:%lf\n", *doubleP); // 200 取该地址的值
}

交换两个变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void change(int *a, int *b){
int temp = *a;
*a = *b;
*b = temp;
}
int main(){
int a = 100;
int b = 200;
printf("a的值为:%d\n", a); // 100
printf("b的值为:%d\n", b); // 200

change(&a, &b);

printf("a的值为:%d\n", a); // 200
printf("b的值为:%d\n", b); // 100
return 0;
}

多级指针

  • 指针变量存放的是内存地址,指针变量自己也有地址
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 多级指针
    void test1(){
    int num = 999;
    int * p1 = &num;
    int ** p2 = &p1;
    int *** p3 = &p2;
    printf("p1的值是:%p, p2的值是:%p, p3的值是:%p\n", p1, p2, p3);// p1存放num内存地址,p2存放p1内存地址,p3存放p2内存地址
    printf("p1的值是:%p, p2的值是:%p, p3的值是:%p\n", &p1, &p2, &p3);// p1,p2,p3自己的内存地址都不一样
    printf("p2的内存地址对应的值是:%d\n",**p2); // 999
    printf("p3的内存地址对应的值是:%d\n",***p3); // 999
    }

    数组指针

  • 指针变量在32位下占4个字节,64位下占8个字节,指针类型决定了sizeof,获取元素时的偏移
  • 数组变量就是一个指针,存放的是第一个元素的内存地址,也等于它自己的内存地址
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 数组指针
    void test2(){
    int arr[] = {1,2,3,4};
    int *arr_p = arr;
    int i = 0;
    for (i = 0; i < sizeof arr / sizeof(int); ++i){
    printf("下标为%d的值是%d\n", i,*(arr_p + i));
    printf("下标%d的内存地址是%p\n",i, arr_p + i); // 地址间隔4个字节
    }
    // 同一个地址
    printf("arr = %p\n", arr);
    printf("&arr = %p\n", &arr);
    printf("&arr[0] = %p\n", &arr[0]);

    // 取数组第二个值
    arr_p ++;
    printf("%d\n", *arr_p);

    // 超出范围,野指针
    arr_p += 200;
    printf("%d\n", *arr_p);
    }

    内存静态开辟和动态开辟

  • 动态开辟需要手动释放,手动释放后如果不赋值为NULL,就是悬空指针
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void dynamicAction(int num){
    // 堆区动态开辟1M内存
    int *arr = malloc(num * sizeof(int));
    printf("dynamicAction函数,arr自己的内存地址:%p,堆区开辟的内存地址:%p\n",&arr,arr);
    // dynamicAction函数,arr自己的内存地址:0x7ffee47e1480,堆区开辟的内存地址:0x7fdb5dd00000
    // 释放堆区开辟的内存
    if(arr){
    free(arr);
    arr = NULL; // 如果不赋值为NULL,就是悬空指针
    printf("dynamicAction函数2,arr自己的内存地址:%p,堆区开辟的内存地址:%p\n",&arr,arr);
    // dynamicAction函数2,arr自己的内存地址:0x7ffee47e1480,堆区开辟的内存地址:0x0
    }
    }
  • 静态开辟,使用栈内存,自动释放
    1
    2
    3
    4
    void staticAction(){
    int arr[6];
    printf("staticAction函数,arr自己的内存地址:%p,堆区开辟的内存地址:%p\n",&arr,arr);
    }

    realloc重新开辟内存

  • 扩容内存时,地址不一定连续,物理内存被其它占用会返回新的地址,所以内存重新开辟时需要传入指针和总大小进行拷贝
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    int main(){
    int num;
    printf("请输入新的个数:");
    // 获取用户输入的值
    scanf("%d", &num);

    int *arr = (int *)malloc(arr, sizeof(int) * num);
    for (int i = 0; i < num; ++i){
    arr[i] = i + 10001;
    }
    printf("开辟的内存地址:%p\n", arr);
    // 开辟新的内存空间
    int newNum;
    printf("请输入新增加的个数");
    scanf("%d", &newNum);
    int *newArr = (int *)realloc(arr, sizeof(int) * (num + newNum));
    if (newArr){
    int j = num;
    for (;j < (num + newNum); ++j){
    arr[j] = j + 10001;
    }
    printf("新开辟的内存地址:%p\n", newArr);
    }
    // 释放内存操作
    if (newArr){
    free(newArr);
    newArr = NULL;
    arr = NULL;
    }
    else{
    free(arr);
    arr = NULL;
    }
    return 0;
    }

    函数指针

  • 使用函数指针实现回调,相当于Java的接口
  • 函数指针和它自己的内存地址相同
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    void add(int num1, int num2){
    printf("num1 + num2 = %d\n", (num1 + num2));
    }

    void mins(int num1, int num2){
    printf("num1 - num2 = %d\n", (num1 - num2));
    }

    // 传递函数指针
    void operate(void(*method) (int,int),int num1, int num2){
    method(num1,num2);
    }

    void test4(){
    operate(add, 10, 20);
    void(*method)(int,int) = mins;
    operate(method, 100, 20);
    // 函数指针和它自己的内存地址相同
    printf("%p, %p\n", add, &add);
    }

    生成随机数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <stdlib.h>
    #include <time.h>
    void test5(){
    srand((unsigned) time(NULL));
    int i;
    for (i = 0; i < 10; ++i) {
    printf("随机数%d\n", rand() % 100);
    }
    }

    复制字符串

    1
    2
    3
    4
    5
    6
    7
    #include <string.h>
    void test6(){
    char string[10];
    char* str1 = "abcdefghi";
    strcpy(string, str1);
    printf("%s\n", string);
    }

    字符串获取长度

  • 也可以直接使用strLen
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    void getLen(int *resultLen, char *str){
    int count = 0;
    while(*str){
    str ++;
    count ++;
    }
    *resultLen = count;
    }
    /**
    * C/C++会把数组优化成指针提高效率,导致长度计算错误
    int getLen(int arr[]){
    int len = sizeof(arr) / sizeof(int);
    return len;
    }
    */
    void test7{
    char str1[] = {'H','e','l','l','o','\0'}; // 遇到\0停下来
    str1[2] = 'z'; // 栈空间,允许修改
    printf("第一种方式:%s\n", str1);
    int count;
    getLen(&count, str1);
    printf("长度为:%d\n", count);
    char *str2 = "Hello"; // 结尾隐式增加\0
    // str2[2] = 'z'; 会报错,不允许修改全局区的字符串
    printf("第二种方式:%s\n", str2);
    }

    字符串转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void convertInt(){
    char *num = "1";
    int res = atoi(num);
    printf("转换结果:%d\n", res);
    }

    void convertDouble(){
    double resD = atof(num);
    printf("转换结果:%lf\n", resD);
    }

    字符串比较

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void test8(){
    char *str1 = "Hello";
    char *str2 = "hello";
    // int res = strcmp(str1, str2); // 区分大小写
    int res = strcmpi(str1, str2); // 不区分大小写
    if (!res){
    printf("相等");
    }else{
    printf("不相等")
    }
    }

    字符串查找子串

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void test9(){
    char *text = "hello world";
    char *subtext = "w";
    // 从搜索到子串的下标位置截取到末尾
    char *pop = strstr(text, subtext);
    if (pop){
    printf("查找到了,pop的值是%s\n",pop);
    }
    else{
    printf("没有查找到,subtext的值是%s\n",subtext);
    }
    int index = pop - text;
    printf("%s第一次出现的位置是:%d\n",subtext, index);
    }

    字符串拼接

    1
    2
    3
    4
    5
    6
    7
    8
    void test10(){
    char destination[25];
    char *blank = "--到--", *CPP = "C++", *Java = "Java";
    strcpy(destination, CPP); // 先拷贝到数组
    strcat(destination, blank); // 拼接blank
    strcat(destination, Java); // 拼接Java
    printf("拼接后的结果%s\n", destination);
    }

    字符串截取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    void substring1(char *res, char *str, int start, int end){
    char *temp = str;
    int index = 0; // 当前截取的位置
    while(*temp){
    if (index > start && index < end){
    *res = *temp;
    res ++;
    }
    temp ++;
    index ++;
    }
    }
    void substring2(char **res, char *str, int start, int end){
    char *temp = str;
    char resArr[end - start]; // 方案1. 临时变量在栈中分配内存,方法结束会被释放
    // char *resArr = malloc(end - start); // 方案2. 堆中开辟内存
    int index = 0;
    for (int i = start, i < end; ++i){
    resArr[index] = *(temp + i);
    index ++;
    }
    // 二级指针的一级指针等于test11的res一级指针
    // *res = resArr; // 方案2. 结果指向堆中的数组,方法结束后也不能释放所以不推荐
    strcpy(*res, resArr); // 方案1. 拷贝到数组
    printf("截取后的结果:%s\n", resArr);
    }
    void substring3(char *res, char *str, int start, int end){
    for (int i = start; i < end; ++i){
    *(result++) = *(str + i);
    }
    }
    void substring4(char *res, char *str, int start, int end){
    strncpy(result, str + start, end - start);
    }
    void test11(){
    char *str = "hello";
    char *res;
    substring1(res, str, 1, 4);
    // substring2(&res, str, 1, 4);
    printf("截取后的内容是:%s",res);
    }

    大小写转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    void lower(char *dest, char *text){
    int *temp = text;
    while(*temp){
    *dest = tolower(*temp);
    temp ++;
    dest ++;
    }
    *dest = '\0'; // 结尾增加\0避免打出系统值
    printf("name:%s\n", text);
    }
    void test12(){
    char *text = "hello";
    char dest[20];
    lower(dest, text);
    printf("小写转换后的结构是:%s\n", dest);
    }

    结构体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    struct Dog{
    char name[10];
    int age;
    char sex;
    }
    struct Person{
    char *name;
    int age;
    char sex;
    } person1 = {"小明", 21, 'M'}
    struct Study{
    char *studyContent;
    }
    struct Student{
    char name[10];
    int age;
    char sex;
    struct Study study; // 引用外部结构体,声明结构体对象
    // 定义结构体并声明结构体对象
    struct Wan {
    char *wanContent;
    }wan;
    }
    // 匿名结构体定义别名
    typedef struct {
    char *name;
    int age;
    char sex;
    } Cat;
    int main(){
    struct Dog dog;
    strcpy(dog.name, "旺财");
    dog.age = 2;
    dog.sex = 'M';
    printf("name:%s, age:%d, sex:%c \n", dog.name, dog.age, dog.sex);

    struct Student student = {"小红", 18, 'F', {"学习C"}, {"王者荣耀"}};
    printf("name:%s, age:%d, sex:%c, study:%s, wan:%s \n", student.name, student.age, student.sex, student.study.studyContent, student.wan.wanContent);

    struct Dog dog2 = {"旺财2"4, 'M'};
    struct Dog *dogp = &dog2;
    dogp -> age = 3;
    dogp -> sex = 'F';
    strcpy(dogp->name, "旺财3");
    printf("name:%s, age:%d, sex:%c \n", dogp->name, dogp->age, dogp->sex);
    free(dogp);
    dogp = NULL;

    struct Dog dogArr[10] = {
    {"旺财4"4, 'M'},
    {"旺财5"5, 'M'},
    {"旺财6"6, 'M'},
    {},
    {},
    {},
    {},
    {},
    {},
    {}
    };
    struct Dog dog9 = {"旺财9"9, 'M'};
    // dogArr[9] = dog9;
    *(dogArr + 9) = dog9;
    printf("name:%s, age:%d, sex:%c \n", dog9.name, dog9.age, dog9.sex
    // 动态申请内存
    struct Dog *dogArr2 = malloc(sizeof(struct Dog) * 10);
    strcpy(dogArr2->name, "大黄1");
    dogArr2 -> age = 2;
    dogArr2 -> sex = 'M';
    printf("name:%s, age:%d, sex:%c \n", dogArr2->name, dogArr2->age, dogArr2->sex);
    // 指针移到第8个元素
    dogArr2 += 7;
    strcpy(dogArr2 -> name, "大黄8");
    dogArr2 -> age = 3;
    dogArr2 -> sex = 'M';
    printf("name:%s, age:%d, sex:%c \n", dogArr2->name, dogArr2->age, dogArr2->sex);
    free(dogArr2);
    dogArr2 = NULL;

    Cat *cat = malloc(sizeof(Cat)); // 结构体指针
    return 0;
    }

    枚举

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    enum CommentType{
    TEXT = 10,
    TEXT_IMAGE,
    IMAGE
    };
    typedef enum {
    TEXT1 = 10,
    TEXT_IMAGE1,
    IMAGE1
    } CommentType1;
    int main(){
    enum CommentType commentType = TEXT;
    printf("%d\n", commentType);

    CommentType1 commentType1 = TEXT1;
    printf("%d\n", commentType1);
    return 0;
    }

    文件读写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    void readFile(){
    char *fileName = "/Users/JessieKate/CLionProjects/TestProject/test.txt";
    FILE *file = fopen(fileName,"r"); // 此文件必须存在
    if (!file){
    printf("文件打开失败,请查看路径");
    exit(0);
    }

    char buffer[10]; // 创建buffer读取
    while(fgets(buffer, 10, file)){
    printf("%s", buffer);
    }
    // 关闭文件
    fclose(file);
    }

    void writeFile(){
    char *fileName = "/Users/JessieKate/CLionProjects/TestProject/test1.txt";
    FILE *file = fopen(fileName, "w"); // 此文件可以不存在
    if (!file){
    printf("文件打开失败,请查看路径");
    exit(0);
    }
    fputs("这是我写入的测试内容", file);
    fclose(file);
    }

    文件复制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    void copyFile(){
    char *fileName = "/Users/JessieKate/CLionProjects/TestProject/test.txt";
    char *fileNameCopy = "/Users/JessieKate/CLionProjects/TestProject/testCopy.txt";
    FILE *file = fopen(fileName, "rb");
    FILE *fileCopy = fopen(fileNameCopy, "wb");
    if (!file || !fileCopy){
    printf("文件打开失败,请查看路径");
    exit(0);
    }
    int buffer[514]; // 缓存数组
    int len; // 每次读取的长度

    // fread 读入缓存buffer, 偏移数量, 读取字节数写入缓存
    while((len = fread(buffer, 1, 514, file)) > 0){
    fwrite(buffer, len, 1, fileCopy);
    }

    fclose(file);
    fclose(fileCopy);
    }

    文件大小

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void getSize(){
    char *fileName = "/Users/JessieKate/CLionProjects/TestProject/test.txt";
    FILE *file = fopen(fileName,"r");
    if (!file){
    printf("文件打开失败,请查看路径");
    exit(0);
    }
    fseek(file, 0, SEEK_END); //从0开始挪动到文件结束
    long fileSize = ftell(file); //读取file的信息
    printf("%s文件的字节大小是:%ld",fileName, fileSize);
    }

    文件加密解密

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    void encrypt(){
    char * fileName = "/Users/JessieKate/CLionProjects/TestProject/Image.jpg";
    char * fileNameEncode = "/Users/JessieKate/CLionProjects/TestProject/Image_encode.jpg";
    FILE * file = fopen(fileName, "rb");
    FILE * fileEncode = fopen(fileNameEncode,"wb");

    if (!file || !fileEncode){
    printf("文件打开失败,请查看路径");
    exit(0);
    }
    char *password = "123456";
    // 加密,破坏文件,解密,还原文件
    int c;
    int index = 0;
    int pass_len = strlen(password); // 获取密码的长度

    // fgetc 返回EOF = end of file
    while((c = fgetc(file)) != EOF){
    // 循环获取密码的每个字符,1,2,3,4,5,6,1,2,3...
    char item = password[index % pass_len];
    printf("item:%c%\n",item);
    fputc( c ^ item, fileEncode);
    index++;
    }
    fclose(file);
    fclose(fileEncode);
    }

    void decrypt(){
    char * fileNameEncode = "/Users/JessieKate/CLionProjects/TestProject/Image_encode.jpg";
    char * fileNameDecode = "/Users/JessieKate/CLionProjects/TestProject/Image_decode.jpg";
    FILE * file = fopen(fileNameEncode, "rb");
    FILE * fileDecode = fopen(fileNameDecode,"wb");
    if (!file || !fileDecode){
    printf("文件打开失败,请查看路径");
    exit(0);
    }
    char *password = "123456";
    int c;
    int index = 0;
    int pass_len = strlen(password);

    // fgetc 返回EOF = end of file
    while((c = fgetc(file)) != EOF){
    char item = password[index % pass_len];
    fputc( c ^ item, fileDecode);
    index++;
    }
    fclose(file);
    fclose(fileDecode);
    }


最近入了一台pixel3当测试机和备用机,完美刷入最新的Android系统11,据说12这样刷入也是可以的,magisk刷root权限和电信模块,确实是最佳测试机,完整的google套件,原汁原味的Android系统,喜欢尝鲜,喜欢原生系统且有梯子的小伙伴可以考虑入手一台,它不会让你失望的。下面记录一下我的刷机流程,第一次刷pixel还是踩了不少的坑


Pixel3不支持中国电信,需要手机root后刷入电信模块才可以使用电信卡(包括打电话和流量上网),Root方案选择了magisk,兼容性好

刷入完整的官方Rom包

  1. 根据自己的手机型号和刷入的系统版本,下载对应的镜像文件,google官方Rom包地址:https://developers.google.com/android/images
  2. 打开开发者模式,狂戳Setting -> Build number,打开USB调试,Setting -> System -> Developer Options打开Usb debugging,勾选oem unlocking
  3. 配置adb命令,如果不是Android开发者可下载android-platform-tools,这里就不赘述了
  4. 解压下载的blueline-rq2a.210505.002-factory-687d8468.zip,有个flash-all.sh的脚本
  5. 手机进入fastboot模式,adb reboot bootloader,再执行这个脚本./flash-all.sh

安装Magisk并Root

github下载最新Magisk并安装到手机,https://github.com/topjohnwu/Magisk/releases

提取boot.img

刚下载的blueline-rq2a.210505.002-factory-687d8468.zip文件解压后内部仍有一个压缩文件image-blueline-rq2a.210505.002.zip,继续解压,解压后的文件夹中会得到一个boot.img,把它push到手机

1
adb push ./blueline-rq2a.210505.002/image-blueline-rq2a.210505.002/boot.img sdcard/Download

Magisk修改boot.img

打开magisk选择第一个install,选择select and patch a file选择刚才push的文件boot.img,执行后会在该目录下生成一个magisk_patched.img,把这个文件pull到PC
adb pull /sdcard/Download/magisk_patched-23000_J2eHI.img

magisk_patched.img刷入手机

  1. 手机进入fastboot模式,adb reboot bootloader
  2. fastboot解锁,fastboot flashing unlock
  3. 刷入magisk_patched.img,fastboot flash boot magisk_patched-23000_J2eHI.img
  4. 重启手机,fastboot reboot

    验证是否root

    执行命令adb shell
    执行命令su
    查看pixel是否有确认root授权的提示

刷入电信模块

  1. github下载china_telecom_supporter,https://github.com/apporc/china_telecom_supporter
  2. 把压缩包push到手机上
    adb push ./china_telecom_supporter.zip /sdcard/Download
  3. adb命令解压
    1
    unzip -d /sdcard/Download/china_telecom_supporter /sdcard/Download/china_telecom_supporter.zip
  4. 移动解压后的文件夹到系统目录/data/adb/modules/
    1
    mv /sdcard/Download/china_telecom_supporter /data/adb/modules/china_telecom_supporter
  5. 删除 fdr_check 文件
    rm /data/vendor/modem_fdr/fdr_check
  6. 重启,如果刷入成功此时进入系统可以看到电信的信号了,能正常打电话和流量上网~


又是一段时间没有更新博客了,这段时间对以前的学习做了一个总结,还是很值得的,希望接下来的时间里,能继续保持学习的热情,稳定输出学习和工作的总结到博客,毕竟一个好习惯值得坚持下去~也希望和各位大佬一起学习和交流,接下来记录用kotlin手写PhotoView的过程


自定义View

  1. 居中位置绘制bitmap
  2. 计算小图,大图缩放比
  3. fling,双击,滑动时调整画布偏移值绘制
  4. 双击,缩放手势调整画布当前缩放比例绘制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
class PhotoView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
lateinit var bitmap: Bitmap
lateinit var paint: Paint
/**
* 原始位置
*/
var originalOffsetX = 0f
var originalOffsetY = 0f
/**
* 滑动偏移
*/
var offsetX = 0f
var offsetY = 0f
var smallScale = 0f // 小图缩放比
var bigScale = 0f // 大图缩放比
var currentScale = 0f // 当前缩放比
get() {
Log.i(TAG, "get: $field")
return field
}
set(value) {
field = value
invalidate()
Log.i(TAG, "set: $value")
}
var isLarge = false // 是否放大状态
lateinit var gestureDetector: GestureDetector // 手势回调
lateinit var scaleGestureDetector: ScaleGestureDetector // 缩放手势回调
val overScroller by lazy { OverScroller(context) } // 滚动工具类,边界回弹
// 缩放动画
val scaleAnimation: ObjectAnimator by lazy {
val temp = ObjectAnimator.ofFloat(this, "currentScale", 0f)
temp.setFloatValues(smallScale, bigScale)
temp
}

companion object {
const val SCALE_FACTOR = 1.5f
}

fun initBitmap(bitmap: Bitmap) {
if (width == 0 || height == 0) {
throw IllegalStateException("call after View is rendered")
}
this.bitmap = bitmap
paint = Paint(Paint.ANTI_ALIAS_FLAG)
gestureDetector = GestureDetector(context, PhotoGesture())
scaleGestureDetector = ScaleGestureDetector(context, PhotoScaleGestureListener())
// bitmap居中渲染
originalOffsetX = (width - bitmap.width) / 2f
originalOffsetY = (height - bitmap.height) / 2f
// 横图计算缩放比
if (bitmap.width.toFloat() / bitmap.height > width.toFloat() / height) {
smallScale = width.toFloat() / bitmap.width.toFloat()
bigScale = (height.toFloat() / bitmap.height.toFloat()) * SCALE_FACTOR
}
// 竖图计算缩放比
else {
smallScale = height.toFloat() / bitmap.height.toFloat()
bigScale = (width.toFloat() / bitmap.width.toFloat()) * SCALE_FACTOR
}
// 当前缩放为最小缩放
currentScale = smallScale
invalidate()
}

// 边界修正
private fun fixOffset(){
offsetX = min(offsetX,(bitmap.width * bigScale - width)/ 2)
offsetX = max(offsetX, -(bitmap.width * bigScale - width) / 2)
offsetY = min(offsetY, (bitmap.height * bigScale - height) / 2)
offsetY = max(offsetY, -(bitmap.height * bigScale - height) / 2)
}

override fun onDraw(canvas: Canvas?) {
if (this::bitmap.isInitialized) {
// 当前缩放占比
val scaleFaction = (currentScale - smallScale) / (bigScale - smallScale)
// 画布平移
canvas?.translate(offsetX * scaleFaction, offsetY * scaleFaction)
// 画布缩放
canvas?.scale(currentScale, currentScale, (width / 2).toFloat(), (height / 2).toFloat())
canvas?.drawBitmap(bitmap, originalOffsetX, originalOffsetY, paint)
}
}

override fun onTouchEvent(event: MotionEvent?): Boolean {
// 默认缩放回调处理
var res = scaleGestureDetector.onTouchEvent(event)
// 不是缩放操作时,由手势回调处理
if (!scaleGestureDetector.isInProgress) {
res = gestureDetector.onTouchEvent(event)
}
return res
}

// 滑动处理
val runnable = object :Runnable{
override fun run() {
if (overScroller.computeScrollOffset()){
offsetX = overScroller.currX.toFloat()
offsetY = overScroller.currY.toFloat()
invalidate()
postOnAnimation(this)
}
}
}

inner class PhotoGesture : GestureDetector.SimpleOnGestureListener() {

override fun onDown(e: MotionEvent?): Boolean {
return true // 拦截DOWN事件
}

override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
// 当前放大状态,调overScroller的fling
if (isLarge) {
overScroller.fling(offsetX.toInt(), offsetY.toInt(), velocityX.toInt(), velocityY.toInt(),
(-(bitmap.width * bigScale - width) / 2).toInt(),
((bitmap.width * bigScale - width) / 2).toInt(),
(-(bitmap.height * bigScale - height) / 2).toInt(),
((bitmap.height * bigScale - height) / 2).toInt(),
300,300)
// 滑动刷新偏移值处理
postOnAnimation(runnable)
}
return super.onFling(e1, e2, velocityX, velocityY)
}

override fun onDoubleTap(e: MotionEvent?): Boolean {
isLarge = !isLarge
// 当前是放大状态
if (isLarge) {
// 刷新绘制位置
e?.let {
offsetX = (it.x - width / 2f) - (it.x - width / 2f) * bigScale / smallScale
offsetY = (it.y - height / 2f) - (it.y - height / 2f) * bigScale / smallScale
}
// 边界修正
fixOffset()
// 缩放动画开始
scaleAnimation.start()
} else {
scaleAnimation.reverse()
}

return super.onDoubleTap(e)
}

override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
// 当前是放大状态
if (isLarge) {
// 刷新绘制位置
offsetX -= distanceX
offsetY -= distanceY
// 边界修正
fixOffset()
invalidate()
}
return super.onScroll(e1, e2, distanceX, distanceY)
}

}

inner class PhotoScaleGestureListener:ScaleGestureDetector.OnScaleGestureListener{
var initScale = 0f
override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean {
// 初始缩放比例
initScale = currentScale
return true
}

override fun onScaleEnd(detector: ScaleGestureDetector?) {

}

override fun onScale(detector: ScaleGestureDetector?): Boolean {
// 如果当前缩放比 大于 最小缩放比 并且 非放大状态 或 当前缩放比 等于 最小缩放比 并且非放大状态
if ((currentScale > smallScale && !isLarge) || (currentScale == smallScale && !isLarge)){
// 设置为放大状态
isLarge = !isLarge
}
// 如果初始缩放比 * 缩放系数 小于 最小缩放比 赋值为最小缩放比 否则为初始缩放比 * 缩放系数
detector?.let {
currentScale = if (initScale * it.scaleFactor < smallScale){
smallScale
} else{
initScale * it.scaleFactor
}
}
invalidate()

return false
}
}
}

在布局中使用

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.tws.moments.views.PhotoView
android:id="@+id/photo"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

设置bitmap

1
2
3
4
5
try {
photo.initBitmap(bitmap)
} catch (e: Exception) {
Log.i(TAG, "subscribe: ${e.stackTrace}")
}


动态换肤算是APP开发中常见的技术,最近公司的项目也是正好用到,需求是从后台下载皮肤包到本地,用户可以选择自己喜欢的皮肤包进行页面的动态换肤,增强用户体验和应用的趣味性。大致整理了一下实现思路,在此做个记录,如果有更好的方案,欢迎交流~

  1. 读取apk的内容
    异步加载本地目录的皮肤资源apk,通过反射调用AssetManager添加资源路径的方法将apk的资源加载进去,得到全新的AssetManager并注册,加载资源时使用新AssetManager的资源,新resource赋给全局的resource,切回默认app皮肤时,再赋值回默认的resource。
  2. 收集换肤的View相关数据,干预xml解析拦截需要换肤的view,Factory2中生产view时记录需要换肤的属性
  3. 观察者模式绑定被观察者-资源管理器和观察者-自定义的LayoutInflater.Factory2,BaseActivity实现统一换肤接口ISkinUpdate的逻辑调资源管理器加载皮肤APK。
  4. 执行换肤逻辑
    设置自定义的LayoutInflater.Factory2,拦截xml中原本解析的view返回一个新的view,判断当前view是否需要换肤,需要则直接设置相应属性,不需要则执行原本逻辑。

皮肤管理器

在Application中初始化皮肤管理器,继承Observable类,是一个可被观察的对象,需要通知观察者对象,主要负责加载本地皮肤包apk并更新皮肤resource对象到资源管理器,通知各个需要换肤的View更新UI,并记录当前使用的皮肤包路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class SkinManager extends Observable {

private volatile static SkinManager instance;
/**
* Activity生命周期回调
*/
private ApplicationActivityLifecycle skinActivityLifecycle;
private Application mContext;

/**
* 初始化 必须在Application中先进行初始化
*
* @param application
*/
public static void init(Application application) {
if (instance == null) {
synchronized (SkinManager.class) {
if (instance == null) {
instance = new SkinManager(application);
}
}
}
}

private SkinManager(Application application) {
mContext = application;
//初始化sp,记录当前使用的皮肤
SkinPreference.init(application);
//资源管理类,从皮肤resource对象中加载资源
SkinResources.init(application);
//注册Activity生命周期,并设置被观察者
skinActivityLifecycle = new ApplicationActivityLifecycle(this);
application.registerActivityLifecycleCallbacks(skinActivityLifecycle);
//加载上次使用保存的皮肤
loadSkin(SkinPreference.getInstance().getSkin());
}

public static SkinManager getInstance() {
return instance;
}


/**
* 加载皮肤并更新resource到皮肤资源管理器
*
* @param skinPath 皮肤路径 如果为空则使用默认皮肤
*/
public void loadSkin(String skinPath) {
if (TextUtils.isEmpty(skinPath)) {
//还原默认皮肤
SkinPreference.getInstance().reset();
SkinResources.getInstance().reset();
} else {
try {
//宿主app的resources;
Resources appResource = mContext.getResources();
//反射创建AssetManager与Resource
AssetManager assetManager = AssetManager.class.newInstance();
//资源路径设置,压缩包的目录
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",
String.class);
addAssetPath.invoke(assetManager, skinPath);

//根据设备显示器信息与配置(横竖屏、语言等)创建新的皮肤Resources
Resources skinResource = new Resources(assetManager, appResource.getDisplayMetrics
(), appResource.getConfiguration());

//获取本地皮肤Apk(皮肤包) 包名,并应用皮肤
PackageManager mPm = mContext.getPackageManager();
PackageInfo info = mPm.getPackageArchiveInfo(skinPath, PackageManager
.GET_ACTIVITIES);
String packageName = info.packageName;
SkinResources.getInstance().applySkin(skinResource, packageName);

//记录当前使用的皮肤路径
SkinPreference.getInstance().setSkin(skinPath);


} catch (Exception e) {
e.printStackTrace();
}
}
//通知采集的View 更新皮肤
//被观察者改变,通知所有观察者
setChanged();
notifyObservers(null);
}

}
  • SP工具类,记录皮肤包路径
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    public class SkinPreference {
    private static final String SKIN_SHARED = "skins";
    private static final String KEY_SKIN_PATH = "skin-path";
    private volatile static SkinPreference instance;
    private final SharedPreferences mPref;

    public static void init(Context context) {
    if (instance == null) {
    synchronized (SkinPreference.class) {
    if (instance == null) {
    instance = new SkinPreference(context.getApplicationContext());
    }
    }
    }
    }

    public static SkinPreference getInstance() {
    return instance;
    }

    private SkinPreference(Context context) {
    mPref = context.getSharedPreferences(SKIN_SHARED, Context.MODE_PRIVATE);
    }

    public void setSkin(String skinPath) {
    mPref.edit().putString(KEY_SKIN_PATH, skinPath).apply();
    }

    public void reset() {
    mPref.edit().remove(KEY_SKIN_PATH).apply();
    }

    public String getSkin() {
    return mPref.getString(KEY_SKIN_PATH, null);
    }

    }

    监听Activity生命周期

    传入被观察者-皮肤管理器对象,缓存activity到对应自定义LayoutInflaterFactory对象的映射
  1. Activity被创建时,反射设置mFactorySet属性为false,设置自定义的LayoutInflaterFactory,并记录与当前activity的映射关系到map,添加自定义的LayoutInflaterFactory对象为皮肤管理器对象的观察者对象,接收皮肤管理器加载完成的通知
  2. Activity被销毁时,获取到对应的自定义LayoutInflaterFactory对象,移除对被观察者-皮肤管理器对象的监听
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    public class ApplicationActivityLifecycle implements Application.ActivityLifecycleCallbacks {

    private Observable mObserable; // 被观察者,皮肤管理器对象
    private ArrayMap<Activity, SkinLayoutInflaterFactory> mLayoutInflaterFactories = new
    ArrayMap<>(); // 记录activity到对应自定义LayoutInflaterFactory对象的映射

    public ApplicationActivityLifecycle(Observable observable) {
    mObserable = observable;
    }

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    /**
    * 更新状态栏
    */
    SkinThemeUtils.updateStatusBarColor(activity);

    /**
    * 更新布局视图
    */
    //获得Activity的布局加载器
    LayoutInflater layoutInflater = activity.getLayoutInflater();

    try {
    //Android 布局加载器 使用 mFactorySet 标记是否设置过Factory
    //反射设置 mFactorySet 标签为false
    Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
    field.setAccessible(true);
    field.setBoolean(layoutInflater, false);
    } catch (Exception e) {
    e.printStackTrace();
    }

    //设置自定义的LayoutInflaterFactory,并记录与当前activity的映射关系到map
    SkinLayoutInflaterFactory skinLayoutInflaterFactory = new SkinLayoutInflaterFactory
    (activity);
    LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutInflaterFactory);
    mLayoutInflaterFactories.put(activity, skinLayoutInflaterFactory);

    mObserable.addObserver(skinLayoutInflaterFactory);
    }

    @Override
    public void onActivityStarted(Activity activity) {

    }

    @Override
    public void onActivityResumed(Activity activity) {

    }

    @Override
    public void onActivityPaused(Activity activity) {

    }

    @Override
    public void onActivityStopped(Activity activity) {

    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

    }

    @Override
    public void onActivityDestroyed(Activity activity) {
    SkinLayoutInflaterFactory observer = mLayoutInflaterFactories.remove(activity);
    SkinManager.getInstance().deleteObserver(observer);
    }
    }

    皮肤资源管理器

    记录原始宿主App的resource对象和新的皮肤resource对象,从皮肤resource对象中获取对应的属性
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    public class SkinResources {

    private String mSkinPkgName;
    private boolean isDefaultSkin = true;

    // app原始的resource
    private Resources mAppResources;
    // 皮肤包的resource
    private Resources mSkinResources;

    private SkinResources(Context context) {
    mAppResources = context.getResources();
    }

    private volatile static SkinResources instance;
    public static void init(Context context) {
    if (instance == null) {
    synchronized (SkinResources.class) {
    if (instance == null) {
    instance = new SkinResources(context);
    }
    }
    }
    }

    public static SkinResources getInstance() {
    return instance;
    }

    public void reset() {
    mSkinResources = null;
    mSkinPkgName = "";
    isDefaultSkin = true;
    }

    public void applySkin(Resources resources, String pkgName) {
    mSkinResources = resources;
    mSkinPkgName = pkgName;
    //是否使用默认皮肤
    isDefaultSkin = TextUtils.isEmpty(pkgName) || resources == null;
    }

    /**
    * 1.通过原始app中的resId(R.color.XX)获取到自己的 名字
    * 2.根据名字和类型获取皮肤包中的ID
    */
    public int getIdentifier(int resId){
    if(isDefaultSkin){
    return resId;
    }
    String resName=mAppResources.getResourceEntryName(resId);
    String resType=mAppResources.getResourceTypeName(resId);
    int skinId=mSkinResources.getIdentifier(resName,resType,mSkinPkgName);
    return skinId;
    }

    /**
    * 输入主APP的ID,到皮肤APK文件中去找到对应ID的颜色值
    * @param resId
    * @return
    */
    public int getColor(int resId){
    if(isDefaultSkin){
    return mAppResources.getColor(resId);
    }
    int skinId=getIdentifier(resId);
    if(skinId==0){
    return mAppResources.getColor(resId);
    }
    return mSkinResources.getColor(skinId);
    }

    public ColorStateList getColorStateList(int resId) {
    if (isDefaultSkin) {
    return mAppResources.getColorStateList(resId);
    }
    int skinId = getIdentifier(resId);
    if (skinId == 0) {
    return mAppResources.getColorStateList(resId);
    }
    return mSkinResources.getColorStateList(skinId);
    }

    public Drawable getDrawable(int resId) {
    if (isDefaultSkin) {
    return mAppResources.getDrawable(resId);
    }
    //通过 app的resource 获取id 对应的 资源名 与 资源类型
    //找到 皮肤包 匹配 的 资源名资源类型 的 皮肤包的 资源 ID
    int skinId = getIdentifier(resId);
    if (skinId == 0) {
    return mAppResources.getDrawable(resId);
    }
    return mSkinResources.getDrawable(skinId);
    }


    /**
    * 可能是Color 也可能是drawable
    *
    * @return
    */
    public Object getBackground(int resId) {
    String resourceTypeName = mAppResources.getResourceTypeName(resId);

    if ("color".equals(resourceTypeName)) {
    return getColor(resId);
    } else {
    // drawable
    return getDrawable(resId);
    }
    }

    }

    自定义LayoutInflatorFactory

  3. 自定义LayoutInflatorFactory继承LayoutInflater.Factory2和Observer,是一个观察者对象,需要对皮肤管理器对象进行监听,接收到通知时应用皮肤,刷新状态栏,所有需要换肤的View刷新UI
  4. 解析xml创建View时,如果不是SDK自带的view就反射创建,如果是SDK的view尝试增加固定前缀,再反射创建,再修改View的属性刷新UI,达到换肤的效果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    public class SkinLayoutInflaterFactory implements LayoutInflater.Factory2, Observer {
    // 待尝试的系统view固定前缀
    private static final String[] mClassPrefixList = {
    "android.widget.",
    "android.webkit.",
    "android.app.",
    "android.view."
    };


    private static final Class<?>[] mConstructorSignature = new Class[] {
    Context.class, AttributeSet.class};
    // 记录view名称与对应构造函数的映射
    private static final HashMap<String, Constructor<? extends View>> mConstructorMap =
    new HashMap<String, Constructor<? extends View>>();

    // 当选择新皮肤后需要替换View与之对应的属性
    // Activity的属性管理器
    private SkinAttribute skinAttribute;
    // 用于获取窗口的状态栏
    private Activity activity;

    // 初始化属性管理器
    public SkinLayoutInflaterFactory(Activity activity) {
    this.activity = activity;
    skinAttribute = new SkinAttribute();
    }

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    //换肤就是替换View的属性(src、background等)
    //创建 View,再修改View属性
    View view = createSDKView(name, context, attrs);
    if (null == view) {
    view = createView(name, context, attrs);
    }

    if (null != view) {
    //加载属性
    skinAttribute.look(view, attrs);
    }
    return view;
    }


    private View createSDKView(String name, Context context, AttributeSet
    attrs) {
    //如果包含 . 则不是SDK中的view,可能是自定义view包括support库中的View,尝试使用后面的构造方法反射创建
    if (-1 != name.indexOf('.')) {
    return null;
    }
    //不包含,是SDK中的view,在解析的name节点前,拼接上一些前缀如:android.widget. 尝试反射创建View
    for (int i = 0; i < mClassPrefixList.length; i++) {
    View view = createView(mClassPrefixList[i] + name, context, attrs);
    if(view!=null){
    return view;
    }
    }
    return null;
    }

    // 反射创建View
    private View createView(String name, Context context, AttributeSet
    attrs) {
    Constructor<? extends View> constructor = findConstructor(context, name);
    try {
    return constructor.newInstance(context, attrs);
    } catch (Exception e) {
    }
    return null;
    }

    private Constructor<? extends View> findConstructor(Context context, String name) {
    Constructor<? extends View> constructor = mConstructorMap.get(name);
    if (constructor == null) {
    try {
    Class<? extends View> clazz = context.getClassLoader().loadClass
    (name).asSubclass(View.class);
    constructor = clazz.getConstructor(mConstructorSignature);
    mConstructorMap.put(name, constructor);
    } catch (Exception e) {
    }
    }
    return constructor;
    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
    return null;
    }

    //如果有人发送通知,这里就会执行
    @Override
    public void update(Observable o, Object arg) {
    SkinThemeUtils.updateStatusBarColor(activity);
    skinAttribute.applySkin();
    }
    }

    属性管理器

    记录所有View需要替换的属性名称,需要换肤的View与View的属性信息列表,遍历当前解析的View的所有属性,过滤该View需要替换的属性并记录,对需要换肤的View进行属性修改
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    public class SkinAttribute {
    private static final List<String> mAttributes = new ArrayList<>();
    // 需要替换的属性名称
    static {
    mAttributes.add("background");
    mAttributes.add("src");
    mAttributes.add("textColor");
    mAttributes.add("drawableLeft");
    mAttributes.add("drawableTop");
    mAttributes.add("drawableRight");
    mAttributes.add("drawableBottom");
    }

    //记录需要换肤的View与View的属性信息
    private List<SkinView> mSkinViews = new ArrayList<>();


    // 遍历当前View的属性,记录需要替换的属性并执行换肤,对View进行属性修改
    public void look(View view, AttributeSet attrs) {
    List<SkinPair> mSkinPars = new ArrayList<>();

    for (int i = 0; i < attrs.getAttributeCount(); i++) {
    // 获得属性名如textColor/background
    String attributeName = attrs.getAttributeName(i);
    // 如果这个属性是需要替换的属性
    if (mAttributes.contains(attributeName)) {
    // 比如color的值有多种格式
    // #
    // ?722727272
    // @722727272
    // 以#开头表示写死的颜色 不可用于换肤
    String attributeValue = attrs.getAttributeValue(i);
    if (attributeValue.startsWith("#")) {
    continue;
    }
    int resId;
    // 以?开头的,去主题资源中找属性值
    if (attributeValue.startsWith("?")) {
    int attrId = Integer.parseInt(attributeValue.substring(1));
    resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
    } else {
    // 正常以 @ 开头
    resId = Integer.parseInt(attributeValue.substring(1));
    }
    // 记录到当前View需要替换的属性表
    SkinPair skinPair = new SkinPair(attributeName, resId);
    mSkinPars.add(skinPair);
    }
    }
    // 如果当前view有属性需要替换,或者该属性是支持换肤的自定义view
    if (!mSkinPars.isEmpty() || view instanceof SkinViewSupport) {
    SkinView skinView = new SkinView(view, mSkinPars);
    // 执行换肤,进行View的属性修改
    skinView.applySkin();
    // 添加到缓存
    mSkinViews.add(skinView);
    }
    }


    /*
    对所有需要换肤的view执行换肤操作
    */
    public void applySkin() {
    for (SkinView mSkinView : mSkinViews) {
    mSkinView.applySkin();
    }
    }

    static class SkinView {
    View view;
    // 当前View需要替换的属性列表
    List<SkinPair> skinPairs;

    public SkinView(View view, List<SkinPair> skinPairs) {
    this.view = view;
    this.skinPairs = skinPairs;

    }
    /**
    * 对一个View中所有需要替换的属性进行修改
    */
    public void applySkin() {
    // 如果是支持换肤的自定义view调换肤接口
    applySkinSupport();
    // 遍历当前View需要替换的属性列表
    for (SkinPair skinPair : skinPairs) {
    Drawable left = null, top = null, right = null, bottom = null;
    switch (skinPair.attributeName) {
    case "background":
    Object background = SkinResources.getInstance().getBackground(skinPair
    .resId);
    //背景可能是 @color 也可能是 @drawable
    if (background instanceof Integer) {
    view.setBackgroundColor((int) background);
    } else {
    ViewCompat.setBackground(view, (Drawable) background);
    }
    break;
    case "src":
    background = SkinResources.getInstance().getBackground(skinPair
    .resId);
    if (background instanceof Integer) {
    ((ImageView) view).setImageDrawable(new ColorDrawable((Integer)
    background));
    } else {
    ((ImageView) view).setImageDrawable((Drawable) background);
    }
    break;
    case "textColor":
    ((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList
    (skinPair.resId));
    break;
    case "drawableLeft":
    left = SkinResources.getInstance().getDrawable(skinPair.resId);
    break;
    case "drawableTop":
    top = SkinResources.getInstance().getDrawable(skinPair.resId);
    break;
    case "drawableRight":
    right = SkinResources.getInstance().getDrawable(skinPair.resId);
    break;
    case "drawableBottom":
    bottom = SkinResources.getInstance().getDrawable(skinPair.resId);
    break;
    default:
    break;
    }
    if (null != left || null != right || null != top || null != bottom) {
    ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right,
    bottom);
    }
    }
    }
    private void applySkinSupport() {
    if (view instanceof SkinViewSupport) {
    ((SkinViewSupport) view).applySkin();
    }
    }
    }

    static class SkinPair {
    //属性名
    String attributeName;
    //对应的资源id
    int resId;

    public SkinPair(String attributeName, int resId) {
    this.attributeName = attributeName;
    this.resId = resId;
    }
    }
    }

主题工具类

负责根据属性id获得theme中对应资源id的值,刷新状态栏颜色为皮肤包中定义的颜色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class SkinThemeUtils {

private static int[] APPCOMPAT_COLOR_PRIMARY_DARK_ATTRS = {
android.support.v7.appcompat.R.attr.colorPrimaryDark
};
private static int[] STATUSBAR_COLOR_ATTRS = {android.R.attr.statusBarColor, android.R.attr
.navigationBarColor
};


/**
* 获得theme中的属性中定义的 资源id
* @param context
* @param attrs
* @return
*/
public static int[] getResId(Context context, int[] attrs) {
int[] resIds = new int[attrs.length];
TypedArray a = context.obtainStyledAttributes(attrs);
for (int i = 0; i < attrs.length; i++) {
resIds[i] = a.getResourceId(i, 0);
}
a.recycle();
return resIds;
}



public static void updateStatusBarColor(Activity activity) {
//5.0以上才能修改
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return;
}
//获得 statusBarColor 与 nanavigationBarColor (状态栏颜色)
//当与 colorPrimaryDark 不同时 以statusBarColor为准
int[] resIds = getResId(activity, STATUSBAR_COLOR_ATTRS);
int statusBarColorResId = resIds[0];
int navigationBarColor = resIds[1];

//如果获取到状态栏颜色资源id,那么设置状态栏颜色
if (statusBarColorResId != 0) {
int color = SkinResources.getInstance().getColor(statusBarColorResId);
activity.getWindow().setStatusBarColor(color);
} else {
//获得主色资源id
int colorPrimaryDarkResId = getResId(activity, APPCOMPAT_COLOR_PRIMARY_DARK_ATTRS)[0];
// 如果获取到主色资源id,那么设置状态栏颜色
if (colorPrimaryDarkResId != 0) {
int color = SkinResources.getInstance().getColor(colorPrimaryDarkResId);
activity.getWindow().setStatusBarColor(color);
}
}
// 如果获取到导航栏资源id,那么设置导航栏颜色
if (navigationBarColor != 0) {
int color = SkinResources.getInstance().getColor
(navigationBarColor);
activity.getWindow().setNavigationBarColor(color);

}
}

}