推薦下,我本身的關於自定義view的一個演示demojava
推薦下,我本身的關於自定義view的一個演示demogithub
ImageView
,能夠使用ImageView
屬性的src
和scaleType
經過src設置的圖片會被裁剪,設置準確大小下scaleType會生效 canvas
先來看下效果吧 bash
圖片角度有兩種方式BitmapShader
(圖片着色器)和PorterDuffXfermode
(圖像疊加覆蓋的規則)app
經過對畫筆Paint
設置shader
和xfermode
來實現圖片的圓角效果。ide
用Bitmap
的像素來做爲圖片或文字的填充。給Paint
設置shder
來使用post
bitMapPaint.shader = bitmapShader
測試
自定義屬性:ui
屬性名 | 屬性類型 | 含義 | 默認值 |
---|---|---|---|
shiv_bg_color |
color/reference | 控件背景色 | Color.TRANSPARENT |
shiv_border_color |
color/reference | 邊框顏色 | Color.WHITE |
shiv_border_width |
dimension/reference | 邊框寬度 | 2dp |
shiv_radius |
dimension/reference | 圓角正方形的邊長 | 5dp |
shiv_radius_x |
dimension/reference | 非圓角矩形的寬 | -1f |
shiv_radius_y |
dimension/reference | 非圓角矩形的長 | -1f(同時設置x,y大於0 纔有效) |
shiv_top_left |
boolean | 左上是否有角度 | true |
shiv_top_right |
boolean | 右上是否有角度 | true |
shiv_bottom_left |
boolean | 左下是否有角度 | true |
shiv_bottom_right |
boolean | 右下是否有角度 | true |
onDraw()重寫 :
刪除spuer.onDraw(),實現本身的圓角邏輯
override fun onDraw(canvas: Canvas?) {
canvas?.drawColor(bgColor)
// BitmapShader實現
canvas?.save()
val bitmap = (drawable as BitmapDrawable).bitmap
val bitmapShader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
val matrix = setBitmapMatrixAndPath(width.toFloat(), height.toFloat(), bitmap)
bitmapShader.setLocalMatrix(matrix)
bitMapPaint.shader = bitmapShader
canvas?.drawPath(clipPath, bitMapPaint)
canvas?.restore()
borderPaint.style = Paint.Style.STROKE
canvas?.drawPath(borderPath, borderPaint)
if (!cornerTopLeftAble) {
borderPaint.style = Paint.Style.FILL
canvas?.drawRect(suppleRectF, borderPaint)
}
}
複製代碼
bitmap
對象,將drawable
轉爲BitmapDrawable
獲取bitmap
對象BitmapShader
對象,須要設置bitmap
,以及端點以外的圖片延伸模式TileMode
, 圖片的matrix
paint
設置shader
後,用canvas
的drawXXX
方法畫出想要的圖形,必須使用設置了shader
的paint
borderPaint
設置顏色,填充模式和描邊寬度便可正常繪製。ShapeBitmaoshaderImageView
繼承AppCompatImageView
,支持一些ImageView
的屬性設置,例如src
,scaleType
等。注意:在實際測試中發現,繪製邊框時,首尾銜接不上,須要在開始的點的左上位置繪製一個邊長爲邊框寬度的一半,顏色爲邊框顏色的正方形用於補充空白部分。修補代碼及效果:
if (!cornerTopLeftAble) {
borderPaint.style = Paint.Style.FILL
canvas?.drawRect(suppleRectF, borderPaint)
}
複製代碼
修補前 | 修補後 |
---|---|
![]() |
![]() |
setBitmapMatrixAndPath(w,h,bitmap) 設置圖片縮放,平移
根據ScaleType
的枚舉值,進行圖片的縮放,平移以達到ImageView
的ScaleType
效果。
/**
* 設置圖片變化的matrix, 裁剪路徑,邊框路徑
*/
private fun setBitmapMatrixAndPath(w: Float, h: Float, bitmap: Bitmap): Matrix {
// 圖片變化的matrix
val matrix = Matrix()
// 圖片縮放比例
val scaleX: Float
val scaleY: Float
// 縮放後的圖片寬高
val bw: Float
val bh: Float
// 移動圓點
var transX = 0f
var transY = 0f
if (isSetSize) {
when(scaleType) {
ScaleType.FIT_XY -> {
// 無論圖片大小,填充整個view
scaleX = w / bitmap.width
scaleY = h / bitmap.height
matrix.setScale(scaleX, scaleY)
setPath(borderWidth, borderWidth, w - borderWidth, h - borderWidth)
}
ScaleType.FIT_CENTER -> {
// fitCenter圖片按比例縮放至View的寬度或者高度(取寬和高的最小值),居中顯示
val scale: Float
if (w < h) {
scale = w / bitmap.width
transY = (h - bitmap.height * scale) / 2
} else {
scale = h / bitmap.height
transX = (w - bitmap.width * scale) / 2
}
matrix.setScale(scale, scale)
matrix.postTranslate(transX, transY)
bw = bitmap.width * scale
bh = bitmap.height * scale
val left = if (transX < 0) borderWidth else transX + borderWidth
val top = if (transY < 0) borderWidth else transY + borderWidth
val right = if (transX < 0) w - borderWidth else transX + bw -borderWidth
val bottom = if (transY < 0) h - borderWidth else transY + bh - borderWidth
setPath(left, top, right, bottom)
}
ScaleType.FIT_START -> {
// 圖片按比例縮放至View的寬度或者高度(取寬和高的最小值),而後居上或者居左顯示
val scale = if (w < h) {
w / bitmap.width
} else {
h / bitmap.height
}
matrix.setScale(scale, scale)
bw = bitmap.width * scale
bh = bitmap.height * scale
val left = borderWidth
val top = borderWidth
val right = if (w < bw) w - borderWidth else bw - borderWidth
val bottom = if (h < bh) h - borderWidth else bh - borderWidth
setPath(left, top, right, bottom)
}
ScaleType.FIT_END -> {
// 圖片按比例縮放至View的寬度或者高度(取寬和高的最小值),而後居下或者居右顯示
val scale: Float
if (w < h) {
scale = w / bitmap.width
transY = h - bitmap.height * scale
} else {
scale = h / bitmap.height
transX = w - bitmap.width * scale
}
matrix.setScale(scale, scale)
matrix.postTranslate(transX, transY)
bw = bitmap.width * scale
bh = bitmap.height * scale
val left = if (transX < 0) borderWidth else transX + borderWidth
val top = if (transY < 0) borderWidth else transY + borderWidth
val right = if (transX < 0) w - borderWidth else transX + bw - borderWidth
val bottom = if (transY < 0) h - borderWidth else transY + bh - borderWidth
setPath(left, top, right, bottom)
}
ScaleType.CENTER -> {
// 按照圖片原始大小,居中顯示,多餘部分裁剪
transX = (w - bitmap.width) / 2
transY = (h - bitmap.height) / 2
matrix.postTranslate(transX, transY)
setPath(if (transX < 0) borderWidth else transX + borderWidth,
if (transY < 0) borderWidth else transY + borderWidth,
if (transX < 0) w - borderWidth else transX + bitmap.width - borderWidth,
if (transY < 0) h - borderWidth else transY + bitmap.height - borderWidth)
}
ScaleType.CENTER_INSIDE -> {
// centerInside的目標是將原圖完整的顯示出來,故按比例縮放原圖,居中顯示
val scale: Float
if (w < h) {
scale = w / bitmap.width
transY = (h - bitmap.height * scale) / 2
} else {
scale = h / bitmap.height
transX = (w - bitmap.width * scale) / 2
}
matrix.setScale(scale, scale)
matrix.postTranslate(transX, transY)
bw = bitmap.width * scale
bh = bitmap.height * scale
val left = if (transX < 0) borderWidth else transX + borderWidth
val top = if (transY < 0) borderWidth else transY + borderWidth
val right = if (transX < 0) w - borderWidth else transX + bw - borderWidth
val bottom = if (transY < 0) h - borderWidth else transY + bh -borderWidth
setPath(left, top, right, bottom)
}
ScaleType.CENTER_CROP -> {
// centerCrop的目標是將ImageView填充滿,故按比例縮放原圖,居中顯示
val scale: Float
if (w > h) {
scale = w / bitmap.width
transY = (h - bitmap.height * scale) / 2
} else {
scale = h / bitmap.height
transX = (w -bitmap.width * scale) / 2
}
matrix.setScale(scale, scale)
matrix.postTranslate(transX, transY)
bw = bitmap.width * scale
bh = bitmap.height * scale
val left = if (transX < 0) borderWidth else transX + borderWidth
val top = if (transY < 0) borderWidth else transY + borderWidth
val right = if (transX < 0) w - borderWidth else transX + bw - borderWidth
val bottom = if (transY < 0) h - borderWidth else transY + bh -borderWidth
setPath(left, top, right, bottom)
}
ScaleType.MATRIX -> {
// 按照原圖大小從左上角繪製,多餘部分裁剪
bw = if (w < bitmap.width) w else bitmap.width.toFloat()
bh = if (h < bitmap.height) h else bitmap.height.toFloat()
setPath(borderWidth, borderWidth, bw - borderWidth, bh - borderWidth)
}
else -> {}
}
} else {
scaleX = w / bitmap.width
scaleY = h / bitmap.height
matrix.setScale(scaleX, scaleY)
setPath(borderWidth, borderWidth, w - borderWidth, h -borderWidth)
}
return matrix
}
複製代碼
ScaleType簡單說明:
ScaleType | 含義 |
---|---|
FIT_XY |
無論圖片大小,填充整個view |
FIT_CENTER |
圖片按比例縮放至View的寬度或者高度(取寬和高的最小值),居中顯示 |
FIT_START |
圖片按比例縮放至View的寬度或者高度(取寬和高的最小值),居上或者居左顯示 |
FIT_END |
圖片按比例縮放至View的寬度或者高度(取寬和高的最小值),居下或者居右顯示 |
CENTER |
按照圖片原始大小,居中顯示,多餘部分裁剪 |
CENTER_INSIDE |
目標是將原圖完整的顯示出來,故按比例縮放原圖,居中顯示 |
CENTER_CROP |
目標是將view填充滿,故按比例縮放原圖,居中顯示 |
MATRIX |
按照原圖大小從左上角繪製,多餘部分裁剪 |
setPath(left, right, top, bottom) 設置裁剪路徑和邊框路徑
setPath
主要根據傳進來的裁剪矩形框的值進行裁剪路徑和邊框路徑的合成。
邊框四邊 = 裁剪框四邊向外擴張borderwidth大小;
邊框角度值 = 裁剪框角度值 + borderwidth / 2
/**
* 設置裁剪路徑和邊框路徑
* @param left 裁剪框的left
* @param top 裁剪框的top
* @param right 裁剪框的right
* @param bottom 裁剪框的bottom
*/
private fun setPath(left: Float, top: Float, right: Float, bottom: Float) {
clipPath.reset()
borderPath.reset()
val w = right - left
val h = bottom - top
setRadius(w, h)
val borderLeft = left - borderWidth / 2
val borderTop = top - borderWidth / 2
val borderRight = right + borderWidth / 2
val borderBottom = bottom + borderWidth / 2
val borderRadiusX = radiusX + borderWidth / 2
val borderRadiusY = radiusY + borderWidth / 2
val bw = borderRight - borderLeft
val bh = borderBottom - borderTop
suppleRectF.left = borderLeft - borderWidth / 2
suppleRectF.top = borderTop - borderWidth / 2
suppleRectF.right = borderLeft
suppleRectF.bottom = borderTop
// 圓角或橢圓角的矩形
val topLeftRectF = RectF()
val topRightRectF = RectF()
val bottomLeftRectF = RectF()
val bottomRightRectF = RectF()
if (radiusX <= 0 && radiusY <= 0) {
// 沒有圓角
clipPath.addRect(left, top, right, bottom, Path.Direction.CW)
borderPath.addRect(borderLeft, borderTop, borderRight, borderBottom, Path.Direction.CW)
} else {
// 有圓角
if (cornerTopLeftAble) {
// 裁剪
// 左上角
topLeftRectF.left = left
topLeftRectF.top = top
topLeftRectF.right = left + radiusX * 2
topLeftRectF.bottom = top + radiusY * 2
clipPath.addArc(topLeftRectF, 180f, 90f)
// 邊框
// 左上角
topLeftRectF.left = borderLeft
topLeftRectF.top = borderTop
topLeftRectF.right = borderLeft + borderRadiusX * 2
topLeftRectF.bottom = borderTop + borderRadiusY * 2
borderPath.moveTo(borderLeft, borderTop + borderRadiusY)
borderPath.addArc(topLeftRectF, 180f, 90f)
borderPath.moveTo(borderLeft + borderRadiusX, borderTop)
} else {
clipPath.moveTo(left, top)
borderPath.moveTo(borderLeft, borderTop)
}
clipPath.lineTo(if (cornerTopRightAble) right - radiusX else right , top)
if (bw != borderRadiusX * 2) {
borderPath.lineTo(if (cornerTopRightAble) borderRight - borderRadiusX else borderRight , borderTop)
}
if (cornerTopRightAble) {
// 右上角
topRightRectF.left = right - radiusX * 2
topRightRectF.top = top
topRightRectF.right = right
topRightRectF.bottom = top + radiusY * 2
clipPath.addArc(topRightRectF, 270f, 90f)
// 右上角
topRightRectF.left = borderRight - borderRadiusX * 2
topRightRectF.top = borderTop
topRightRectF.right = borderRight
topRightRectF.bottom = borderTop + borderRadiusY * 2
borderPath.addArc(topRightRectF, 270f, 90f)
borderPath.moveTo(borderRight, borderTop + borderRadiusY)
}
clipPath.lineTo(right, if (cornerBottomRightAble) bottom - radiusY else bottom)
if (bh != borderRadiusY * 2) {
borderPath.lineTo(borderRight, if (cornerBottomRightAble) borderBottom - borderRadiusY else borderBottom)
}
if (cornerBottomRightAble) {
// 右下角
bottomRightRectF.left = right - radiusX * 2
bottomRightRectF.top = bottom - radiusY * 2
bottomRightRectF.right = right
bottomRightRectF.bottom = bottom
clipPath.addArc(bottomRightRectF, 0f, 90f)
// 右下角
bottomRightRectF.left = borderRight - borderRadiusX * 2
bottomRightRectF.top = borderBottom - borderRadiusY * 2
bottomRightRectF.right = borderRight
bottomRightRectF.bottom = borderBottom
borderPath.addArc(bottomRightRectF, 0f, 90f)
borderPath.moveTo(borderRight - borderRadiusX ,borderBottom)
}
clipPath.lineTo(if (cornerBottomLeftAble) left + radiusX else left, bottom)
if (bw != borderRadiusX * 2) {
borderPath.lineTo(if (cornerBottomLeftAble) borderLeft + borderRadiusX else borderLeft, borderBottom)
}
if (cornerBottomLeftAble) {
// 左下角
bottomLeftRectF.left = left
bottomLeftRectF.top = bottom - radiusY * 2
bottomLeftRectF.right = left + radiusX * 2
bottomLeftRectF.bottom = bottom
clipPath.addArc(bottomLeftRectF, 90f, 90f)
// 左下角
bottomLeftRectF.left = borderLeft
bottomLeftRectF.top = borderBottom - borderRadiusY * 2
bottomLeftRectF.right = borderLeft + borderRadiusX * 2
bottomLeftRectF.bottom = borderBottom
borderPath.addArc(bottomLeftRectF, 90f, 90f)
borderPath.moveTo(borderLeft, borderBottom - borderRadiusY)
}
clipPath.lineTo(left, if (cornerTopLeftAble) top + radiusY else top)
if (cornerTopLeftAble) {
clipPath.lineTo(left, top + radiusY)
}
if (cornerTopRightAble) {
clipPath.lineTo(right - radiusX, top)
}
if (cornerBottomRightAble) {
clipPath.lineTo(right, bottom - radiusY)
}
if (cornerBottomLeftAble) {
clipPath.lineTo(left + radiusX, bottom)
}
if (bh != borderRadiusY * 2) {
borderPath.lineTo(borderLeft, if (cornerTopLeftAble) borderTop + borderRadiusY else borderTop)
}
}
}
複製代碼
setRadius(w,h) 設置圓角值
/**
* 設置圓角值
*/
private fun setRadius(w: Float, h: Float) {
if (radiusX < 0 || radiusY < 0) {
if (cornerRadius < 0) {
cornerRadius = 0f
}
radiusX = cornerRadius
radiusY = cornerRadius
}
if (radiusX > w / 2) {
radiusX = w / 2
}
if (radiusY > h / 2) {
radiusY = h / 2
}
}
複製代碼
若是設置了角度的x,y且大於等於0,則使用,不然使用圓角值(>=0)
PorterDuffXfermode
是指目標圖片和源圖片的疊加覆蓋規則,給Paint
設置xfermode
來使用,須要注意PorterDuffXfermode
須要設置具體某種繪製規則(PorterDuff.Mode
)
bitMapPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
注意: xfermode和BitmapShader角度圖片,裁剪路徑和邊框的設置是同樣的。 不一樣的是xfermode須要用裁剪路徑去生成源圖,設置的圖片用做目標圖來進行繪製,而bitmapshader直接使用裁剪路徑。
ShapeXfermodeImageView 自定義屬性:
屬性名 | 屬性類型 | 含義 | 默認值 |
---|---|---|---|
sxiv_bg_color |
color/reference | 控件背景色 | Color.TRANSPARENT |
sxiv_border_color |
color/reference | 邊框顏色 | Color.WHITE |
sxiv_border_width |
dimension/reference | 邊框寬度 | 2dp |
sxiv_radius |
dimension/reference | 圓角正方形的邊長 | 5dp |
sxiv_radius_x |
dimension/reference | 非圓角矩形的寬 | -1f |
sxiv_radius_y |
dimension/reference | 非圓角矩形的長 | -1f(同時設置x,y大於0 纔有效) |
sxiv_top_left |
boolean | 左上是否有角度 | true |
sxiv_top_right |
boolean | 右上是否有角度 | true |
sxiv_bottom_left |
boolean | 左下是否有角度 | true |
sxiv_bottom_right |
boolean | 右下是否有角度 | true |
onDraw()重寫:
刪除spuer.onDraw(),實現本身的圓角邏輯
override fun onDraw(canvas: Canvas?) {
canvas?.drawColor(bgColor)
// BitmapShader實現
val saved = canvas?.saveLayer(null, null, Canvas.ALL_SAVE_FLAG)
val dstBitmap = (drawable as BitmapDrawable).bitmap
val matrix = setBitmapMatrixAndPath(width.toFloat(), height.toFloat(), dstBitmap)
val srcBitmap = createSrcBitmap(width, height)
canvas?.drawBitmap(dstBitmap, matrix, bitMapPaint)
bitMapPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
canvas?.drawBitmap(srcBitmap, 0f, 0f, bitMapPaint)
bitMapPaint.xfermode = null
canvas?.restoreToCount(saved?: 0)
borderPaint.style = Paint.Style.STROKE
canvas?.drawPath(borderPath, borderPaint)
if (!cornerTopLeftAble) {
borderPaint.style = Paint.Style.FILL
canvas?.drawRect(suppleRectF, borderPaint)
}
}
複製代碼
Paint
實例進行繪製xfermode
以前的drawBitmap
是繪製目標圖,以後的是源圖PorterDuff.Mode
是DST_IN
值,保留目標圖和源圖的交集部分createSrcBitmap(w,h)
,根據裁剪路徑繪製源圖PorterDuff.ModeJ簡單說明:
createSrcBitmap(w,h)
根據裁剪路徑繪製源圖
/**
* 獲取源圖biatmap,用於截出形狀圖
*/
private fun createSrcBitmap(w: Int, h: Int): Bitmap {
val srcBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
srcBitmap.eraseColor(Color.TRANSPARENT)
val canvas = Canvas(srcBitmap)
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
style = Paint.Style.FILL
}
canvas.drawPath(clipPath, paint)
return srcBitmap
}
複製代碼
setBitmapMatrixAndPath(), setPath(), setRadius() 和ShpaeShaderImageView的方法是同樣的
以上就是BitmapShader
和Xfermode
實現角度圖片的過程