Kotlin實(shí)現(xiàn)萬能自定義ImageView

hello! I'm coming!又有好長時間沒更新了撑螺。今天我們來實(shí)現(xiàn)一個基于Kotlin的萬能自定義ImageView(在Google的大力推動下,Kotlin已經(jīng)成為android開發(fā)的主流語言了)崎弃。
作為移動端開發(fā)來說甘晤,最郁悶的莫過于UI設(shè)計(jì)師的天馬行空的想象了含潘,一會出個圓形的頭像,一會出個圓角的頭像线婚,又會出一些不規(guī)則的頭像...但對于我們開發(fā)來說好不容易開發(fā)好一個滿足要求的自定義View遏弱,但下個版本有可能就變了,這時候心頭真是萬馬奔騰塞弊。沒辦法漱逸,誰叫我們是搬磚的呢,繼續(xù)苦命的開發(fā)游沿。這時候我就想饰抒,能不能寫一個萬能的CustomImageView呢,這樣就不用糾結(jié)UI的各種變動了诀黍。不知道大家了解PorterDuffXfermode不袋坑?如果不了解的也可以去官網(wǎng)瞅瞅,我們可以基于它來實(shí)現(xiàn)圖片的混合渲染繪制以達(dá)到我們想要顯示的形狀眯勾,哪怕是不規(guī)則的枣宫,也不需要各種跪求UI設(shè)計(jì)師給我們提供對應(yīng)的path來進(jìn)行圖片裁剪。想想就很激動ψ(`?′)ψ咒精。先來目睹一下我們的效果圖


CustomImageView.png

感覺效果還可以吧镶柱,嘿嘿!
那就開擼吧DP稹P稹!

1.首先當(dāng)然是定義自定義屬性

<declare-styleable name="CustomImageView">
        <!--目標(biāo)圖片資源范咨,即想要截取形狀的樣圖-->
        <attr name="dsc" format="reference"/>
        <!--邊框圖片資源-->
        <attr name="bord" format="reference"/>
        <!--是否顯示邊框-->
        <attr name="hasBord" format="boolean"/>
    </declare-styleable>

2.下面便是正式自定義ImageView設(shè)計(jì)代碼了

/**
 * Created by coud on 2018/10/30.
 * 萬能自定義imageview
 */
class CustomImageView(context: Context, attrs: AttributeSet? = null) : AppCompatImageView(context, attrs) {

    //是否有邊框
    var isBord = false
        set(value) {
            field = value
            setUp()
        }

    //目標(biāo)資源
    var dscRes = R.mipmap.ic_default_custom_dsc
        set(value) {
            field = value
            setUp()
        }

    //邊框資源
    var bordRes = R.mipmap.ic_default_custom_bord
        set(value) {
            field = value
            isBord = true
            setUp()
        }

    private var mReady = true
    private var mSetupPending = false
    //源bitmap
    private var mSrcBitmap: Bitmap? = null
    //目標(biāo)bitmap
    private var mDstBitmap: Bitmap? = null
    //邊框bitmap
    private var mBordBitmap: Bitmap? = null

    private val bitmapConfig by lazy { Bitmap.Config.ARGB_8888 }
    private val colorDrawableDimension by lazy { 2 }
    private val drawableRect by lazy { RectF() }
    private val borderRect by lazy { RectF() }
    private val shaderMatrix by lazy { Matrix() }
    private val bitmapPaint by lazy { Paint(Paint.ANTI_ALIAS_FLAG) }

    init {
        attrs?.also {
            val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomImageView)
            isBord = typedArray.getBoolean(R.styleable.CustomImageView_hasBord, false)
            dscRes = typedArray.getResourceId(R.styleable.CustomImageView_dsc, R.mipmap.ic_default_custom_dsc)
            bordRes = typedArray.getResourceId(R.styleable.CustomImageView_bord, R.mipmap.ic_default_custom_bord)
            typedArray.recycle()
        }
        init()
    }

    private fun init() {
        if (mSetupPending) {
            setUp()
            mSetupPending = false
        }
    }

    override fun setAdjustViewBounds(adjustViewBounds: Boolean) {
        if (adjustViewBounds) {
            throw IllegalArgumentException("adjustViewBounds not supported.")
        }
    }

    override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
        super.setPadding(left, top, right, bottom)
        setUp()
    }

    override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
        super.setPaddingRelative(start, top, end, bottom)
        setUp()
    }

    override fun setImageBitmap(bm: Bitmap?) {
        super.setImageBitmap(bm)
        initializeBitmap()
    }

    override fun setImageDrawable(drawable: Drawable?) {
        super.setImageDrawable(drawable)
        initializeBitmap()
    }

    override fun setImageResource(resId: Int) {
        super.setImageResource(resId)
        initializeBitmap()
    }

    override fun setImageURI(uri: Uri?) {
        super.setImageURI(uri)
        initializeBitmap()
    }

    override fun setColorFilter(cf: ColorFilter?) {
        super.setColorFilter(cf)
        cf?.also {
            applyColorFilter()
            invalidate()
        }
    }

    @SuppressLint("DrawAllocation")
    override fun onDraw(canvas: Canvas?) {
        mSrcBitmap?.also { it ->
            //背景色設(shè)為白色故觅,方便比較效果
            canvas?.drawColor(Color.TRANSPARENT)
            //將繪制操作保存到新的圖層,因?yàn)閳D像合成是很昂貴的操作渠啊,將用到硬件加速输吏,這里將圖像合成的處理放到離屏緩存中進(jìn)行
            val saveCount = canvas?.saveLayer(drawableRect, bitmapPaint, ALL_SAVE_FLAG) ?: 0
            //繪制目標(biāo)圖
            mDstBitmap?.also {
                canvas?.drawBitmap(it, null, drawableRect, bitmapPaint)
            }
            //設(shè)置混合模式
            bitmapPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
            //繪制源圖
            canvas?.drawBitmap(it, null, drawableRect, bitmapPaint)
            //清除混合模式
            bitmapPaint.xfermode = null
            // 還原畫布
            canvas?.restoreToCount(saveCount)

            if (isBord) {
                mBordBitmap?.also {
                    canvas?.drawBitmap(it, null, drawableRect, bitmapPaint)
                }
            }
        }
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        setUp()
    }

    private fun setUp() {
        if (!mReady) {
            mSetupPending = true
            return
        }
        if (0 == width || 0 == height) {
            return
        }
        mSrcBitmap?.also { it ->
            borderRect.set(calculateBounds())

            drawableRect.set(borderRect)

            val srcShader = BitmapShader(it, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)

            mDstBitmap = mDstBitmap ?: getBitmapFromRes(dscRes)

            if (isBord) {
                mBordBitmap = getBitmapFromRes(bordRes)
                mBordBitmap?.also {
                    val bordShader = BitmapShader(it, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
                    updateShaderMatrix(it, bordShader)
                }
            }

            applyColorFilter()

            updateShaderMatrix(it, srcShader)

            mDstBitmap?.also {
                val dstShader = BitmapShader(it, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
                updateShaderMatrix(it, dstShader)
            }

            invalidate()
        } ?: also {
            invalidate()
        }
    }

    private fun initializeBitmap() {
        mSrcBitmap = getBitmapFromDrawable(drawable)
        setUp()
    }

    private fun getBitmapFromDrawable(drawable: Drawable): Bitmap {
        return when (drawable) {
            is BitmapDrawable -> drawable.bitmap
            is ColorDrawable -> creatBitmap(Bitmap.createBitmap(colorDrawableDimension, colorDrawableDimension, bitmapConfig), drawable)
            else -> creatBitmap(Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, bitmapConfig), drawable)
        }
    }

    private fun creatBitmap(bitmap: Bitmap, drawable: Drawable): Bitmap {
        return bitmap.also {
            val canvas = Canvas(it)
            drawable.setBounds(0, 0, canvas.width, canvas.height)
            drawable.draw(canvas)
        }
    }

    @SuppressLint("NewApi")
    private fun applyColorFilter() {
        colorFilter?.also {
            bitmapPaint.colorFilter = it
        }
    }

    private fun getBitmapFromRes(@DrawableRes resId: Int): Bitmap {
        return resId.let {
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
                creatBitmap(Bitmap.createBitmap(drawableRect.width().toInt(),
                        drawableRect.height().toInt(), bitmapConfig),
                        resources.getDrawable(it, null))
            } else {
                BitmapFactory.decodeResource(resources, it)
            }
        }
    }

    private fun calculateBounds(): RectF {
        val availableWidth = width - paddingLeft - paddingRight
        val availableHeight = height - paddingTop - paddingBottom
        val sideLength = Math.min(availableWidth, availableHeight)
        val left = paddingLeft - (availableWidth - sideLength) / 2.0f
        val top = paddingTop - (availableHeight - sideLength) / 2.0f
        return RectF(left, top, left + sideLength, top + sideLength)
    }

    private fun updateShaderMatrix(bitmap: Bitmap, shader: Shader) {
        shaderMatrix.set(null)
        val scaleX = bitmap.width / drawableRect.width()
        val scaleY = bitmap.height / drawableRect.height()
        shaderMatrix.setScale(scaleX, scaleY)
        shader.setLocalMatrix(shaderMatrix)
    }
}

以后不管UI設(shè)計(jì)師想要什么樣的頭像效果,我們都可以很方便的實(shí)現(xiàn)替蛉。這時候我們只需要讓UI設(shè)計(jì)師提供一張默認(rèn)的效果圖贯溅,然后我們設(shè)置通過app:dsc="@drawable/icon_dsc", 如果要帶邊框app:hasBord="true" app:bord="@drawable/icon_bord"躲查,就可以輕松實(shí)現(xiàn)它浅。so easy,從此再也不怕各種形狀了镣煮。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末姐霍,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌镊折,老刑警劉巖胯府,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異恨胚,居然都是意外死亡骂因,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進(jìn)店門与纽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來侣签,“玉大人,你說我怎么就攤上這事急迂。” “怎么了蹦肴?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵僚碎,是天一觀的道長。 經(jīng)常有香客問我阴幌,道長勺阐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任矛双,我火速辦了婚禮渊抽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘议忽。我一直安慰自己懒闷,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布栈幸。 她就那樣靜靜地躺著愤估,像睡著了一般。 火紅的嫁衣襯著肌膚如雪速址。 梳的紋絲不亂的頭發(fā)上玩焰,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天,我揣著相機(jī)與錄音芍锚,去河邊找鬼昔园。 笑死,一個胖子當(dāng)著我的面吹牛并炮,可吹牛的內(nèi)容都是我干的默刚。 我是一名探鬼主播,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼渣触,長吁一口氣:“原來是場噩夢啊……” “哼羡棵!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起嗅钻,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤皂冰,失蹤者是張志新(化名)和其女友劉穎店展,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體秃流,經(jīng)...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赂蕴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了舶胀。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片概说。...
    茶點(diǎn)故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖嚣伐,靈堂內(nèi)的尸體忽然破棺而出糖赔,到底是詐尸還是另有隱情,我是刑警寧澤轩端,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布放典,位于F島的核電站,受9級特大地震影響基茵,放射性物質(zhì)發(fā)生泄漏奋构。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一拱层、第九天 我趴在偏房一處隱蔽的房頂上張望弥臼。 院中可真熱鬧,春花似錦根灯、人聲如沸径缅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽芥驳。三九已至,卻和暖如春茬高,著一層夾襖步出監(jiān)牢的瞬間兆旬,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工怎栽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留丽猬,地道東北人。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓熏瞄,卻偏偏與公主長得像脚祟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子强饮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評論 2 354

推薦閱讀更多精彩內(nèi)容