-
-
Save chaosgoo/eed6cda2ed7c166defee22991601c1fd to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package io.serialflow.editor.ui | |
| import android.content.Context | |
| import android.graphics.* | |
| import android.util.AttributeSet | |
| import android.view.Gravity | |
| import android.widget.FrameLayout | |
| import kotlin.math.PI | |
| import kotlin.math.cos | |
| import kotlin.math.roundToInt | |
| import kotlin.math.sin | |
| import io.serialflow.editor.R | |
| /** | |
| * 这是一个容器组件 | |
| * 具有类似于LVGL的Border属性的FrameLayout | |
| */ | |
| class BorderFrameLayout : FrameLayout { | |
| /** | |
| * [DEFAULT_CHILD_GRAVITY] copy from [FrameLayout] | |
| */ | |
| private val DEFAULT_CHILD_GRAVITY = Gravity.TOP or Gravity.START | |
| // 圆角部分是否会将内容裁剪 | |
| protected var clipContent: Boolean = true | |
| /** | |
| * 背景Path | |
| * 为了实现圆角效果而创建的辅助Path, 是整个控件的轮廓 | |
| * 同时也是Border的外轮廓,和[mBorderInnerContourPath]包围区域构成轮廓 | |
| */ | |
| private var mBackgroundPath = Path() | |
| /** | |
| * 实际Border的内边界, 此范围内为内部child允许摆放范围 | |
| */ | |
| private var mBorderInnerContourPath = Path() | |
| // 圆角半径, 轮廓的四个圆角半径必须相同 | |
| private var mBorderRadius = 0f | |
| // 描述Border宽度,LVGL中四边Border只要存在,他们的宽度就相等. | |
| private var mBorderWidth: Float = 0f | |
| // border尺寸 + LVGL中Padding尺寸 = Android中Padding | |
| // border尺寸 | |
| private var mBorderLeft: Float = 0f | |
| private var mBorderTop: Float = 0f | |
| private var mBorderRight: Float = 0f | |
| private var mBorderBottom: Float = 0f | |
| // 真实的Padding | |
| private var mRealPaddingLeft: Float = 0f | |
| private var mRealPaddingTop: Float = 0f | |
| private var mRealPaddingRight: Float = 0f | |
| private var mRealPaddingBottom: Float = 0f | |
| // border画笔 | |
| private val mBorderPaint = Paint().apply { | |
| isAntiAlias = true | |
| style = Paint.Style.FILL | |
| } | |
| // border颜色 | |
| private var mBorderColor: Int = Color.TRANSPARENT | |
| // border风格, 1:"dash" 虚线 0: "solid" 实心 | |
| private var mBorderStyle: Int = 0 | |
| // 背景色 | |
| private var mBackgroundColor: Int = Color.TRANSPARENT | |
| // 渐变角度 | |
| private var mBackgroundGradientAngle = 0f | |
| // 渐变色 | |
| private var mBackgroundGradientColor = Color.TRANSPARENT | |
| // 用于处理轮廓的Path | |
| private val counterPath = Path() | |
| constructor(context: Context) : this(context, null) | |
| constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) | |
| constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super( | |
| context, attrs, defStyle | |
| ) { | |
| val typedArray = context.obtainStyledAttributes(attrs, R.styleable.BorderFrameLayout, defStyle, 0) | |
| mBorderRadius = typedArray.getDimension(R.styleable.BorderFrameLayout_borderRadius, 0f) | |
| mBorderColor = typedArray.getColor(R.styleable.BorderFrameLayout_borderColor, Color.TRANSPARENT) | |
| if (mBorderColor != Color.TRANSPARENT) { | |
| mBorderColor = mBorderColor or 0xff000000.toInt() | |
| } | |
| mBorderStyle = typedArray.getInt(R.styleable.BorderFrameLayout_borderStyle, 0) | |
| mBackgroundColor = typedArray.getColor(R.styleable.BorderFrameLayout_backgroundColor, Color.TRANSPARENT) | |
| mBackgroundGradientAngle = typedArray.getFloat(R.styleable.BorderFrameLayout_backgroundGradientAngle, 0f) | |
| mBackgroundGradientColor = typedArray.getColor(R.styleable.BorderFrameLayout_backgroundGradientColor, Color.TRANSPARENT) | |
| clipContent = typedArray.getBoolean(R.styleable.BorderFrameLayout_clipContent, true) | |
| mRealPaddingLeft = typedArray.getDimension(R.styleable.BorderFrameLayout_realPaddingLeft, 0f) | |
| mRealPaddingTop = typedArray.getDimension(R.styleable.BorderFrameLayout_realPaddingTop, 0f) | |
| mRealPaddingRight = typedArray.getDimension(R.styleable.BorderFrameLayout_realPaddingRight, 0f) | |
| mRealPaddingBottom = typedArray.getDimension(R.styleable.BorderFrameLayout_realPaddingBottom, 0f) | |
| val borderWidth = typedArray.getDimension(R.styleable.BorderFrameLayout_borderWidth, 0f) | |
| val borderLeft = typedArray.getDimension(R.styleable.BorderFrameLayout_borderLeft, borderWidth) | |
| val borderTop = typedArray.getDimension(R.styleable.BorderFrameLayout_borderTop, borderWidth) | |
| val borderRight = typedArray.getDimension(R.styleable.BorderFrameLayout_borderRight, borderWidth) | |
| val borderBottom = typedArray.getDimension(R.styleable.BorderFrameLayout_borderBottom, borderWidth) | |
| typedArray.recycle() | |
| // Initialize with XML values | |
| setBorderWidth(borderLeft, borderTop, borderRight, borderBottom) | |
| } | |
| override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { | |
| // layoutChildren 内部通过 getPaddingXXXWithForeground() 已经处理了 border | |
| // 所以这里直接传递原始边界,避免 border 被重复计算 | |
| layoutChildren(left, top, right, bottom) | |
| } | |
| /** | |
| * super.onLayout会无视自定义的Border | |
| * | |
| * 所以修改FrameLayout原始的layoutChildren代码,补充border的计算逻辑 | |
| * **[layoutChildren] 内容和android-33\android\widget\FrameLayout.java没有区别** | |
| * 只是被自动转换成kotlin语法 | |
| * 让考虑border的关键在于 | |
| * * [getPaddingLeftWithForeground] | |
| * * [getPaddingTopWithForeground] | |
| * * [getPaddingRightWithForeground] | |
| * * [getPaddingBottomWithForeground] | |
| * | |
| **/ | |
| private fun layoutChildren(left: Int, top: Int, right: Int, bottom: Int) { | |
| val count = childCount | |
| val parentLeft: Int = getPaddingLeftWithForeground() | |
| val parentRight: Int = right - left - getPaddingRightWithForeground() | |
| val parentTop: Int = getPaddingTopWithForeground() | |
| val parentBottom: Int = bottom - top - getPaddingBottomWithForeground() | |
| for (i in 0 until count) { | |
| val child = getChildAt(i) | |
| if (child.visibility != GONE) { | |
| val lp = child.layoutParams as LayoutParams | |
| val width = child.measuredWidth | |
| val height = child.measuredHeight | |
| var childLeft: Int | |
| var childTop: Int | |
| var gravity = lp.gravity | |
| if (gravity == -1) { | |
| gravity = DEFAULT_CHILD_GRAVITY | |
| } | |
| val layoutDirection = layoutDirection | |
| val absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection) | |
| val verticalGravity = gravity and Gravity.VERTICAL_GRAVITY_MASK | |
| childLeft = when (absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK) { | |
| Gravity.CENTER_HORIZONTAL -> parentLeft + (parentRight - parentLeft - width) / 2 + | |
| lp.leftMargin - lp.rightMargin | |
| Gravity.RIGHT -> { | |
| parentRight - width - lp.rightMargin | |
| } | |
| Gravity.LEFT -> parentLeft + lp.leftMargin | |
| else -> parentLeft + lp.leftMargin | |
| } | |
| childTop = when (verticalGravity) { | |
| Gravity.TOP -> parentTop + lp.topMargin | |
| Gravity.CENTER_VERTICAL -> parentTop + (parentBottom - parentTop - height) / 2 + | |
| lp.topMargin - lp.bottomMargin | |
| Gravity.BOTTOM -> parentBottom - height - lp.bottomMargin | |
| else -> parentTop + lp.topMargin | |
| } | |
| child.layout(childLeft, childTop, childLeft + width, childTop + height) | |
| } | |
| } | |
| } | |
| override fun dispatchDraw(canvas: Canvas) { | |
| drawBorderAndBackground(canvas) | |
| // 储存画布,接下来可能会裁切 | |
| canvas.save() | |
| // FIXME clip会导致边缘存在锯齿 | |
| // 子控件的paint是他自己的,所以这里设置xfermode没用,暂时不知道如何处理得到更好的抗锯齿效果 | |
| if (clipContent) { | |
| // 确定边缘 | |
| canvas.clipPath(mBackgroundPath) | |
| // 确定允许绘制的区域,这样就不会覆盖Border了 | |
| canvas.clipPath(generateChildDrawAreaPath()) | |
| } | |
| // 绘制内部控件 | |
| super.dispatchDraw(canvas) | |
| // 还原裁切 | |
| canvas.restore() | |
| } | |
| fun setCoordPad(left: Float, top: Float, right: Float, bottom: Float) { | |
| mRealPaddingLeft = left | |
| mRealPaddingTop = top | |
| mRealPaddingRight = right | |
| mRealPaddingBottom = bottom | |
| setPadding( | |
| (mRealPaddingLeft + mBorderLeft).roundToInt(), | |
| (mRealPaddingTop + mBorderTop).roundToInt(), | |
| (mRealPaddingRight + mBorderRight).roundToInt(), | |
| (mRealPaddingBottom + mBorderBottom).roundToInt() | |
| ) | |
| } | |
| fun setBorderRadius(radius: Float) { | |
| // 半径不能比短边的一半还要大 | |
| val maxR = minOf(height, width) / 2f | |
| mBorderRadius = if (radius > minOf(height, width) / 2f) maxR else radius | |
| } | |
| fun setBorderStyle(style: Int) { | |
| mBorderStyle = style | |
| } | |
| fun setBorderColor(color: Int) { | |
| mBorderColor = color or 0xff000000.toInt() | |
| } | |
| fun setBorderWidth(width: Float) { | |
| setBorderWidth(width, width, width, width) | |
| } | |
| fun setBorderWidth( | |
| leftWidth: Float, | |
| topWidth: Float, | |
| rightWidth: Float, | |
| bottomWidth: Float | |
| ) { | |
| // border取最大的 | |
| mBorderWidth = maxOf(leftWidth, topWidth, rightWidth, bottomWidth) | |
| // 检查是不是有更小的border并且矫正 | |
| setPadding( | |
| (if (leftWidth != 0f && leftWidth != mBorderWidth) { | |
| mBorderLeft = mBorderWidth | |
| // 矫正leftBorder非0时候不等于最大Border宽度情况 | |
| mBorderWidth | |
| } else { | |
| mBorderLeft = leftWidth | |
| leftWidth | |
| } + mRealPaddingLeft).roundToInt(), | |
| (if (topWidth != 0f && topWidth != mBorderWidth) { | |
| mBorderTop = mBorderWidth | |
| // 矫正topBorder非0时候不等于最大Border宽度情况 | |
| mBorderWidth | |
| } else { | |
| mBorderTop = topWidth | |
| topWidth | |
| } + mRealPaddingTop).roundToInt(), | |
| (if (rightWidth != 0f && rightWidth != mBorderWidth) { | |
| mBorderRight = mBorderWidth | |
| // 矫正rightBorder非0时候不等于最大Border宽度情况 | |
| mBorderWidth | |
| } else { | |
| mBorderRight = rightWidth | |
| rightWidth | |
| } + mRealPaddingRight).roundToInt(), | |
| (if (bottomWidth != 0f && bottomWidth != mBorderWidth) { | |
| mBorderBottom = mBorderWidth | |
| // 矫正bottomBorder非0时候不等于最大Border宽度情况 | |
| mBorderWidth | |
| } else { | |
| mBorderBottom = bottomWidth | |
| bottomWidth | |
| } + mRealPaddingBottom).roundToInt() | |
| ) | |
| } | |
| fun setBackgroundGradColor(color: Int) { | |
| // 容器的背景不能为透明 | |
| mBackgroundGradientColor = color | |
| } | |
| fun setBackgroundGradAngle(angle: Float) { | |
| mBackgroundGradientAngle = angle | |
| } | |
| override fun setBackgroundColor(color: Int) { | |
| mBackgroundColor = color | |
| } | |
| /** | |
| * 绘制背景和轮廓 | |
| * @param canvas 布局的canvas | |
| */ | |
| private fun drawBorderAndBackground(canvas: Canvas) { | |
| /** | |
| * [mBackgroundPath]整个控件的边界 | |
| */ | |
| generateBackgroundContour(mBackgroundPath) | |
| /** | |
| * 1.绘制背景 | |
| */ | |
| canvas.drawPath(mBackgroundPath, mBorderPaint.apply { | |
| // 处理渐变色,计算渐变色方向 | |
| if (mBackgroundGradientColor != Color.TRANSPARENT) { | |
| val xDir = width / 2f * sin(mBackgroundGradientAngle * PI / 180f).toFloat() | |
| val yDir = height / 2f * cos(mBackgroundGradientAngle * PI / 180f).toFloat() | |
| this.shader = LinearGradient( | |
| width / 2f, | |
| height / 2f, | |
| width / 2f - xDir, | |
| height / 2f - yDir, | |
| mBackgroundColor, | |
| mBackgroundGradientColor, | |
| Shader.TileMode.MIRROR | |
| ) | |
| } | |
| this.color = mBackgroundColor | |
| this.style = Paint.Style.FILL | |
| this.strokeWidth = mBorderWidth | |
| }) | |
| // clear shader | |
| mBorderPaint.shader = null | |
| // 限制绘制区域为背景轮廓内,用xfermode会导致滚动的时候,绘制的裁切失效.所以这里还是用clip来做 | |
| canvas.save() | |
| canvas.clipPath(mBackgroundPath, Region.Op.INTERSECT) | |
| // 还原clip | |
| canvas.restore() | |
| // 没宽度说明没有border,提前结束 | |
| if (mBorderWidth == 0f) { | |
| return | |
| } | |
| /** | |
| * 2.绘制Border | |
| * 由于Android的PorterDuff.Mode不能像canvas的clip一样进行多次合成计算,所以这里需要创建一个辅助图层 | |
| * [maskBitmap]是border的Mask层 | |
| * [maskCanvas]是为了绘制Mask层而创建的Canvas | |
| */ | |
| /*=================== 生成Mask =================== */ | |
| val maskBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ALPHA_8) | |
| val maskCanvas = Canvas(maskBitmap) | |
| // 由于这个Canvas本身就是"干净"的,所以无需save操作 | |
| // 先绘制出整个控件的背景轮廓, | |
| maskCanvas.drawPath(mBackgroundPath, mBorderPaint.apply { | |
| style = Paint.Style.FILL | |
| // 由于背景可能是透明的,透明的画笔会影响mask的绘制 | |
| // 所以这里手动在mask绘制的时候设置画笔颜色.只要不是透明的就行 | |
| color = Color.BLACK | |
| }) | |
| // 设置paint模式为[PorterDuff.Mode.DST_OUT],代表以原始内容为基础 | |
| // 接下来的绘制得到保留背景和轮廓不重合的部分遮罩 | |
| mBorderPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) | |
| maskCanvas.drawPath(generateChildDrawAreaPath(), mBorderPaint.apply { | |
| color = Color.WHITE | |
| }) | |
| // 根据轮的类型, 再次处理这一层遮罩范围 | |
| // 如果是虚线类型边框, 则绘制虚线线段相反的部分 | |
| // --- --- --- --- --- 这是虚线 | |
| // - - - - 这是虚线线段相反的部分 | |
| if (mBorderStyle == 1) { | |
| counterPath.reset() | |
| val offset = mBorderWidth / 2f | |
| val maxDashIntervals = mBorderWidth | |
| counterPath.addRoundRect( | |
| 0f + offset, | |
| 0f + offset, | |
| width.toFloat() - offset, | |
| height.toFloat() - offset, | |
| mBorderRadius - offset, | |
| mBorderRadius - offset, | |
| Path.Direction.CCW | |
| ) | |
| maskCanvas.drawPath(counterPath, | |
| mBorderPaint.apply { | |
| style = Paint.Style.STROKE | |
| this.strokeWidth = mBorderWidth | |
| pathEffect = DashPathEffect( | |
| floatArrayOf(maxDashIntervals, maxDashIntervals * 3), | |
| 0f | |
| ) | |
| this.color = Color.BLACK | |
| }) | |
| } | |
| // 还原画笔模式 | |
| mBorderPaint.xfermode = null | |
| // 这个时候就得到了Border的MASK.利用这个mask就可以限制Border的绘制范围了 | |
| /*=================== 绘制Border =================== */ | |
| // 现在回到原始Canvas上,利用刚才的mask绘制Border | |
| // 开辟一个新的Layer | |
| val originCanvasLayer = canvas.saveLayer( | |
| 0f, | |
| 0f, | |
| width.toFloat(), | |
| height.toFloat(), | |
| null, | |
| ) | |
| // 把mask绘制到新的Layer上 | |
| canvas.drawBitmap(maskBitmap, 0f, 0f, mBorderPaint) | |
| // 设置模式为[PorterDuff.Mode.SRC_IN],代表接下来的绘制只可以在mask的范围内绘制 | |
| mBorderPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) | |
| // 由于mask的存在,直接填充就行了. mask已经处理好虚线和实线的可绘制区域范围 | |
| canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), mBorderPaint.apply { | |
| style = Paint.Style.FILL | |
| this.color = mBorderColor | |
| }) | |
| // 标记释放Mask | |
| maskBitmap.recycle() | |
| // 还原xfermode | |
| mBorderPaint.xfermode = null | |
| // 还原图层 | |
| canvas.restoreToCount(originCanvasLayer) | |
| } | |
| /** | |
| * 生成背景轮廓,等价于border的外边界 | |
| * 主要用于绘制背景及其圆角 | |
| */ | |
| private fun generateBackgroundContour(backgroundPath: Path) { | |
| // 重置Path | |
| backgroundPath.reset() | |
| // 从左上角,顺时针开始添加 | |
| backgroundPath.addRoundRect( | |
| 0f, | |
| 0f, | |
| width.toFloat(), | |
| height.toFloat(), | |
| mBorderRadius, | |
| mBorderRadius, | |
| Path.Direction.CCW | |
| ) | |
| } | |
| /** | |
| * 此处计算和[UiParamsBox]里面不同,额外处理了mRealPaddingXXX | |
| * 因为[BorderFrameLayout]既存在Border又存在Padding, | |
| * 而[UiParamsBox]把自己的Padding假装成Border,Padding部分交给了内部的View | |
| */ | |
| private fun generateChildDrawAreaPath(): Path { | |
| val r = RectF( | |
| if (mBorderLeft == 0f) -mBorderWidth.toFloat() else paddingLeft.toFloat() - mRealPaddingLeft, | |
| if (mBorderTop == 0f) -mBorderWidth.toFloat() else paddingTop.toFloat() - mRealPaddingTop, | |
| if (mBorderRight == 0f) width.toFloat() + mBorderWidth else width.toFloat() - paddingRight + mRealPaddingRight, | |
| if (mBorderBottom == 0f) height.toFloat() + mBorderWidth else height.toFloat() - paddingBottom + mRealPaddingBottom, | |
| ) | |
| return Path().apply { | |
| addRoundRect( | |
| r, | |
| maxOf(mBorderRadius - mBorderWidth, 0f).toFloat(), | |
| maxOf(mBorderRadius - mBorderWidth, 0f).toFloat(), | |
| Path.Direction.CCW | |
| ) | |
| } | |
| } | |
| /** | |
| * 对于BoxComponent来说,他的Padding是积木系统中Padding部分加上Border部分 | |
| */ | |
| override fun getPaddingLeft(): Int { | |
| return (mRealPaddingLeft + mBorderLeft).roundToInt() | |
| } | |
| /** | |
| * 对于BoxComponent来说,他的Padding是积木系统中Padding部分加上Border部分 | |
| */ | |
| override fun getPaddingRight(): Int { | |
| return (mRealPaddingRight + mBorderRight).roundToInt() | |
| } | |
| /** | |
| * 对于BoxComponent来说,他的Padding是积木系统中Padding部分加上Border部分 | |
| */ | |
| override fun getPaddingTop(): Int { | |
| return (mRealPaddingTop + mBorderTop).roundToInt() | |
| } | |
| /** | |
| * 对于BoxComponent来说,他的Padding是积木系统中Padding部分加上Border部分 | |
| */ | |
| override fun getPaddingBottom(): Int { | |
| return (mRealPaddingBottom + mBorderBottom).roundToInt() | |
| } | |
| /** | |
| * 修改了返回的值,函数名是和FrameLayout在onLayoutChild内部调用的保持统一 | |
| */ | |
| private fun getPaddingLeftWithForeground(): Int { | |
| return paddingLeft | |
| } | |
| /** | |
| * 修改了返回的值,函数名是和FrameLayout在onLayoutChild内部调用的保持统一 | |
| */ | |
| private fun getPaddingRightWithForeground(): Int { | |
| return paddingRight | |
| } | |
| /** | |
| * 修改了返回的值,函数名是和FrameLayout在onLayoutChild内部调用的保持统一 | |
| */ | |
| private fun getPaddingTopWithForeground(): Int { | |
| return paddingTop | |
| } | |
| /** | |
| * 修改了返回的值,函数名是和FrameLayout在onLayoutChild内部调用的保持统一 | |
| */ | |
| private fun getPaddingBottomWithForeground(): Int { | |
| return paddingBottom | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment