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

前言

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

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

亮度条

kotlin 复制代码
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变量存下来了

kotlin 复制代码
/**
 * 绘制背景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,这种写法还是麻烦了些,后面有时间优化一下;

前景也类似

kotlin 复制代码
canvas?.drawRoundRect(mLightnessBarRectF, mCornerRadius, mCornerRadius, mPaint)

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

kotlin 复制代码
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

相关推荐
一航jason1 小时前
Android Jetpack Compose 现有Java老项目集成使用compose开发
android·java·android jetpack
猿小蔡-Cool1 小时前
Android 中的 Zygote 和 Copy-on-Write 机制详解
android·zygote
顾北川_野1 小时前
android 默认关闭增强型4GLTE开关;去掉VT视频通话功能及菜单
android·开发语言
海绵波波1072 小时前
聊天服务器(7)数据模块
android·服务器·adb
软件聚导航2 小时前
uniapp 实现 ble蓝牙同时连接多台蓝牙设备,支持app、苹果(ios)和安卓手机,以及ios连接蓝牙后的一些坑
android·ios·uni-app
深海呐7 小时前
Android 最新的AndroidStudio引入依赖失败如何解决?如:Failed to resolve:xxxx
android·failed to res·failed to·failed to resol·failed to reso
解压专家6667 小时前
安卓解压软件推荐:高效处理压缩文件的实用工具
android·智能手机·winrar·7-zip
Rverdoser7 小时前
Android 老项目适配 Compose 混合开发
android
️ 邪神9 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】标题栏
android·flutter·ios·鸿蒙·reatnative
努力遇见美好的生活10 小时前
Mysql每日一题(行程与用户,困难※)
android·数据库·mysql