联系我们
简单又实用的WordPress网站制作教学
当前位置:网站首页 > 程序开发学习 > 正文

Android自定义View(二)——亮度条

作者:访客发布时间:2023-12-25分类:程序开发学习浏览:70


导读:前言昨天凌晨在B站抽盲盒抽上瘾,花了六百多块,就中了一个九十多块的mikufufu,还不是世嘉版的,标注的8%概率,假得很。前天老板问我什么时候能做完,答下周五,毕竟不能留着...

前言

昨天凌晨在B站抽盲盒抽上瘾,花了六百多块,就中了一个九十多块的miku fufu,还不是世嘉版的,标注的8%概率,假得很。

前天老板问我什么时候能做完,答下周五,毕竟不能留着跨年,然后他让我下周三之前做完,周末又不想跑这么远去公司,没办法,只能把项目copy回家了(没有远程代码仓库,就我一个android开发的小公司,保密协议现在都没给我签...)

亮度条

class LightnessBar @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
): View(context, attributeSet, defStyleAttr, defStyleRes) {
    companion object {
        private const val TAG = "LightnessBar"
    }

    /**
     * 前景画笔,默认绘制白色的条
     */
    private val mPaint: Paint = Paint()

    /**
     * 背景RectF,负责描述背景条的位置信息
     */
    private val mBackgroundRectF = RectF()

    /**
     * 亮度条RectF,负责描述亮度条的位置信息
     */
    private val mLightnessBarRectF = RectF()

    /**
     * 背景和前景条的切角
     */
    private val mCornerRadius: Float

    /**
     * 当前亮度
     */
    private var mCurrentBrightness: Int

    /**
     * 仿seekbar做个maxWidth
     */
    private val mMaxWidth: Int

    /**
     * 缓存背景bitmap
     */
    private var mBackgroundBitmap: Bitmap? = null

    /**
     * draw()不宜新建对象
     */
    private val mMode = PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP)

    init {
        context.theme.obtainStyledAttributes(
            attributeSet,
            R.styleable.LightBar,
            defStyleAttr, defStyleRes
        ).apply {
            mCornerRadius = getDimension(R.styleable.LightBar_cornerRadius, 0f)

            val contentResolver = context.contentResolver
            val mode = Settings.System.getInt(contentResolver, Settings.System.SCREEN_BRIGHTNESS_MODE)
            if (mode == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC) {
                Settings.System.putInt(contentResolver, Settings.System.SCREEN_BRIGHTNESS_MODE,
                    Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL)
            }

            mCurrentBrightness = Settings.System.getInt(contentResolver, Settings.System.SCREEN_BRIGHTNESS)
            mMaxWidth = getDimensionPixelOffset(R.styleable.LightBar_maxWidth, DensityUtils.dip2px(40))
            recycle()
        }
        initPaint()
    }

    private fun initPaint() {
        mPaint.style = Paint.Style.FILL
        mPaint.color = Color.parseColor("#F2FFFFFF")
        mPaint.isAntiAlias = true
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val paddingVertical = (measuredWidth - min(measuredWidth, mMaxWidth)) * 0.5f
        mBackgroundRectF.top = 0f
        mBackgroundRectF.left = paddingVertical
        mBackgroundRectF.right = measuredWidth.toFloat() - paddingVertical
        mBackgroundRectF.bottom = measuredHeight.toFloat()
        mBackgroundBitmap = makeBackground(measuredWidth, measuredHeight)

        mLightnessBarRectF.bottom = mBackgroundRectF.bottom
        mLightnessBarRectF.left = mBackgroundRectF.left
        mLightnessBarRectF.right = mBackgroundRectF.right
        mLightnessBarRectF.top = measuredHeight * (1 - mCurrentBrightness / 255f)
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        if (canvas == null) return
        val sc = canvas.saveLayer(mBackgroundRectF, mPaint)
        drawBackground(canvas)
        mPaint.xfermode = mMode
        drawForeground(canvas)
        mPaint.xfermode = null
        canvas.restoreToCount(sc)
    }

    private fun drawBackground(canvas: Canvas?) {
        if (mBackgroundBitmap != null) {
            canvas?.drawBitmap(mBackgroundBitmap!!, 0f, 0f, mPaint)
        }
    }

    private fun drawForeground(canvas: Canvas?) {
        canvas?.drawRoundRect(mLightnessBarRectF, mCornerRadius, mCornerRadius, mPaint)
    }

    /**
     * 记录上一次亮度变化时的纵坐标y
     */
    private var mLastLightnessY = 0f

    /**
     * 记录指针移动时动态变化的纵坐标y
     */
    private var mPointerMoveY: Float = 0f

    override fun onTouchEvent(event: MotionEvent?): Boolean {
//        LogUtil.i(TAG, "onTouchEvent: $event")
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                mPointerMoveY = event.y
                mLastLightnessY = event.y
                parent.requestDisallowInterceptTouchEvent(true)
            }
            MotionEvent.ACTION_MOVE -> trackTouchEvent(event)
            MotionEvent.ACTION_UP -> {
                parent.requestDisallowInterceptTouchEvent(false)
            }
            MotionEvent.ACTION_CANCEL -> {
                parent.requestDisallowInterceptTouchEvent(false)
            }
        }
        return true
    }

    private fun trackTouchEvent(event: MotionEvent) {
        val newTop = mLightnessBarRectF.top - (mPointerMoveY - event.y)
        if (mPointerMoveY == event.y) {
            LogUtil.e(TAG, "mPointerMoveY: $mPointerMoveY event.y: ${event.y}")
        }
        mPointerMoveY = event.y

        mLightnessBarRectF.top = when {
            newTop <= mBackgroundRectF.top -> {
                if (mCurrentBrightness >= 255) return
                mBackgroundRectF.top
            }
            newTop >= mLightnessBarRectF.bottom -> {
                if (mCurrentBrightness <= 0) return
                mLightnessBarRectF.bottom
            }
            else -> newTop
        }

        LogUtil.i(TAG, """top: ${mLightnessBarRectF.top} newTop: $newTop""")
        if (mCurrentBrightness > 255 || mCurrentBrightness < 0) {
            LogUtil.i(TAG, """mCurrentBrightness: $mCurrentBrightness""")
        }

        val newLightness: Int = mCurrentBrightness + (((mLastLightnessY - mPointerMoveY) / measuredHeight) * 255).toInt()

        if (newLightness < mCurrentBrightness && mCurrentBrightness > 0) {
            mCurrentBrightness = if (newLightness > 0) {
                newLightness
            } else {
                0
            }
            adjustLightness()
        }

        if (newLightness > mCurrentBrightness && mCurrentBrightness < 255) {
            mCurrentBrightness = if (newLightness < 255) {
                newLightness
            } else {
                255
            }
            adjustLightness()
        }

        invalidate()
    }

    /**
     * 限制每次调节系统亮度的协程数量
     */
    private val mMutex = Mutex()

    /**
     * 调节亮度
     */
    private fun adjustLightness() {
        mLastLightnessY = mPointerMoveY
        CoroutineScope(Dispatchers.IO).launch {
            mMutex.tryLock()
            if (mCurrentBrightness in 0..255) {
                Settings.System.putInt(
                    context.contentResolver,
                    Settings.System.SCREEN_BRIGHTNESS,
                    mCurrentBrightness
                )
            } else {
                throw IllegalArgumentException("mCurrentBrightness超出了界限")
            }
            mMutex.unlock()
        }
    }

    /**
     * 绘制背景bitmap
     */
    private fun makeBackground(w: Int, h: Int): Bitmap {
        val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        val paint = Paint(Paint.ANTI_ALIAS_FLAG)
        paint.color = Color.parseColor("#CC050603")
        canvas.drawRoundRect(mBackgroundRectF, mCornerRadius, mCornerRadius, paint)
        ContextCompat.getDrawable(context, R.drawable.icon_light_mode)?.let {
            it.setBounds(
                (DensityUtils.dip2px(10) + mBackgroundRectF.left).toInt(),
                (mBackgroundRectF.bottom - DensityUtils.dip2px(32)).toInt(),
                (mBackgroundRectF.right - DensityUtils.dip2px(10)).toInt(),
                (mBackgroundRectF.bottom - DensityUtils.dip2px(12)).toInt()
            )
            it.draw(canvas)
        }
        return bitmap
    }
}

详解

就像手机上的亮度条、音量条一样,看起来是两个叠加上去的(大佬或许可以一次绘制完毕),背景我使用bitmap变量存下来了

/**
 * 绘制背景bitmap
 */
private fun makeBackground(w: Int, h: Int): Bitmap {
    val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)
    val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    paint.color = Color.parseColor("#CC050603")
    canvas.drawRoundRect(mBackgroundRectF, mCornerRadius, mCornerRadius, paint)
    ContextCompat.getDrawable(context, R.drawable.icon_light_mode)?.let {
        it.setBounds(
            (DensityUtils.dip2px(10) + mBackgroundRectF.left).toInt(),
            (mBackgroundRectF.bottom - DensityUtils.dip2px(32)).toInt(),
            (mBackgroundRectF.right - DensityUtils.dip2px(10)).toInt(),
            (mBackgroundRectF.bottom - DensityUtils.dip2px(12)).toInt()
        )
        it.draw(canvas)
    }
    return bitmap
}

上面的R.drawable.icon_light_mode是我找的小太阳图标,标注为亮度条;DensityUtils是我的工具类,用来做屏幕适配和在代码中使用dp,这种写法还是麻烦了些,后面有时间优化一下;

前景也类似

canvas?.drawRoundRect(mLightnessBarRectF, mCornerRadius, mCornerRadius, mPaint)

但是要注意的是,我们对于前景表示进度的矩形条可能有不同的需求,比如上平下圆、上圆下圆等;这里我的需求是上圆下圆,但是也可以很简单过渡到上平下圆,关键代码就是这里:

override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)

    if (canvas == null) return
    val sc = canvas.saveLayer(mBackgroundRectF, mPaint)
    drawBackground(canvas)
    mPaint.xfermode = mMode
    drawForeground(canvas)
    mPaint.xfermode = null
    canvas.restoreToCount(sc)
}

这里我的xfermode使用的是SRC_ATOP,这方面不懂的可以搜索相关文章,但是可以参考这里:android.googlesource.com/platform/de…

要注意val sc = canvas.saveLayer(mBackgroundRectF, mPaint)和canvas.restoreToCount(sc),如果没有这两行代码,就很有问题,xfermode会出现各种各样的故障。

结尾

这里我没解耦,没空,在家办公ing。

Android自定义View(一)——竖向SeekBar


标签:自定义亮度安卓系统View


程序开发学习排行
最近发表
网站分类
标签列表