Out of the box source address

Onion math with ShadowLayout -ShadowLayout

Support for custom attributes:

  • sl_shadowRadius: Shadow divergence distance
  • sl_shadowColor: Shadow color
  • sl_dx: Left and right shadow offset
  • sl_dy: The upper and lower offset of the shadow
  • sl_cornerRadius: Rounded corners
  • sl_borderWidth: Layout border width
  • sl_borderColor: Layout border color
  • sl_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:

  1. Extract custom Layout properties for users to quickly start
  2. 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 towrite once use everywhereLet’s just write a layout, so we can wrap whatever we want, so define an inheritance, okayFrameLayoutThe layout of the nameShadowLayout
  • The core is implementationshadows, check the information to know that it can be usedPaintthesetShadowLayer()API
  • Fillet processing we can usexfermodeApply one to the child View on the canvasGo round the corners
  • Border handling is easy to useCanvasthedrawRoundRectPainting can be
  • Controls the display of shadows on one or more edges, using custom attributesflagsType implementation (exactly what we need)

Frame of thought:

  1. Define and initialize properties
  2. Set the padding to leave space for the shadow
  3. Draw a shadow of the size of the content area (Content area == subview area == layout-padding)
  4. Draw the content area and handle rounded corners
  5. 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

  1. Its type isflagsAnd plural, so this can be used to set multiple flag bits for a property
  2. This is how it is used in AN XML layoutapp:sl_shadowSides="TOP|RIGHT|BOTTOM"Through the| (logical or)Join multiple flag bits (this method is often used)
  3. Value specifies the value definedOne, two, four, eight, fifteenIt’s regular, it’s not arbitrary
  4. When used in code
    • judgeFlag setWhether there is aflag(Used this time)
    • inFlag setAdd newflag
    • inFlag setTo remove aflag

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:

  1. Why use Layout padding? Instead of using the area space after removing the padding
  2. 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:

  1. Open a new layer
  2. I’m going to draw the child View asxfermodeSynthetic modeThe target
  3. Use Path to build four rounded corners as the source of the compositing pattern
  4. withDST_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