系统为我们提供的控件是有限的,当我们想要在有限的屏幕上显示更丰富多彩的内容,我们往往需要自定义控件。作为一个android初学者,我对android的自定义View也不是很熟悉。这段时间刚好无事,就先从我们平常使用的圆形头像开始练起吧。
我们要知道一个View绘制需要三大流程onMeasure,onLayout,onDraw
控件实现主代码
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工具类
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)
}
}
<declare-styleable name="CircleImageView">
<attr name="radius" format="dimension"/>
declare-styleable>
"1.0" encoding="utf-8"?>
.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" />
.support.constraint.ConstraintLayout>
参考: Android BitmapShader 实战 实现圆形、圆角图片
代码里面有很多注释,在这里就不解释了
/**
* 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的介绍和用处(遮罩图层,圆形图片)