前言
昨天凌晨在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。