Android原生控件中并没有展示圆形,以及圆角的基础控件,想要实现圆形和圆角样式的展示就需要借助,Shape ,CardView,圆形图片等形式展示圆形以及圆角.最近时间允许就探究下圆形以及圆角图形的实现.
Android原生控件不支持圆形和圆角,那么实现圆形和圆角,无非是将原生控件切割和绘制成圆形和圆角就可以了
关键类解释:
PorterDuffXfermode :将所绘制的图形的像素与Canvas中对应位置的像素按照一定规则进行混合,形成新的像素值
切割和绘制圆形圆角的方案:
- PorterDuffXfermode 方案(操作Canvas)
利用Paint的setXfermode()方法 设置PorterDuffXfermode 叠加展示圆形以及圆角效果
缺点:
1:如果你觉得混合模式没有正确使用,可以让调用setLayerType(View.LAYER_TYPE_SOFTWARE, null)方法切换到软件渲染模式
- PorterDuffXfermode 方案(操作Canvas)
- Canvas.clipPath() 方案 (操作Canvas)
利用画布裁剪路径的方式绘制圆形/圆角形状 ShapeableImageView采用此方案
缺点:
1:cliptPath不支持硬件加速,因此在调用前必须禁用硬件加速,setLayerType(View.LAYER_TYPE_SOFTWARE, null)
2:这种方式剪裁的是Canvas图形,View的实际形状是不变的,
因此只能对src属性有效,对background属性是无效的
- Canvas.clipPath() 方案 (操作Canvas)
3.BitmapShader 位图着色器 (Paint)
利用位图着色器,在画布上绘制圆形或者圆角,Glide实现圆角以及圆形采用此方案
缺点:
就是如果要定义一个圆角图片,必须调用canvas.drawRoundRect进行绘制,但是这个方法要求API>=214.ViewOutlineProvider 将View切割成圆形或者圆角(View)
OutlineProvider轮廓提供者,可以给View提供一个外轮廓,并且让其根据轮廓进行剪切
缺点:
OutlineProvider 需要>21 使用
1.PorterDuffXfermode 方案
思路:
1:利用canvas.drawCircle()绘制一个圆形形状
2:给画笔设置叠加模式 mPaint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.SRC_IN))
3:绘制图片,然后取消绘制模式
注意:
如果你觉得混合模式没有正确使用,可以让调用setLayerType(View.LAYER_TYPE_SOFTWARE, null)方法切换到软件渲染模式
package com.wkq.workkotlin.custom.cycle
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import androidx.compose.ui.geometry.Rect
import com.wkq.workkotlin.R
/**
*@Desc:Xfermode 处理圆形图片
*
*@Author: wkq
*
*@Time: 2024/11/7 10:55
*
*/
class CycleXFerModeView : View {
var mContext: Context
var mPaint: Paint
val DEFAULT_SIZE = 500
var mSize: Int = 0
var mRadius=0f
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, -1)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : this(
context, attrs, defStyleAttr, -1
)
@SuppressLint("ResourceAsColor")
constructor(
context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int
) : super(context, attrs, defStyleAttr, defStyleRes) {
mContext = context
mPaint = Paint()
mPaint.isAntiAlias = true
mPaint.style = Paint.Style.FILL
val tp = mContext.obtainStyledAttributes(attrs, R.styleable.CycleXFerModeViewStyle)
mRadius = tp.getFloat(R.styleable.CycleXFerModeViewStyle_mRadius, 0f)
tp.recycle()
setLayerType(LAYER_TYPE_SOFTWARE, null);
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val width: Int = getMeasureSize(widthMeasureSpec)
val height: Int = getMeasureSize(heightMeasureSpec)
mSize = Math.min(width, height)
setMeasuredDimension(mSize, mSize)
}
private fun getMeasureSize(measureSpec: Int): Int {
val mode = MeasureSpec.getMode(measureSpec)
val size = MeasureSpec.getSize(measureSpec)
return if (mode == MeasureSpec.EXACTLY) size else DEFAULT_SIZE
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (mRadius==0f){
processCycleXMode(canvas)
}else{
processConnerXMode(canvas)
}
}
/**
* 处理圆角
* @param canvas Canvas
*/
var curBitmap : Bitmap?=null
private fun processConnerXMode(canvas: Canvas) {
curBitmap=getScaleBitmap()
if (curBitmap==null)return
val mRect= RectF(0f,0f,width.toFloat(), height.toFloat())
canvas.drawRoundRect(mRect,20f,20f, mPaint)
mPaint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.SRC_IN))
canvas.drawBitmap(curBitmap!!, 0f, 0f, mPaint)
mPaint.setXfermode(null)
}
/**
* 处理圆形
* @param canvas Canvas
*/
private fun processCycleXMode(canvas: Canvas) {
curBitmap=getScaleBitmap()
if (curBitmap==null)return
canvas.drawCircle((height / 2.0).toFloat(), (height / 2.0).toFloat(), (height / 2.0).toFloat(), mPaint)
mPaint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.SRC_IN))
canvas.drawBitmap(curBitmap!!, 0f, 0f, mPaint)
mPaint.setXfermode(null)
}
private fun getScaleBitmap(): Bitmap {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeResource(resources, R.mipmap.test, options)
options.inSampleSize = calcSampleSize(options, mSize, mSize)
options.inJustDecodeBounds = false
return BitmapFactory.decodeResource(resources, R.mipmap.test, options)
}
/**
* 计算缩放比例
*
* @param option
* @param width
* @param height
* @return
*/
private fun calcSampleSize(option: BitmapFactory.Options, width: Int, height: Int): Int {
var originWidth = option.outWidth
var originHeight = option.outHeight
var sampleSize = 1
while (((originWidth shr 1).also {
originWidth = it
}) > width && ((originHeight shr 1).also {
originHeight = it
}) > height) {
sampleSize = sampleSize shl 1
}
return sampleSize
}
}
2.clipPath方案
思路:
1:mPath.addRoundRect()/addCircle()绘制路径
2:canvas.clipPath(mPath) 按路径绘制
注意:
1:canvas.clipPath:不支持硬件加速,所以在使用前需要禁止硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
2:clipPath要在super.onDraw方法前,调用,否则无效(canvas已经被设置给View了)
package com.wkq.workkotlin.custom.cycle
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import androidx.appcompat.widget.AppCompatImageView
import androidx.compose.ui.geometry.Rect
import com.wkq.workkotlin.R
import kotlin.math.min
/**
*@Desc:Xfermode 处理圆形图片
*
*@Author: wkq
*
*@Time: 2024/11/7 10:55
*
*/
class CycleClipPathView : AppCompatImageView {
var mContext: Context
private var mRect: RectF
private var mPath: Path
private var mRadius = 0f
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, -1)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context, attrs, defStyleAttr
){
mContext = context
val tp = mContext.obtainStyledAttributes(attrs, R.styleable.CycleXFerModeViewStyle)
mRadius = tp.getFloat(R.styleable.CycleXFerModeViewStyle_mRadius, 0f)
tp.recycle()
mRect = RectF()
mPath = Path()
setLayerType(LAYER_TYPE_HARDWARE, null);
}
override fun onDraw(canvas: Canvas) {
canvas.clipPath(mPath)
super.onDraw(canvas)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (mRadius <= 0f) {
clipCircle(w, h);
} else {
clipRoundRect(w, h);
}
}
/**
* 圆角
*/
private fun clipRoundRect(width: Int, height: Int) {
mRect.left = 0f
mRect.top = 0f
mRect.right = width.toFloat()
mRect.bottom = height.toFloat()
mPath.addRoundRect(mRect, mRadius, mRadius, Path.Direction.CW)
}
/**
* 圆形
*/
private fun clipCircle(width: Int, height: Int) {
val radius = (min(width.toDouble(), height.toDouble()) / 2).toInt()
mPath.addCircle(width.toFloat() / 2, height.toFloat() / 2, radius.toFloat(), Path.Direction.CW)
}
}
3.BitmapShader 方案
思路:
1:获取Bitmap,生成BitmapShader 着色器
2:将着色器设置给画笔 mPaint.setShader(bitmapShader)
3:在画布上绘制圆形或者圆角 canvas.drawCircle()/drawRoundRect()
package com.wkq.workkotlin.custom.cycle
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BitmapShader
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.graphics.Shader
import android.util.AttributeSet
import android.view.View
import com.wkq.workkotlin.R
/**
*@Desc:
*
*@Author: wkq
*
*@Time: 2024/11/7 11:25
*
*/
class CycleBitmapShaderView:View {
val DEFAULT_SIZE = 500
var mContext: Context
var mPaint: Paint
private var mRadius = 0f
var mSize: Int = 0
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, -1)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context, attrs, defStyleAttr
){
mContext = context
val tp = mContext.obtainStyledAttributes(attrs, R.styleable.CycleXFerModeViewStyle)
mRadius = tp.getFloat(R.styleable.CycleXFerModeViewStyle_mRadius, 0f)
tp.recycle()
mPaint = Paint()
mPaint.isAntiAlias = true
mPaint.style = Paint.Style.FILL
}
/**
* BitmapShader 方式绘制圆形
* @param canvas Canvas
*/
private fun processCycleBitmapShader(canvas: Canvas) {
val bitmapShader = BitmapShader(getScaleBitmap(), Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
mPaint.setShader(bitmapShader)
canvas.drawCircle((height / 2.0).toFloat(), (height / 2.0).toFloat(), (height / 2.0).toFloat(), mPaint)
}
/**
* BitmapShader 方式绘制圆形
* @param canvas Canvas
*/
private fun processConnerBitmapShader(canvas: Canvas) {
val bitmapShader = BitmapShader(getScaleBitmap(), Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
mPaint.setShader(bitmapShader)
val mRect = RectF()
mRect.left = 0f
mRect.top = 0f
mRect.right = width.toFloat()
mRect.bottom = height.toFloat()
canvas.drawRoundRect(mRect, mRadius, mRadius, mPaint)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val width: Int = getMeasureSize(widthMeasureSpec)
val height: Int = getMeasureSize(heightMeasureSpec)
mSize = Math.min(width, height)
setMeasuredDimension(mSize, mSize)
}
private fun getMeasureSize(measureSpec: Int): Int {
val mode = MeasureSpec.getMode(measureSpec)
val size = MeasureSpec.getSize(measureSpec)
return if (mode == MeasureSpec.EXACTLY) size else DEFAULT_SIZE
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (mRadius<=0f){
processCycleBitmapShader(canvas)
}else{
processConnerBitmapShader(canvas)
}
}
private fun getScaleBitmap(): Bitmap {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeResource(resources, R.mipmap.test, options)
options.inSampleSize = calcSampleSize(options, mSize, mSize)
options.inJustDecodeBounds = false
return BitmapFactory.decodeResource(resources, R.mipmap.test, options)
}
/**
* 计算缩放比例
*
* @param option
* @param width
* @param height
* @return
*/
private fun calcSampleSize(option: BitmapFactory.Options, width: Int, height: Int): Int {
var originWidth = option.outWidth
var originHeight = option.outHeight
var sampleSize = 1
while (((originWidth shr 1).also {
originWidth = it
}) > width && ((originHeight shr 1).also {
originHeight = it
}) > height) {
sampleSize = sampleSize shl 1
}
return sampleSize
}
}
4.ViewOutlineProvider 方案
思路:
通过view.setOutlineProvider,给我们的View控件设置一个圆形轮廓,然后让View根据轮廓提供者进行切割(使用任何组件)
扩展:
圆角TextView
圆形文字
package com.wkq.workkotlin.custom.cycle
import android.content.Context
import android.graphics.Outline
import android.graphics.Paint
import android.graphics.Rect
import android.os.Build
import android.util.AttributeSet
import android.view.View
import android.view.ViewOutlineProvider
import androidx.appcompat.widget.AppCompatTextView
import com.wkq.workkotlin.R
/**
*@Desc: ViewOutlineProvider 实现 圆形
*
*@Author: wkq
*
*@Time: 2024/11/7 11:25
*
*/
class CycleViewOutlineProviderView: AppCompatTextView {
var mContext: Context
var mPaint: Paint
private var mRadius = 0f
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, -1)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context, attrs, defStyleAttr
){
mContext = context
val tp = mContext.obtainStyledAttributes(attrs, R.styleable.CycleXFerModeViewStyle)
mRadius = tp.getFloat(R.styleable.CycleXFerModeViewStyle_mRadius, 0f)
tp.recycle()
mPaint = Paint()
mPaint.isAntiAlias = true
mPaint.style = Paint.Style.FILL
initView()
}
private fun initView() {
if (Build.VERSION.SDK_INT >= 21) {
val outlineProvider: ViewOutlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
val width = view.width
val height = view.height
if (mRadius <= 0f) {
val rect: Rect = Rect(0, 0, width, height)
outline.setOval(rect) // API>=21
} else {
// 右边圆弧
// val rect: Rect = Rect(0, 0, width, height)
// outline.setRoundRect(rect, height.toFloat()/2)
// 圆角
val rect: Rect = Rect(0, 0, width, height)
outline.setRoundRect(rect, mRadius)
}
}
}
clipToOutline = true
setOutlineProvider(outlineProvider)
}
}
}
总结:
通过四种方式实现了圆形以及圆角形状,主要是通过操作画布,画笔实现了圆形以及圆角的功能.就这四种方案简单说两句
- 1:PorterDuffXfermode API强大能,灵活能绘制各种需求
- 2.clipPath 按照路径绘制圆形圆角,能绘制切割出 各式各样的样式
- 3.BitmapShader 位图着色器 根据画布绘制的形状填充 图片
- 4:ViewOutlineProvider 给View 设置一个轮廓 不仅限于图片(扩展性强)