自定义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>
效果图

这里写图片描述

参考: Android BitmapShader 实战 实现圆形、圆角图片

使用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" />
效果图

这里写图片描述

参考: 关于Xfermode的介绍和用处(遮罩图层,圆形图片)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个简单的圆形进度条的自定义View实现: ```java public class CircleProgressBar extends View { private float mProgress = 0; // 当前进度值 private float mMax = 100; // 最大进度值 private int mCircleWidth = 10; // 圆环宽度 private int mCircleColor = Color.GRAY; // 圆环颜色 private int mProgressColor = Color.BLUE; // 进度条颜色 private Paint mPaint; public CircleProgressBar(Context context) { super(context); init(); } public CircleProgressBar(Context context, AttributeSet attrs) { super(context, attrs); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar); mCircleWidth = ta.getDimensionPixelSize(R.styleable.CircleProgressBar_circleWidth, 10); mCircleColor = ta.getColor(R.styleable.CircleProgressBar_circleColor, Color.GRAY); mProgressColor = ta.getColor(R.styleable.CircleProgressBar_progressColor, Color.BLUE); ta.recycle(); init(); } private void init() { mPaint = new Paint(); mPaint.setAntiAlias(true); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int centerX = getWidth() / 2; int centerY = getHeight() / 2; int radius = getWidth() / 2 - mCircleWidth / 2; // 画圆环 mPaint.setColor(mCircleColor); mPaint.setStrokeWidth(mCircleWidth); mPaint.setStyle(Paint.Style.STROKE); canvas.drawCircle(centerX, centerY, radius, mPaint); // 画进度条 mPaint.setColor(mProgressColor); mPaint.setStrokeWidth(mCircleWidth); mPaint.setStyle(Paint.Style.STROKE); RectF rectF = new RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius); canvas.drawArc(rectF, -90, 360 * mProgress / mMax, false, mPaint); } public void setProgress(float progress) { mProgress = progress; invalidate(); } public void setMax(float max) { mMax = max; invalidate(); } } ``` 其中,我们可以设置圆环的宽度、圆环颜色、进度条颜色等属性。在onDraw()方法中,我们先画出圆环,然后再画出进度条,进度条的弧度根据当前进度值和最大进度值计算得出。 使用时,可以在布局文件中添加如下代码: ```xml <com.example.customview.CircleProgressBar android:id="@+id/circle_progress_bar" android:layout_width="wrap_content" android:layout_height="wrap_content" app:circleColor="#FFA500" app:circleWidth="20dp" app:progressColor="#00BFFF" /> ``` 然后在代码中设置进度值即可: ```java CircleProgressBar circleProgressBar = findViewById(R.id.circle_progress_bar); circleProgressBar.setMax(100); circleProgressBar.setProgress(50); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值