1. 程式人生 > >自定義View學習一(圓形頭像)

自定義View學習一(圓形頭像)

前言

系統為我們提供的控制元件是有限的,當我們想要在有限的螢幕上顯示更豐富多彩的內容,我們往往需要自定義控制元件。作為一個android初學者,我對android的自定義View也不是很熟悉。這段時間剛好無事,就先從我們平常使用的圓形頭像開始練起吧。

我們要知道一個View繪製需要三大流程onMeasure,onLayout,onDraw

使用BitmapShader實現

控制元件實現主程式碼

CircleImageView.kt

class CircleImageView : ImageView {

    private var radius: Float? = null
    private val defaultRadius = 40.toFloat()
private var mPaint: Paint? = null private var mWidth: Int? = null constructor(context: Context) : super(context) { CircleImageView(context, null) } constructor(context: Context, attributeSet: AttributeSet?) : super(context, attributeSet) { init(attributeSet) } constructor(context
: Context, attributeSet: AttributeSet?, defStyleAttributeSet: Int)
: super(context, attributeSet, defStyleAttributeSet) { init(attributeSet) } init { mPaint = Paint() mPaint!!.isAntiAlias = true } fun init(attributeSet: AttributeSet?){ val typeArray = context.obtainStyledAttributes(attributeSet
, R.styleable.CircleImageView)
; radius = typeArray.getDimension(R.styleable.CircleImageView_radius, defaultRadius); typeArray.recycle() } //支援padding override fun onDraw(canvas: Canvas?) { if (drawable == null) return setUpShader() val mWidth = width - paddingLeft - paddingRight val mHeight = height - paddingBottom - paddingTop val temp = Math.min(mWidth,mHeight) if (radius!!*2 > temp){ radius = temp/2f } //這個控制元件的radius原本是不需要的,新增radius主要是為了演示自定義View的自定義屬性。 /** * onDraw方法裡面我們支援了padding * 由於我們繪製圖像的時候需要將影象繪製到中心區域,所以繪製的時候我們也需要考慮padding */ canvas?.drawCircle(paddingLeft+radius!!, paddingTop+radius!!, radius!!, mPaint) } //重寫onMeasure,這個自定義view是繼承自ImageView,可以不用重寫onMeasure方法,但是如果是直接繼承自View或者是ViewGroup就需要 //重寫onMeasure,否則的話,自定義控制元件的wrap_content的效果和match_parent一樣 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) mWidth = Math.min(measuredWidth, measuredHeight) setMeasuredDimension(mWidth!!, mWidth!!) } /** * TileMode的取值有三種: * CLAMP 拉伸 * REPEAT 重複 * MIRROR 映象 */ private fun setUpShader() { if (drawable == null) return val bitmap = BitmapUtils.drawableToBitmap(drawable) val temp = Math.min(bitmap.width,bitmap.height) val squareBitmap = BitmapUtils.cropBitmap(bitmap,temp,temp) val shader = BitmapShader(squareBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) var scale = 1.0f scale = mWidth!! * scale / squareBitmap.width val matrix = Matrix() matrix.setScale(scale, scale) // 為了縮放使用 shader.setLocalMatrix(matrix) mPaint!!.shader = shader } }

使用到的BitmapUtils工具類

BitmapUtils.kt

object BitmapUtils {

    //將drawable轉換成bitmap
    fun drawableToBitmap(drawable: Drawable): Bitmap {

        val w = drawable.intrinsicWidth
        val h = drawable.intrinsicHeight
        val config = if (drawable.opacity != PixelFormat.OPAQUE)
            Bitmap.Config.ARGB_8888
        else
            Bitmap.Config.RGB_565
        val bitmap = Bitmap.createBitmap(w, h, config)
        //注意,下面三行程式碼要用到,否則在View或者SurfaceView裡的canvas.drawBitmap會看不到圖
        val canvas = Canvas(bitmap)
        drawable.setBounds(0, 0, w, h)
        drawable.draw(canvas)
        return bitmap
    }

    /**
     * 裁剪
     * @param bitmap 原圖
     * *
     * @return 裁剪後的影象
     */
    fun cropBitmap(bitmap: Bitmap, aimWidth:Int, aimHeight:Int): Bitmap {
        var toWidth = aimWidth;
        var toHeight = aimHeight
        if (aimWidth > bitmap.width) toWidth = bitmap.width
        if (aimHeight > bitmap.height) toHeight = bitmap.height

        val cropWidthSide = (bitmap.width - aimWidth)/2
        val cropHeightSide = (bitmap.height - aimHeight)/2
        return Bitmap.createBitmap(bitmap,cropWidthSide,cropHeightSide,toWidth,toHeight)
    }
}

CircleImageView的屬性定義,目前只定義了一個半徑

    <declare-styleable name="CircleImageView">
        <!--圓形頭像的半徑-->
        <attr name="radius" format="dimension"/>
    </declare-styleable>

具體使用

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.xiaojun.blog.MainActivity">

    <com.example.xiaojun.kotlin_try.ui.widget.blog.CircleImageView
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:radius="50dp"
        android:src="@drawable/xue3"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

效果圖

這裡寫圖片描述

使用Xfermode實現圓形頭像

程式碼裡面有很多註釋,在這裡就不解釋了

CircleImageViewX.kt

/**
 * CircleImageViewX是用xFerMode來實現的圓形頭像
 * CircleImageViewX在頭像圖片的外層有一圈自定義的圓環,美化頭像,預設存在圓環
 */
//直接繼承自View而不再是ImageView,這個時候如果我們想實現wrap_content效果必須自己重寫onMeasure

class CircleImageViewX :View{

    private val defaultWidth = 200
    private val defaultHeight = 200
    private val defaultRingWidth = 10f
    private val defaultRingColor = Color.WHITE
    private val defaultHasRing = true

    private var ringColor = defaultRingColor
    private var ringWidth = defaultRingWidth
    private var hasRing = defaultHasRing
    private var drawable:Drawable? = null


    private var mPaint: Paint? = null

    constructor(context: Context) : super(context) {

    }

    constructor(context: Context, attributeSet: AttributeSet?) : super(context, attributeSet) {
        init(attributeSet)
    }

    constructor(context: Context, attributeSet: AttributeSet?, defStyleAttributeSet: Int) : super(context, attributeSet, defStyleAttributeSet) {
        init(attributeSet)
    }

    init {
        mPaint = Paint()
        mPaint!!.isAntiAlias = true
    }

    fun init(attributeSet: AttributeSet?){
        val typeArray = context.obtainStyledAttributes(attributeSet, R.styleable.CircleImageViewX)
        drawable = typeArray.getDrawable(R.styleable.CircleImageViewX_src)
        hasRing = typeArray.getBoolean(R.styleable.CircleImageViewX_hasRing,defaultHasRing)
        if (hasRing){
            ringColor = typeArray.getColor(R.styleable.CircleImageViewX_ringColor,defaultRingColor)
            ringWidth = typeArray.getDimension(R.styleable.CircleImageViewX_ringWidth,defaultRingWidth)
        }
        typeArray.recycle()
    }

    //當佈局中的寬或者高屬性設定的是wrap_content的時候,我們返回預設寬或者高
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val widthSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        val heightSize = MeasureSpec.getSize(heightMeasureSpec)
        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(defaultWidth,defaultHeight)
        }else if (widthMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(defaultWidth,heightSize)
        }else if (heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSize,defaultHeight)
        }else{
            setMeasuredDimension(widthSize,heightSize)
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    }

    override fun onDraw(canvas: Canvas?) {
        if (drawable == null){
            Log.e("drawable","null")
            return
        }
        Log.e("drawable","draw")
        val bitmap = BitmapUtils.drawableToBitmap(drawable!!)
        val squareBitmap = BitmapUtils.cropSquareBitmap(bitmap)
        //為了支援padding
        val mWidth = width - paddingLeft - paddingRight
        val mHeight = height - paddingBottom - paddingTop
        var interval = 0
        val radius = Math.min(mWidth,mHeight)/2f
        if (hasRing){
            //畫圓環
            interval = ringWidth.toInt()
            mPaint?.color = ringColor
            mPaint?.strokeWidth = ringWidth
            mPaint?.style = Paint.Style.STROKE
            //當線條有寬度的時候,paint是預設線上條寬度的中央繪製,這就要求我們在繪製外部大圓的時候有所調整
            canvas?.drawCircle(paddingLeft+radius,paddingTop+radius,radius - ringWidth/2,mPaint)
        }

        val circleBitmapRadius = radius - interval
        val circleBitmap = cropCircleBitmap(squareBitmap,circleBitmapRadius)
        //畫圓形圖片
        val srcRect = Rect(0,0,circleBitmap.width,circleBitmap.height)
        val desRect = Rect(paddingLeft+interval,paddingTop+interval,circleBitmap.width+paddingLeft+interval,circleBitmap.height+paddingTop+interval)
        canvas?.drawBitmap(circleBitmap,srcRect,desRect,mPaint)
    }

    //傳進來的bitmap是已經裁剪好的bitmap
    fun cropCircleBitmap(bitmap: Bitmap,radius:Float):Bitmap{

        val ret = Bitmap.createBitmap(2*radius.toInt(),2*radius.toInt(), Bitmap.Config.ARGB_8888)
        val paint = Paint(Paint.ANTI_ALIAS_FLAG)
        //建立一個和目標圖片大小相同的canvas
        val canvas = Canvas(ret)
        //繪製下層圖,是一個圓
        canvas.drawCircle(radius,radius,radius,paint)
        // 設定混合模式,取繪製的圖的交集部分,顯示上層
        paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
        // 第一個Rect 代表要繪製的bitmap 區域,第二個 Rect 代表的是要將bitmap 繪製在螢幕的什麼地方
        val srcRect = Rect(0,0,bitmap.height,bitmap.width)
        val desRect = Rect(0,0,radius.toInt()*2,radius.toInt()*2)

        //上面兩個rect的含義相當於用matrix設定scale。含義是把源圖片的srcRect區域內容繪製在desRect區域內
        canvas.drawBitmap(bitmap,srcRect,desRect,paint)
        return ret
    }

    //設定圓環顏色
    fun setRingColor(color:Int){
        this.ringColor = color
        invalidate()
    }
    //設定圓環寬度
    fun setRingWidth(width:Float){
        this.ringWidth = width
        invalidate()
    }

    //設定是否有圓環
    fun setHasRing(has:Boolean){
        hasRing = has
        invalidate()
    }


}

自定義屬性

    <declare-styleable name="CircleImageViewX">

        <!--圖片資源-->
        <attr name="src" format="reference"/>
        <!--是否新增外圍圓環,如果沒有的話,那麼後面的兩個引數都沒有意義-->
        <attr name="hasRing" format="boolean"/>
        <!--圓環寬度-->
        <attr name="ringWidth" format="dimension"/>
        <!--圓環顏色-->
        <attr name="ringColor" format="color"/>

    </declare-styleable>

使用

    <com.example.xiaojun.blog.widget.CircleImageViewX
        android:id="@+id/circleX"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:ringColor="@color/colorAccent"
        app:hasRing="true"
        app:ringWidth="10px"
        app:src="@drawable/xue1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginBottom="88dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintHorizontal_bias="0.539" />

效果圖

這裡寫圖片描述