自定义控件绘制(Paint之Shader)篇十一

参考:

  1. https://blog.csdn.net/harvic880925/article/details/52039081

shader称为着色器,用来给图片上色用的;
Shader类只是一个基类,只有两个方法setLocalMatrix(Matrix localM)getLocalMatrix(Matrix localM)用来设置坐标变换矩阵的;
Shader类与ColorFiler一样,其实是一个空类,它的功能的实现,主要是靠它的派生类来实现的。

Shader子类

BitmapShader

构造函数:

public BitmapShader(Bitmap bitmap, TileMode tileX, TileMode tileY)

参数:

  • bitmap: 用来指定图案;
  • tileX: 用来指定当X轴超出单个图片大小时时所使用的重复策略;
  • tileY: 同上,用于指定当Y轴超出单个图片大小时时所使用的重复策略;取值有:
    • TileMode.CLAMP:用边缘色彩填充多余空间
    • TileMode.REPEAT:重复原图像来填充多余空间
    • TileMode.MIRROR:重复使用镜像模式的图像来填充多余空间
val bmp = BitmapFactory.decodeResource(resources, R.mipmap.sanjing)
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    shader = BitmapShader(bmp, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
}
override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    // 在矩形内使用指定了shader的画笔作画
   canvas.drawRect(0f, 0f, width.toFloat(), height * 2 / 3.toFloat(), paint)
}
Repeat

如上图,效果:
使用X轴和Y轴都使用REPEAT模式下,在超出单个图像的区域后,就会重复绘制这个图像

Clamp

如上图,效果:
当控件区域超过当前单个图片的大小时,空白位置的颜色填充就用图片的边缘颜色来填充;

要填充横向和竖向时,是先填充竖向的!上图中的右下部分

mirror

如上图,效果:
镜相效果其实就是在显示下一图片的时候,就相当于两张图片中间放了一个镜子一样;

填充模式混用

Mirror与Repeat混用
// x轴上重复,y上镜像
BitmapShader(bmp, Shader.TileMode.REPEAT, Shader.TileMode.MIRROR)
如上效果图,注意:红色线上下左右

无论哪两种模式混合,我们在理解时只需要记着填充顺序是先填充Y轴,然后再填充X轴!这样效果图就很好理解了;

其他混用也很好理解;

绘图位置与模式关系

上面的rect的设置,都是比较大的,我们将rect缩小,看看效果

// 矩形小于图片大小
canvas.drawRect(100f, 100f, 320f, 200f, paint)
image.png

如上图,可以看到图片,像是从原图片上裁剪了一块,进行了绘制;

其实这正说明了一个问题:无论你利用绘图函数绘多大一块,在哪绘制,与Shader无关。因为Shader总是在**控件的左上角**开始,而你绘制的部分只是显示出来的部分而已。没有绘制的部分虽然已经生成,但只是不会显示出来罢了。

望眼镜效果

Paint设置了Shader以后,无论我们绘图位置在哪,Shader中的图片都是从控件的左上角开始填充的,而我们所使用的绘图函数只是用来指定哪部分显示出来,所以当我们在手指按下位置画上一个圆形时,就会把圆形部分的图像显示出来了,看起来就是个望远镜效果。

val bmp = BitmapFactory.decodeResource(resources, R.mipmap.animal_)
var bmpBG: Bitmap? = null

var dx = -1f
var dy = -1f

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    if (bmpBG == null) {
        // bitmap设置为控件宽高
        bmpBG = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
        val canvasBG = Canvas(bmpBG)
        canvasBG.drawBitmap(bmp, null, RectF(0f, 0f, width * 1.0f, height * 1.0f), paint)
    }

    // 画出局部
    if (dx != -1f && dy != -1f) {
        paint.shader = BitmapShader(bmpBG, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
        canvas.drawCircle(dx, dy, 150f, paint)
    }
}

override fun onTouchEvent(event: MotionEvent): Boolean {
    when (event.action) {
        MotionEvent.ACTION_DOWN,
        MotionEvent.ACTION_MOVE -> {
            dx = event.x
            dy = event.y
        }
        else -> {
            dx = -1f
            dy = -1f
        }
    }
    postInvalidate()
    return true
}
效果图

BitmapShader生成不规则头像

用 xfermode可以实现,这里采用BitmapShader来实现;

val bmp = BitmapFactory.decodeResource(resources, R.mipmap.sanjing)

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    val shader = BitmapShader(bmp, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)

    val matrix = Matrix()
    val size = Math.min(bmp.width, bmp.height)
    val scale = size / Math.min(width, height).toFloat()
    matrix.setScale(scale, scale)  // Matrix 缩放
    shader.setLocalMatrix(matrix)

    paint.shader = shader
    canvas.drawCircle(size / 2.toFloat(), size / 2.toFloat(),
            size / 2.toFloat(), paint)
}

// 强制控件大小(正方形)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val size = Math.min(bmp.width, bmp.height)
    setMeasuredDimension(size, size)
}
shader实现圆角图片

如果是其他形状,如五角星,通过path来绘制即可;

LinearGradient线性渐变

构造函数

/**
 * (x0, y0), (x1,y1)分别表示开始点与结束点
 * color0 起始点颜色,颜色值必须使用0xAARRGGBB形式的16进制表示!表示透明度的AA一定不能少;
 * color1 终点颜色
 * tile 填充模式
 **/
public LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1,
           TileMode tile) 
/**
 * 基本与上类似
 * colors[]用于指定渐变的颜色值数组;
 * positions[]与渐变的颜色相对应,取值是0-1的float类型,表示在每一个颜色在整条渐 变线中的百分比位置 
 **/
public LinearGradient(float x0, float y0, float x1, float y1, int colors[],
           float positions[], TileMode tile) 

双色渐变示例:

paint.shader = LinearGradient(0f, (height / 2).toFloat(), width.toFloat(),
            (height / 2).toFloat(), -0x10000, -0xff0100, Shader.TileMode.CLAMP)
canvas.drawRect(0f, 0f, width * 1.0f, height * 1.0f, paint)
双色渐变

多色渐变示例:

val colors = intArrayOf(-0x10000, -0xff0100, -0xffff01, -0x100, -0xff0001)
val pos = floatArrayOf(0f, 0.2f, 0.4f, 0.6f, 1.0f)  // 20%,20%,20%,20%,40%
val multiGradient = LinearGradient(0f, (height / 2).toFloat(), width.toFloat(),
     (height / 2).toFloat(), colors, pos, Shader.TileMode.CLAMP)
paint.shader = multiGradient
canvas.drawRect(0f, 0f, width * 1.0f, height * 1.0f, paint)
image.png

??如上图,这里的 变线中的百分比位置不太理解

填充模式

上面都是 clamp边缘填充,我们使用下repeat填充,渐变点是从(0,0)到屏幕的中间点(width/2,height.2):

val multiGradient = LinearGradient(0f, 0f, 
        width / 2.toFloat(), height / 2.toFloat(),
        colors, pos, Shader.TileMode.REPEAT)
repeat填充

Mirror很容易理解;

填充方式是什么?

类似bitmapShader也是从控件左上角开始填充;

// 多个颜色
val colors = intArrayOf(-0x10000, -0xff0100, -0xffff01, -0x100, -0xff0001)
val pos = floatArrayOf(0f, 0.2f, 0.4f, 0.6f, 1.0f)  // 20%,20%,20%,20%,40%
val multiGradient = LinearGradient(0f, 0f, width / 2.toFloat(), height / 2.toFloat(), colors, pos, Shader.TileMode.REPEAT)
paint.shader = multiGradient
// 减小区域
canvas.drawRect(100f, 100f, 260f, 200f, paint)
类似切片

无论哪种Shader,都是从控件的左上角开始填充的,利用canvas.drawXXX系列函数只是用来指定显示哪一块

image.png
文字闪动效果,请参考原博客,
init {
    setLayerType(LAYER_TYPE_SOFTWARE, null)
}

private var shade: LinearGradient? = null
private var mDx = 0f
private var anim: ValueAnimator? = null

override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
    super.onLayout(changed, left, top, right, bottom)

    if (shade == null) {
        shade = LinearGradient((-measuredWidth).toFloat(), 0f, 0f, 0f,
                intArrayOf(currentTextColor, -0xff0100, currentTextColor),
                floatArrayOf(0f, 0.5f, 1f),
                Shader.TileMode.CLAMP
        )

        anim = ValueAnimator.ofFloat(0f, 2 * measuredWidth * 1.0f).apply {
            duration = 1500
            repeatMode = ValueAnimator.RESTART
            repeatCount = ValueAnimator.INFINITE
            addUpdateListener { it ->
                mDx = it.animatedValue as Float
                postInvalidate()
            }
        }
        anim?.start()
    }
}

override fun onDraw(canvas: Canvas?) {
    val matrix = Matrix()
    matrix.setTranslate(mDx, 0f)  // 设置偏移
    shade?.setLocalMatrix(matrix)
    paint.shader = shade
    super.onDraw(canvas)
}

override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    anim?.let {
        it.cancel()
    }
}
效果图

RadialGradient 放射渐变

构造函数:

// 双色
public RadialGradient(float centerX, float centerY, float radius,
             int centerColor, int edgeColor,TileMode tileMode) 
// 多色
public RadialGradient(float centerX, float centerY, float radius,
             int colors[], float stops[],
             TileMode tileMode) {

参数说明:

  • centerX,Y: 渐变中心点;
  • radius: 渐变半径;
  • centerColor:渐变起始颜色,取值类型必须是八位的0xAARRGGBB色值!透明底Alpha值不能省略,不然不会显示出颜色。
  • edgeColor:结束颜色,同上;
  • colors 与 stops 与 LinearGradient类似;stop数组的起始和终止数值设为0和1;

示例

    private var shade: RadialGradient? = null
    val paint = Paint().apply {}
    val radius = 400f

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // 双色
//        shade = RadialGradient(width / 2.toFloat(), height / 2.toFloat(),
//                100f, 0xffff0000.toInt(), 0xff00ff00.toInt(), Shader.TileMode.REPEAT)

        // 多色
        val colors = intArrayOf(-0x10000, -0xff0100, -0xffff01, -0x100)
        val stops = floatArrayOf(0f, 0.2f, 0.5f, 1f)
        shade = RadialGradient(width / 2.toFloat(), height / 2.toFloat(),
                radius, colors, stops, Shader.TileMode.REPEAT)

        paint.shader = shade
        canvas.drawCircle(width / 2.toFloat(), height / 2.toFloat(),
                radius, paint)
    }
多颜色
双色

填充模式

比较好理解;

填充方式

shader都是从控件的左上角开始填充的;不信,就看

val colors = intArrayOf(-0x10000, -0xff0100, -0xffff01, -0x100)
val stops = floatArrayOf(0f, 0.2f, 0.5f, 1f)
shade = RadialGradient(width / 2.toFloat(), height / 2.toFloat(),
                100f, colors, stops, Shader.TileMode.REPEAT)
paint.shader = shade
canvas.drawCircle(200f, 200f,150f, paint)
image.png

水波纹按钮效果

比较好理解,如下代码:

init {
    setLayerType(LAYER_TYPE_SOFTWARE, null)
}

private var shade: RadialGradient? = null
val paint = Paint().apply {}
var currentX: Float = 0f
var currentY: Float = 0f
val DEFAULT_RADIUS = 80f
var radius = 0f
    set(value) {
        field = value
        if (value > 0) {
            shade = RadialGradient(currentX, currentY, value,
                    0x00ffffff, 0xFF58FAAC.toInt(), Shader.TileMode.CLAMP)
            paint.shader = shade
        }
        postInvalidate()
    }
var anim: Animator? = null

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    canvas.drawCircle(currentX, currentY, radius, paint)
}

override fun onTouchEvent(event: MotionEvent): Boolean {
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            currentX = event.x
            currentY = event.y
            radius = DEFAULT_RADIUS
            return true
        }
        MotionEvent.ACTION_UP -> {
            startAnim(400)
        }
    }
    return super.onTouchEvent(event)
}

private fun startAnim(duration: Long) {
    anim?.let { it.cancel() }
    anim = ObjectAnimator.ofFloat(this, "radius", DEFAULT_RADIUS, width * 1.0f).apply {
        setDuration(duration)
        interpolator = AccelerateInterpolator()
        addListener(object : Animator.AnimatorListener {
            override fun onAnimationRepeat(animation: Animator) {
            }
            override fun onAnimationEnd(animation: Animator) {
                radius = 0f     // 清除效果
            }
            override fun onAnimationCancel(animation: Animator) {
            }
            override fun onAnimationStart(animation: Animator) {
            }
        })
    }
    anim?.start()
}

你可能感兴趣的:(自定义控件绘制(Paint之Shader)篇十一)