实现 Android 的 ShadowLayout

作者: lguipeng | 来源:发表于2018-09-07 16:44 被阅读363次

    Android 里面并没有带阴影相关的参数控件之类,特别是能满足设计师要求的...
    于是实现了一个 ShadowLayout 继承自 FrameLayout,用 ShadowLayout 作为布局的父布局就好了。
    首先 阴影 有几个比较重要的参数,偏移量x 偏移量y 圆角 模糊度 阴影颜色
    主要利用 paint.setShadowLayer 方法。

    建立自定义 View 属性值

    //attrs.xml
    <declare-styleable name="ShadowLayout">
            <attr name="shadow_layout_color" format="color"/>
            <attr name="shadow_layout_background_color" format="color"/>
            <attr name="shadow_layout_offsetX" format="dimension"/>
            <attr name="shadow_layout_offsetY" format="dimension"/>
            <attr name="shadow_layout_blur" format="dimension"/>
            <attr name="shadow_layout_radius" format="dimension"/>
     </declare-styleable>
    
    class ShadowLayout : FrameLayout {
    
        var mShadowColor: Int = 0
        var mShadowRadius: Float = 0.toFloat()
        var mCornerRadius: Float = 0.toFloat()
        var mDx: Float = 0.toFloat()
        var mDy: Float = 0.toFloat()
        var mBackgroundColor: Int = 0
    
        private var mInvalidateShadowOnSizeChanged = true
        private var mForceInvalidateShadow = false
    
        constructor(context: Context) : super(context) {
            initView(context, null)
        }
    
        constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
            initView(context, attrs)
        }
    
        constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
            initView(context, attrs)
        }
    
        override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
            super.onSizeChanged(w, h, oldw, oldh)
            if (w > 0 && h > 0 && (background == null || mInvalidateShadowOnSizeChanged || mForceInvalidateShadow)) {
                mForceInvalidateShadow = false
                setBackgroundCompat(w, h)
            }
        }
    
        override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
            super.onLayout(changed, left, top, right, bottom)
            if (mForceInvalidateShadow) {
                mForceInvalidateShadow = false
                setBackgroundCompat(right - left, bottom - top)
            }
        }
    
        fun setInvalidateShadowOnSizeChanged(invalidateShadowOnSizeChanged: Boolean) {
            mInvalidateShadowOnSizeChanged = invalidateShadowOnSizeChanged
        }
    
        fun invalidateShadow() {
            mForceInvalidateShadow = true
            requestLayout()
            invalidate()
        }
    
        private fun initView(context: Context, attrs: AttributeSet?) {
            initAttributes(context, attrs)
            refreshPadding()
        }
    
        fun refreshPadding() {
            val xPadding = (mShadowRadius + Math.abs(mDx)).toInt()
            val yPadding = (mShadowRadius + Math.abs(mDy)).toInt()
            setPadding(xPadding, yPadding, xPadding, yPadding)
        }
    
        private fun setBackgroundCompat(w: Int, h: Int) {
            val bitmap = createShadowBitmap(w, h, mCornerRadius, mShadowRadius, mDx, mDy, mShadowColor, Color.TRANSPARENT)
            val drawable = BitmapDrawable(resources, bitmap)
            background = drawable
        }
    
    
        private fun initAttributes(context: Context, attrs: AttributeSet?) {
            val attr = getTypedArray(context, attrs, R.styleable.ShadowLayout) ?: return
            try {
                mCornerRadius = attr.getDimension(R.styleable.ShadowLayout_shadow_layout_radius, 0f)
                mShadowRadius = attr.getDimension(R.styleable.ShadowLayout_shadow_layout_blur, 0f)
                mDx = attr.getDimension(R.styleable.ShadowLayout_shadow_layout_offsetX, 0f)
                mDy = attr.getDimension(R.styleable.ShadowLayout_shadow_layout_offsetY, 0f)
                mShadowColor = attr.getColor(R.styleable.ShadowLayout_shadow_layout_color, Color.parseColor("#22000000"))
                mBackgroundColor = attr.getColor(R.styleable.ShadowLayout_shadow_layout_background_color, Integer.MIN_VALUE)
            } finally {
                attr.recycle()
            }
        }
    
        private fun getTypedArray(context: Context, attributeSet: AttributeSet?, attr: IntArray): TypedArray? {
            return context.obtainStyledAttributes(attributeSet, attr, 0, 0)
        }
    
        private fun createShadowBitmap(shadowWidth: Int, shadowHeight: Int, cornerRadius: Float, shadowRadius: Float,
                                       dx: Float, dy: Float, shadowColor: Int, fillColor: Int): Bitmap {
            val output: Bitmap = if (mBackgroundColor != Integer.MIN_VALUE) {
                Bitmap.createBitmap(shadowWidth, shadowHeight, Bitmap.Config.ARGB_8888)
            } else {
                Bitmap.createBitmap(shadowWidth, shadowHeight, Bitmap.Config.ALPHA_8)
            }
            val canvas = Canvas(output)
    
            val shadowRect = RectF(
                    shadowRadius,
                    shadowRadius,
                    shadowWidth - shadowRadius,
                    shadowHeight - shadowRadius)
    
            if (dy > 0) {
                shadowRect.top += dy
                shadowRect.bottom -= dy
            } else if (dy < 0) {
                shadowRect.top += Math.abs(dy)
                shadowRect.bottom -= Math.abs(dy)
            }
    
            if (dx > 0) {
                shadowRect.left += dx
                shadowRect.right -= dx
            } else if (dx < 0) {
                shadowRect.left += Math.abs(dx)
                shadowRect.right -= Math.abs(dx)
            }
    
            val paint = Paint()
            paint.isAntiAlias = true
            paint.color = fillColor
            paint.style = Paint.Style.FILL
    
            paint.setShadowLayer(shadowRadius, dx, dy, shadowColor)
            canvas.drawRoundRect(shadowRect, cornerRadius, cornerRadius, paint)
            if (mBackgroundColor != Integer.MIN_VALUE) {
                paint.clearShadowLayer()
                paint.color = Color.WHITE
                val backgroundRect = RectF(paddingLeft.toFloat(), paddingTop.toFloat(), (width - paddingRight).toFloat(), (height - paddingBottom).toFloat())
                canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, paint)
            }
    
            return output
        }
    
    }
    

    这里把阴影生成之后设置在背景里面,这样能减少 UI 绘制阴影的次数,特别在列表里面,比起在 draw 里面每次绘制,会流畅很多...

    效果图 (随便找了公司产品的页面....)

    图1
    图2

    相关文章

      网友评论

      • 有点健忘:哪天cardview阴影可以弄一圈就省事了。

      本文标题:实现 Android 的 ShadowLayout

      本文链接:https://www.haomeiwen.com/subject/krvcgftx.html