Out of the box source address
Onion math with ShadowLayout -ShadowLayout
Support for custom attributes:
sl_shadowRadius
: Shadow divergence distancesl_shadowColor
: Shadow colorsl_dx
: Left and right shadow offsetsl_dy
: The upper and lower offset of the shadowsl_cornerRadius
: Rounded cornerssl_borderWidth
: Layout border widthsl_borderColor
: Layout border colorsl_shadowSides
: Displays shadows on one or more edges
The origin of
In recent months, our painters (design masters) began to use more and more shadows, so they could no longer use the.9. PNG implementation mode, and then we have the ShadowLayout of this encapsulation, whose main features are:
- Extract custom Layout properties for users to quickly start
- UI performance exquisite, high degree of reduction
However, there is still a drawback that can not be avoided, that is, the shaded area occupies the Layout Padding area, which requires the user to calculate the Layout width and height in his head, although the calculation is very simple.
The first picture is that this Demo shows three scenarios, and then combined with the partial UI draft, you can compare them.
Thinking analysis
Let’s consider the key implementation points:
- In order to
write once use everywhere
Let’s just write a layout, so we can wrap whatever we want, so define an inheritance, okayFrameLayout
The layout of the nameShadowLayout
- The core is implementation
shadows
, check the information to know that it can be usedPaint
thesetShadowLayer()
API - Fillet processing we can use
xfermode
Apply one to the child View on the canvasGo round the corners - Border handling is easy to use
Canvas
thedrawRoundRect
Painting can be - Controls the display of shadows on one or more edges, using custom attributes
flags
Type implementation (exactly what we need)
Frame of thought:
- Define and initialize properties
- Set the padding to leave space for the shadow
- Draw a shadow of the size of the content area (Content area == subview area == layout-padding)
- Draw the content area and handle rounded corners
- Draw the border
After sorting out the technical points and ideas, I started the code, which still has some details. Go Ahead!
Drawing process
NOTE: Since we often need to customize views, we have extracted common tool methods using Kotlin’s extension method in drawutil.kt file.
MPaint. UtilReset (), for example, is an extended method, not an API of the Paint class.
Drawutil.kt
1. Define and initialize attributes
The first step is to do the basics: define our attributes in attrs.xml, declare variables in Layout, and initialize them.
Sl_shadowSides sl_shadowSides
- Its type is
flags
And plural, so this can be used to set multiple flag bits for a property - This is how it is used in AN XML layout
app:sl_shadowSides="TOP|RIGHT|BOTTOM"
Through the| (logical or)
Join multiple flag bits (this method is often used) - Value specifies the value defined
One, two, four, eight, fifteen
It’s regular, it’s not arbitrary - When used in code
- judge
Flag set
Whether there is aflag
(Used this time) - in
Flag set
Add newflag
- in
Flag set
To remove aflag
- judge
Therefore, the DrawUtil method is extended to facilitate reuse.
I have a link to this part of the theory in the article I read, so you can help yourself.
Post a large wave of initialization related code, as follows:
<?xml version="1.0" encoding="utf-8"? >
<resources>
<declare-styleable name="ShadowLayout">
<attr name="sl_cornerRadius" format="dimension" />
<attr name="sl_shadowRadius" format="dimension" />
<attr name="sl_shadowColor" format="color" />
<attr name="sl_dx" format="dimension" />
<attr name="sl_dy" format="dimension" />
<attr name="sl_borderColor" format="color" />
<attr name="sl_borderWidth" format="dimension" />
<attr name="sl_shadowSides" format="flags">
<flag name="TOP" value="1" />
<flag name="RIGHT" value="2" />
<flag name="BOTTOM" value="4" />
<flag name="LEFT" value="8" />
<flag name="ALL" value="15" />
</attr>
</declare-styleable>
</resources>
Copy the code
/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
//* Custom attributes section
/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/** * Shadow color */
@ColorInt
private var mShadowColor: Int = 0
/** * blur */
private var mShadowRadius: Float = 0f
/** ** x offset distance */
private var mDx: Float = 0f
/**
* y轴偏移距离
*/
private var mDy: Float = 0f
/** * Fillet radius */
private var mCornerRadius: Float = 0f
/** * border color */
@ColorInt
private var mBorderColor: Int = 0
/** * the width of the border */
private var mBorderWidth: Float = 0f
/** * controls whether the edges show shadows */
private var mShadowSides: Int = default_shadowSides
/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
//* Draw the properties section used
/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/** * global brush */
private var mPaint: Paint = createPaint(color = Color.WHITE)
private var mHelpPaint: Paint = createPaint(color = Color.RED)
/** * global Path */
private var mPath = Path()
/** * Compositing mode */
private var mXfermode: PorterDuffXfermode by Delegates.notNull()
/** * RectF instance of view content area */
private var mContentRF: RectF by Delegates.notNull()
/** * RectF instance of view border */
private var mBorderRF: RectF? = null
Copy the code
init {
initAttributes(context, attrs)
initDrawAttributes()
processPadding()
// Set the software rendering type
setLayerType(View.LAYER_TYPE_SOFTWARE, null)}Copy the code
companion object {
const val debug = true
private const val FLAG_SIDES_TOP = 1
private const val FLAG_SIDES_RIGHT = 2
private const val FLAG_SIDES_BOTTOM = 4
private const val FLAG_SIDES_LEFT = 8
private const val FLAG_SIDES_ALL = 15
const val default_shadowColor = Color.BLACK
const val default_shadowRadius = 0f
const val default_dx = 0f
const val default_dy = 0f
const val default_cornerRadius = 0f
const val default_borderColor = Color.RED
const val default_borderWidth = 0f
const val default_shadowSides = FLAG_SIDES_ALL
}
Copy the code
private fun initAttributes(context: Context, attrs: AttributeSet?). {
val a = context.obtainStyledAttributes(attrs, R.styleable.ShadowLayout)
try{ a? .run { mShadowColor = getColor(R.styleable.ShadowLayout_sl_shadowColor, default_shadowColor) mShadowRadius = getDimension(R.styleable.ShadowLayout_sl_shadowRadius, context.dpf2pxf(default_shadowRadius)) mDx = getDimension(R.styleable.ShadowLayout_sl_dx, default_dx) mDy = getDimension(R.styleable.ShadowLayout_sl_dy, default_dy) mCornerRadius = getDimension(R.styleable.ShadowLayout_sl_cornerRadius, context.dpf2pxf(default_cornerRadius)) mBorderColor = getColor(R.styleable.ShadowLayout_sl_borderColor, default_borderColor) mBorderWidth = getDimension(R.styleable.ShadowLayout_sl_borderWidth, context.dpf2pxf(default_borderWidth)) mShadowSides = getInt(R.styleable.ShadowLayout_sl_shadowSides, default_shadowSides) } }finally{ a? .recycle() } }Copy the code
/** * Initializes the attributes associated with drawing */
private fun initDrawAttributes(a) {
// Use XferMode to make the composition on the layer and handle the rounded corners
mXfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
}
Copy the code
2. Set the padding to leave space for shadows
private fun processPadding(a) {
val xPadding = (mShadowRadius + mDx.absoluteValue).toInt()
val yPadding = (mShadowRadius + mDy.absoluteValue).toInt()
setPadding(
if (mShadowSides.containsFlag(FLAG_SIDES_LEFT)) xPadding else 0.if (mShadowSides.containsFlag(FLAG_SIDES_TOP)) yPadding else 0.if (mShadowSides.containsFlag(FLAG_SIDES_RIGHT)) xPadding else 0.if (mShadowSides.containsFlag(FLAG_SIDES_BOTTOM)) yPadding else 0)}Copy the code
This is where backward users need to calculate the actual size of the layout in their heads.
NOTE:
- ShadowLayout Actual width = Content area width + (mShadowRadius + math.abs (mDx)) *2
- ShadowLayout Actual height = Content area height + (mShadowRadius + math.abs (mDy)) *2
- MShadowRadius + math.abs (mDx, mDy)
Here are two little questions:
- Why use Layout padding? Instead of using the area space after removing the padding
- Why set up, down, or left, when shadows are displayed up, down, or right
(mShadowRadius + Math. Abs (mDx)
The padding distance of? (Because of the offset, when you move to one side, the other side doesn’t need as much space.)
In fact, the reason is: to make it easier for users to calculate the actual size of the layout, and also save the trouble of calculating the size of the Canvas passed to the child View
DispatchDraw (Canvas: Canvas?) (represented only by this method), the width and height of the canvas does not include the padding of the parent View.
3. Draw a shadow of the size of the content area
This is called “Draw a shadow of the size of the content area” because we use Paint’s setShadowLayer() and Canvas’s drawRoundRect() to draw a rounded rectangle with a shadow based on the size of the content area.
The subview is then drawn on top of the rectangle and fits the size of the content area, visually acting as if the subview has a shadow.
SetLayerType (view.layer_type_software, NULL) is set to software render type. Take a look at the source code for this method.
Tips: For more on setLayerType(), see the article I read
/** * This draws a shadow layer below the main layer, with the specified * offset and color, And blur radius. If radius is 0, then the shadow * layer is removed. * This method draws a shadow layer under the main layer using the specified offset value, color, and divergence distance. * If divergence is 0, this layer is not drawn. * * Can be used to create a blurred shadow underneath text. Support for use * with other drawing operations is Constrained to the software rendering * Pipeline. * Can be used to create a blur shadow underneath the text. * Other drawing operations are also supported, but must be set to software render type. *
* The alpha of the shadow will be the paint's alpha if the shadow color is * opaque, Or the alpha from the shadow color if not. * If shadowColor is opaque (alpha channel value 255), * then use the brush opacity, otherwise use this value as transparency. * /
public void setShadowLayer(float radius, float dx, float dy, int shadowColor) {
mShadowLayerRadius = radius;
mShadowLayerDx = dx;
mShadowLayerDy = dy;
mShadowLayerColor = shadowColor;
nSetShadowLayer(mNativePaint, radius, dx, dy, shadowColor);
}
Copy the code
The code for drawing shadows is as follows:
// Calculate the size of the content area
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mContentRF = RectF(
paddingLeft.toFloat(),
paddingTop.toFloat(),
(w - paddingRight).toFloat(),
(h - paddingBottom).toFloat()
)
// Fine tune the drawing position of the border at 1/3 of the width of the border to get a better visual effect when the border is wider
val bw = mBorderWidth / 3
if (bw > 0) {
mBorderRF = RectF(
mContentRF.left + bw,
mContentRF.top + bw,
mContentRF.right - bw,
mContentRF.bottom - bw
)
}
}
Copy the code
override fun dispatchDraw(canvas: Canvas?). {
if (canvas == null) return
canvas.helpGreenCurtain(debug)
// Draw a shadow
drawShadow(canvas)
// Draw the child View, which will be said later
drawChild(canvas) {
super.dispatchDraw(it)
}
// Draw the border, which will be said later
drawBorder(canvas)
}
Copy the code
private fun drawShadow(canvas: Canvas) {
canvas.save()
mPaint.setShadowLayer(mShadowRadius, mDx, mDy, mShadowColor)
canvas.drawRoundRect(mContentRF, mCornerRadius, mCornerRadius, mPaint)
mPaint.utilReset()
canvas.restore()
}
Copy the code
Post a picture to see the effect:
The layout property value is app:sl_shadowRadius=”12dp”
4. Draw the content area and handle rounded corners
Here is a look at the code to explain, as follows:
override fun dispatchDraw(canvas: Canvas?).{...// Omit the code
// Draw a subview
drawChild(canvas) {
super.dispatchDraw(it)
}
...// Omit the code
}
Copy the code
private fun drawChild(canvas: Canvas, block: (Canvas) -> Unit) {
canvas.saveLayer(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat(), mPaint, Canvas.ALL_SAVE_FLAG)
// Draw the child control first
block.invoke(canvas)
// Use path to build four rounded corners
mPath = mPath.apply {
addRect(
mContentRF,
Path.Direction.CW
)
addRoundRect(
mContentRF,
mCornerRadius,
mCornerRadius,
Path.Direction.CW
)
fillType = Path.FillType.EVEN_ODD
}
// Use XferMode to make the composition on the layer and handle the rounded corners
mPaint.xfermode = mXfermode
canvas.drawPath(mPath, mPaint)
mPaint.utilReset()
mPath.reset()
canvas.restore()
}
Copy the code
The drawing process is:
- Open a new layer
- I’m going to draw the child View as
xfermode
Synthetic modeThe target - Use Path to build four rounded corners as the source of the compositing pattern
- with
DST_OUT(Remove target)
Pattern synthesis
Here’s another illustration:
Layout property value app:sl_cornerRadius=”10dp”
5. Draw the border
This step is also easy, the code:
override fun dispatchDraw(canvas: Canvas?).{...// Omit the code
// Draw the border
drawBorder(canvas)
}
Copy the code
private fun drawBorder(canvas: Canvas){ mBorderRF? .let { canvas.save() mPaint.strokeWidth = mBorderWidth mPaint.style = Paint.Style.STROKE mPaint.color = mBorderColor canvas.drawRoundRect(it, mCornerRadius, mCornerRadius, mPaint) mPaint.utilReset() canvas.restore() } }Copy the code
The final renderings are as follows:
The layout property value is app:sl_borderWidth=”2dp”
At the end of the article
My personal ability is limited. If there is something wrong, I welcome you to criticize and point it out. I will accept it humbly and modify it in the first time so as not to mislead you.
Read the article
- An in-depth look at the Attributes flag in Android
- Android custom View 1-8 Hardware acceleration
My other articles
- [Custom View] Douyin popular text clock – Part 1
- 【 custom View】 onion math same style ShadowLayout -ShadowLayout
- 【 custom View】 Onion math with the same radar map in-depth analysis -RadarView
- 【 custom View】 Onion mathematics with the same Banner evolution -BannerView