Android 自定义 View进阶 - Shader

Posted 星火燎原2016

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 自定义 View进阶 - Shader相关的知识,希望对你有一定的参考价值。

不知曾几何时,渐变色变得流行起来了,各大手机厂商都发布了各自的渐变色手机,同时越来越多的 App ,也开始应用了渐变色的设计。为了满足工作需要,我们也要学习下 android 中的渐变着色器 Shader。

效果图

Shader 介绍

Shader 单词为 着色器,在 Android 中的文档解释为:

Shader is the based class for objects that return horizontal spans of colors during drawing. A subclass of Shader is installed in a Paint calling paint.setShader(shader). After that any object (other than a bitmap) that is drawn with that paint will get its color(s) from the shader

着色器是在绘制过程中返回水平颜色范围的对象的基类。 Shader的子类安装在Paint中,调用paint.setShader(着色器)之后,使用该绘制绘制的任何对象(位图除外)都将从着色器中获取其颜色

Shader 相关类:

可以看出: Shader 有五个子类

  • BitmapShader 位图渲染
  • ComposeShader 组合渲染
  • LinearGradient 线性渲染
  • SweepGradient 梯度渲染
  • RadialGradient 光束渲染

Shader 用法

(1)调用 Shader 子类对象的构造方法创建着色器,设定 渲染模式。

// 创建线性着色器
linearGradient = new LinearGradient(0, 0, mViewWidth, 0,
                new int[]Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW,
                new float[]0.2f, 0.5f, 0.8f, 0.9f,
                Shader.TileMode.REPEAT);

其中渲染模式 TileMode 有:

  • CLAMP 边缘拉伸模式,他会拉伸边缘的一个像素来填充其他区域。
  • REPEAT 重复模式,水平和垂直重复着色器的图像
  • MIRROR 镜像模式,水平和垂直重复着色器的图像,交替镜像,使相邻的图像始终接缝

(2)给画笔设置 Shader 着色器

// 为画笔设置渐变渲染器
paint.setShader(linearGradient);

(3)在 onDraw() 方法中使用 paint 进行绘制。

canvas.drawRect(rectF, paint);

LinearGradient

LinearGradient 构造方法:

public LinearGradient(float x0, float y0, float x1, float y1,@ColorInt int color0,           @ColorInt int color1, @NonNull TileMode tile)
            
public LinearGradient(float x0, float y0, float x1, float y1, 
   @NonNull @ColorInt int colors[],@Nullable float positions[], @NonNull TileMode tile)            

上面一个构造函数很好理解,(x0,y0),(x1,y1) 分别表示起点和终点的坐标,颜色从 color0 到 color1 线性变化;

下面一个构造函数参数有点不好理解了。我们先看下使用这个构造函数后的效果:

/**
 * 线性渐变
 */
public class LinearGradientView extends View 

    private Paint paint;
    private Paint linePaint;
    private LinearGradient linearGradient;
    private int mViewWidth;
    private int mViewHeight;
    private RectF rectF;

    public LinearGradientView(Context context) 
        super(context);
        init();
    

    public LinearGradientView(Context context, AttributeSet attrs) 
        super(context, attrs);
        init();
    

    private void init() 
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        linePaint.setStrokeWidth(DensityUtil.dp2Px(getContext(), 2));
        linePaint.setStyle(Paint.Style.FILL);
        linePaint.setColor(Color.BLACK);
        linePaint.setTextSize(DensityUtil.dp2Px(getContext(), 14));
        linePaint.setTextAlign(Paint.Align.CENTER);
    

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) 
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidth = w;
        mViewHeight = h;

        rectF = new RectF(0, 0, mViewWidth, mViewHeight);
        /**
         * 渐变范围为 左上角顶点 (0,0) -> 右上角顶点 (mViewWidth,0) ,
         * 超出这个范围以 Shader.TileMode.REPEAT 重复的形式填充, 颜色渐变的方向为 水平方向,
         *
         *
         * 如果渐变范围:  (0,0) -> (mViewWidth,mViewHeight), 则 渐变方向为 对角线方向
         */
        linearGradient = new LinearGradient(0, 0, mViewWidth, 0,
                new int[]Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW,
                new float[]0.2f, 0.5f, 0.8f, 0.9f,
                Shader.TileMode.REPEAT);
        paint.setShader(linearGradient);

    

    @Override
    protected void onDraw(Canvas canvas) 
        super.onDraw(canvas);
        canvas.drawRect(rectF, paint);
        for (int i = 0; i <= 10; i++) 
            canvas.drawLine(rectF.width() * i * 0.1f, rectF.bottom, rectF.width() * i * 0.1f, rectF.bottom - 20, linePaint);
            canvas.drawText(String.valueOf(i / 10f), rectF.width() * i * 0.1f, rectF.bottom - 30, linePaint);
        
    


结论: int colors[] 数组表示颜色数组,float positions[] 数组表示各个颜色在渲染范围内的位置点,上面我们在 0.2,0.5,0.8,0.9 这四个位置点上设置了 RED, GREEN, BLUE, YELLOW 四个颜色值, 从效果图上看,这四种颜色值刚好出现在了这四个位置点上,并且与相邻位置点上颜色形成渐变效果,同时,0.5 与 0.2 相间隔 0.3 ,0.5 与 0.8 也是相间隔 0.3,所以 GREEN 左边绿色渐变色和右边绿色渐变色的宽度是一样的,而 0.8 与 0.5 相间隔 0.3,0.8 与 0.9 相间隔 0.1 ,所以左边蓝色渐变色要比右边蓝色渐变色宽些,调整 当前位置点的左右位置点的间距大小可以调整这个颜色的渐变宽度。

仿 iPhone 滑动解锁

通过属性动画不断的改变 Matrix X 坐标值,重新绘制可以实现文字渐变动画效果

public class LinearShaderTextView extends AppCompatTextView 

    private static final String TAG = "LinearShaderTextView";
    private int mViewWidth;
    private int mViewHeight;
    private LinearGradient linearGradient;
    private Matrix gradientMatrix;
    private float value;

    public LinearShaderTextView(Context context) 
        this(context, null);
    

    public LinearShaderTextView(Context context, AttributeSet attrs) 
        super(context, attrs);
        init();
    

    private void init() 

    

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) 
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidth = w;
        mViewHeight = h;

        value = -mViewWidth;
        // 获取当前绘制 TextView 的 画笔对象
        Paint paint = getPaint();

        int paintColor = paint.getColor();
        Log.e(TAG, "onSizeChanged: paintColor = " + paintColor);
        int currentTextColor = getCurrentTextColor();
        Log.e(TAG, "onSizeChanged: currentTextColor = " + currentTextColor);

        // 自定义渐变渲染器
        linearGradient = new LinearGradient(0, 0,
                mViewWidth, mViewHeight,
                new int[]currentTextColor, Color.WHITE, currentTextColor,
                new float[]0.25f, 0.5f, 0.75f,
                Shader.TileMode.CLAMP);

        // 为画笔设置渐变渲染器
        paint.setShader(linearGradient);
        // 创建矩阵
        gradientMatrix = new Matrix();

        startAnimator();
    

    @Override
    protected void onDraw(Canvas canvas) 
        super.onDraw(canvas);
        if (linearGradient != null) 
            gradientMatrix.setTranslate(value, 0);
            // 为着色器设置矩阵
            linearGradient.setLocalMatrix(gradientMatrix);
        
    

    public void startAnimator() 
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(-mViewWidth, mViewWidth);
        valueAnimator.setDuration(1500);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() 
            @Override
            public void onAnimationUpdate(ValueAnimator animation) 
                value = (float) animation.getAnimatedValue();
                invalidate();
            
        );
        valueAnimator.start();
    


效果:

BitmapShader

BitmapShader 可以实现各种形状的图片控件,下面实现圆形图片。

/**
 * BitmapShader 实现各形状的图片,继承自 ImageView
 */
public class BitmapShaderShapeImageView extends android.support.v7.widget.AppCompatImageView 

    private Paint paint;
    private BitmapShader bitmapShader;
    private int mViewWidth;
    private int mViewHeight;
    private Bitmap bitmap;
    private Random random;
    private String[] shapes = "RoundRectangle", "Circle", "Triangle";
    private int index;
    private RectF rectF;


    public BitmapShaderShapeImageView(Context context) 
        super(context);
        init();
    

    public BitmapShaderShapeImageView(Context context, AttributeSet attrs) 
        super(context, attrs);
        init();
    

    private void init() 
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.key);
        bitmapShader = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);
        paint.setShader(bitmapShader);

        random = new Random();
    

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getMeasuredSize(widthMeasureSpec, bitmap.getWidth());
        int height = getMeasuredSize(heightMeasureSpec, bitmap.getHeight());
        setMeasuredDimension(width, height);
    

    private int getMeasuredSize(int measureSpec, int defSize) 
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        int value;
        if (mode == MeasureSpec.EXACTLY) 
            value = size;
         else 
            value = defSize;
        
        return value;
    

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) 
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidth = w;
        mViewHeight = h;
        rectF = new RectF(mViewWidth / 5f, mViewHeight / 4f, mViewWidth * 4 / 5f, mViewHeight * 3 / 4f);
    

    @Override
    protected void onDraw(Canvas canvas) 
//        super.onDraw(canvas);
        switch (index % shapes.length) 
            case 0:
                canvas.drawRoundRect(rectF, 30, 30, paint);
                break;
            case 1:
                canvas.drawCircle(mViewWidth / 2f, mViewHeight / 2f, 100, paint);
                break;
            case 2:
                canvas.drawOval(rectF, paint);
                break;
        
    


    @Override
    public boolean onTouchEvent(MotionEvent event) 
        if (event.getAction() == MotionEvent.ACTION_DOWN) 
            index++;
            invalidate();
        
        return true;
    


效果:

SweepGradient

SweepGradient 结合属性动画可以扫描效果。

public class SweepGradientView extends View 

    private static final String TAG = "SweepGradientView";
    private Paint paint;
    private int mViewWidth;
    private int mViewHeight;
    private float centerX;
    private float centerY;
    private float radius;
    private int degree = -90;

    public SweepGradientView(Context context) 
        super(context);
        init();
    

    public SweepGradientView(Context context, AttributeSet attrs) 
        super(context, attrs);
        init();
    

    private void init() 
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) 
        super.onSizeChanged(w, h, oldw, oldh);
        Log.e(TAG, "onSizeChanged: ");
        mViewWidth = w;
        mViewHeight = h;

        centerX = mViewWidth / 2f;
        centerY = mViewHeight / 2f;

        radius = Math.min(mViewWidth, mViewHeight) / 2f;

        /**
         *  扇形,绕着一个中心点进行扫描的渐变着色器。
         *  中心点为 ( mViewWidth / 2, mViewHeight / 2 ) , 顺时针 0 度开始渐变
         */
        SweepGradient sweepGradient = new SweepGradient(centerX, centerY,
                Color.parseColor("#FFB9B3B3"),
                Color.parseColor("#ffcc1111"));

        paint.setShader(sweepGradient);

        startAnimator();

    

    @Override
    protected void onDraw(Canvas canvas) 
        super.onDraw(canvas);
        canvas.save();
        canvas.rotate(degree, centerX, centerY);
        canvas.drawCircle(centerX, centerY, radius, paint);
        canvas.restore();
    

    private void startAnimator() 
        ValueAnimator valueAnimator = ValueAnimator.ofInt(-90, 270);
        valueAnimator.setDuration(3000);
        // 动画重复次数
        valueAnimator.setRepeatCount(100);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() 

            @Override
            public void onAnimationUpdate(ValueAnimator animation) 
                degree = (int) animation.getAnimatedValue();
                invalidate();
            
        );
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.start();
    


RadialGradient

RadialGradient 可以实现类似水波纹效果

/**
 * 径向渐变着色器
 */
public class RadialGradientView extends View 
    private Paint paint;
    private int mViewWidth;
    private int mViewHeight;
    private RectF rectF;

    public RadialGradientView(Context context) 
        super(context);
        init();
    

    public RadialGradientView(Context context, AttributeSet attrs) 
        super(context, attrs);
        init();
    

    private void init() 
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) 
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidth = w;
        mViewHeight = h;

        rectF = new RectF(0, 0, mViewWidth, mViewHeight);


        /**
         *  径向着色,中心点为 view 的 中心点,半径为 100px, 超过这个范围的区域以 repeat 重复的形式填充
         */
        RadialGradient radialGradient = new RadialGradient(mViewWidth / 2,
                mViewHeight / 2, 100,
                Color.parseColor("#ff0000"),
                Color.parseColor("#0000ff"),
                Shader.TileMode.REPEAT);

        paint.setShader(radialGradient);

    

    @Override
    protected void onDraw(Canvas canvas) 
        super.onDraw(canvas);
        canvas.drawRect(rectF, paint);
    



效果:

ComposeShader

组合渲染模式可以将两种渲染模式组合到一起,需要结合 Xfermode 一起使用,下面利用 BitmapShader 和 LinearGradient 实现图片底案 loading 效果。

/**
 * 组合着色器,它通过 Xfermode 将两个着色

以上是关于Android 自定义 View进阶 - Shader的主要内容,如果未能解决你的问题,请参考以下文章

Android进阶之绘制-自定义View完全掌握

Android进阶之自定义View实战九宫格手势解锁实现

Android进阶之绘制-自定义View完全掌握

Android进阶之绘制-自定义View完全掌握

我的Android进阶之旅------>Android自定义View实现带数字的进度条(NumberProgressBar)

Android进阶之绘制-自定义View完全掌握