自定义控件绘制(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()
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容