Android Shader 实战

本文已授权微信公众号:鸿洋(hongyangAndroid)原创首发。
转载请标明出处:
http://blog.csdn.net/qian520ao/article/details/61421857
本文出自凶残的程序员的博客

一、前言

Shape标签详解


自定义view中画笔Paint有设置着色器方法paint.setShader(),我们来看一下这个Shader的子类Android Shader 实战_第1张图片



二、SweepGradient [雷达实战]


效果图

Android Shader 实战_第2张图片


使用方式


"300dp"
        android:layout_height="300dp"
        qdx:bgColor="#000"           //设置雷达背景色,默认黑色
        qdx:circleCount="4"          //设置雷达圆圈个数,默认4个
        qdx:endColor="#aaff0000"    
        qdx:startColor="#0000ff00"   //设置雷达扫描的颜色,gif的绿色
        qdx:lineColor="#00ff00"      //雷达线段绘制的颜色             
        />



启动雷达 | 关闭雷达 扫描

startScan();   
stopScan();



SweepGradient【梯度渐变】

Android Shader 实战_第3张图片


上图我们可以看到SweepGradient有两个构造方法,分别是多色渐变和两色渐变,我介绍一下各自的参数。

  • float cx : 中心点x坐标
  • float cy : 中心点y坐标
  • int[] colors : 渐变的颜色数组
  • float[] position : 取值范围[0,1],对应colors[i]的颜色占比
  • int color1 : 这个是两色渐变的起始颜色
  • int color2 : 两色渐变的终止颜色

    Android Shader 实战_第4张图片


绘制雷达

有了这个SweepGradient,绘制雷达就轻松多了,我们只要把雷达的基线绘制好,然后通过paint处理的着色器绘制渐变的圆进行旋转就是我们所要的雷达效果了。


- 首先我们在values目录下面创建attrs.xml文件,设置自定义view的参数供布局使用

    "RadarView">
        "startColor" format="color" />   //起始颜色
        "endColor" format="color" />     //终止颜色
        "bgColor" format="color" />      //雷达背景颜色
        "circleCount" format="integer" />//圆圈数量
        "lineColor" format="color" />    //绘制圆圈和基线的颜色
    



- 在代码中我们获取布局中定义的参数

TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RadarView);

startColor = ta.getColor(R.styleable.RadarView_startColor, startColor);
endColor = ta.getColor(R.styleable.RadarView_endColor, endColor);
mRadarBgColor = ta.getColor(R.styleable.RadarView_bgColor, mRadarBgColor);
mRadarLineColor = ta.getColor(R.styleable.RadarView_lineColor, mRadarLineColor);
radarCircleCount = ta.getInteger(R.styleable.RadarView_circleCount, radarCircleCount);
ta.recycle();



- onMeasure的时候,我们根据用户的不同宽高设置,设置对应的布局大小

int width = measureSize(1, DEFAULT_WIDTH, widthMeasureSpec);
int height = measureSize(0, DEFAULT_HEIGHT, heightMeasureSpec);
int measureSize = Math.max(width, height);     //取最大的 宽|高
setMeasuredDimension(measureSize, measureSize);//重新设置布局measure需要调用
   /**
     * 测绘measure
     * @param specType    1为宽, 其他为高
     * @param contentSize 默认值
     */
    private int measureSize(int specType, int contentSize, int measureSpec) {
        int result;
        //获取测量的模式和Size
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            result = Math.max(contentSize, specSize);
        } else {
            result = contentSize;

            if (specType == 1) {
                // 根据传人方式计算宽
                result += (getPaddingLeft() + getPaddingRight());
            } else {
                // 根据传人方式计算高
                result += (getPaddingTop() + getPaddingBottom());
            }
        }

        return result;

    }



- 上述热身运动做完之后我们开始绘制雷达,首先我们将画板canvas移动到屏幕的中心点,然后根据用户设定圆圈个数来绘制圆圈,最后在用设置着色器的paint来绘制过渡颜色的圆,最后进行旋转即可。

        canvas.translate(mRadarRadius, mRadarRadius);   //将画板移动到屏幕的中心点

        mRadarBg.setShader(null);
        canvas.drawCircle(0, 0, mRadarRadius, mRadarBg);  //绘制底色(默认为黑色),可以使雷达的线看起来更清晰

        for (int i = 1; i <= radarCircleCount; i++) {     //根据用户设定的圆个数进行绘制
            canvas.drawCircle(0, 0, (float) (i * 1.0 / radarCircleCount * mRadarRadius), mRadarPaint);  //画圆圈
        }

        canvas.drawLine(-mRadarRadius, 0, mRadarRadius, 0, mRadarPaint);  //绘制雷达基线 x轴
        canvas.drawLine(0, mRadarRadius, 0, -mRadarRadius, mRadarPaint);  //绘制雷达基线 y轴

        //设置颜色渐变从透明到不透明
        mRadarBg.setShader(radarShader);
        canvas.concat(matrix);
        canvas.drawCircle(0, 0, mRadarRadius, mRadarBg);

初始化paint和shader

        mRadarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);     //设置抗锯齿
        mRadarPaint.setColor(mRadarLineColor);                  //画笔颜色
        mRadarPaint.setStyle(Paint.Style.STROKE);           //设置空心的画笔,只画圆边
        mRadarPaint.setStrokeWidth(2);                      //画笔宽度

        mRadarBg = new Paint(Paint.ANTI_ALIAS_FLAG);     //设置抗锯齿
        mRadarBg.setColor(mRadarBgColor);                  //画笔颜色
        mRadarBg.setStyle(Paint.Style.FILL);           //设置空心的画笔,只画圆边

        radarShader = new SweepGradient(0, 0, startColor, endColor);
        matrix = new Matrix();


我们通过Handler让雷达动起来。

雷达旋转的方法是旋转canvas,这其中有两种实现方式(Matrix详解)

canvas.rotate(degress); 
canvas.concat(matrix); 
matrix.preRotate(rotateAngel, 0, 0);
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

            rotateAngel += 3;
            postInvalidate();

            matrix.reset(); //重置matrix
            matrix.preRotate(rotateAngel, 0, 0);
            mHandler.sendEmptyMessageDelayed(MSG_WHAT, DELAY_TIME);
        }
    };


三、BitmapShader [多边形图片实战]

Android Shader 实战_第5张图片


从上图可以看出BitmapShader 只有一个构造方法,

  • bitmap : 设置图片
  • TileModeX | Y : X,Y 轴填充模式
    Shader.TileMode里有三种模式:CLAMP(拉伸)、MIRROR(镜像)、REPETA(重复)


    Android Shader 实战_第6张图片


    上面的这张德莱文X轴是CLAMP模式,Y轴是REPETA模式,那么问题来了,激光强还是斧头强。
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dlw);
BitmapShader bmpShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.REPEAT);
Paintm Paint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setShader(bmpShader);
canvas.drawRect(bmpRect, mPaint);  //rect为整个屏幕的大小
  • 这时候我们如果在onDraw的时候,在中心位置绘制一个圆,那么是不是就能够相当于以上图为背景,扣出一个圆呢。
        canvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2, mPaint);  //中心画一个半径为宽的圆

Android Shader 实战_第7张图片



那么就好理解了,paint设置了BitmapShader相当于绘制了底层的图片背景,你可以设置了BitmapShader之后,canvas用这枝画笔去绘制任何图像,相当于从背景图扣出对应的形状。

有一点要注意的是BitmapShader通过构造函数初始化设置bitmap时候,默认这个着色背景为当前bitmap的大小,可以通过setLocalMatrix去重新设置着色背景的形状/范围

  • 绘制多边形

效果图



Android Shader 实战_第8张图片


那么我们撸起袖子来通过这个BitmapShader来绘制多边形,我们首先画一个五角形

思路:

Android Shader 实战_第9张图片


首先我们将中心点移动到五角形的中心,以r为半径,我们睁眼说瞎话的设A角度为0°,那么就可以计算B,C,D,E各自的角度,然后按照我们上片自定义菜单得出的公式
x=Math.sin(2PI/360angle)r
y=Math.cos(2PI/360angle)r
x=Math.sin(Math.toRadiansangle)r
y=Math.cos(Math.toRadiansangle)r


现在我们就能够求出各个点的坐标了,下面我们看一下绘制流程。
Android Shader 实战_第10张图片
我们的绘制流程是
0 - 2 - 4 - 1 - 3 - 0
A - C - E - B - D - A

int angleCount=5;                    //angleCount五角星,五个角
int partOfAngle = 360 / angleCount;  //每个部分的角度
int[] angles = new int[angleCount];

for (int i = 0; i < angleCount; i++) {   
angles[i] = currentAngle + partOfAngle * i;

float x = (float) (Math.sin(Math.toRadians(angles[i])) * startRadius);//获取各个点的x轴坐标
float y = (float) (Math.cos(Math.toRadians(angles[i])) * startRadius);
pointFList.add(new PointF(x, y));             //存入集合,方便使用
}

我们通过比较简单的方法来验证一下我们的计算

        canvas.translate(mWidth / 2, mHeight / 2);  //将画板移动到中心


        mPath.moveTo(pointFList.get(0).x, pointFList.get(0).y);
        mPath.lineTo(pointFList.get(2).x, pointFList.get(2).y);
        mPath.lineTo(pointFList.get(4).x, pointFList.get(4).y);

        mPath.lineTo(pointFList.get(1).x, pointFList.get(1).y);
        mPath.lineTo(pointFList.get(3).x, pointFList.get(3).y);

        canvas.drawPath(mPath, mPaint);  //mPaint的stype->Paint.Style.FILL

Android Shader 实战_第11张图片


根据上面这个点的规律,再加上计算(奇/偶数角度计算方式有点区别),下面我直接po出最后的计算方式,虽然感觉不是很干练[小于4的情况 : Go home, You are drunk]

        canvas.translate(mWidth / 2, mHeight / 2);

        mPath.moveTo(pointFList.get(0).x, pointFList.get(0).y);
        for (int i = 2; i < angleCount; i++) {
            if (i % 2 == 0) {// 除以二取余数,余数为0则为偶数,否则奇数
                mPath.lineTo(pointFList.get(i).x, pointFList.get(i).y);
            }
        }

        if (angleCount % 2 == 0) {  //如果是偶数,moveTo
            mPath.moveTo(pointFList.get(1).x, pointFList.get(1).y);
        } else {                    //奇数,lineTo
            mPath.lineTo(pointFList.get(1).x, pointFList.get(1).y);
        }

        for (int i = 3; i < angleCount; i++) {
            if (i % 2 != 0) {
                mPath.lineTo(pointFList.get(i).x, pointFList.get(i).y);
            }
        }

        canvas.drawPath(mPath, mPaint);


三十角形

[凶残的德莱文]
Android Shader 实战_第12张图片



四、LinearGradient [霓虹灯文字实战/图片倒影实战]

Android Shader 实战_第13张图片

  • float x0 : 起始点渐变 x0 的坐标 float y0 : 起始点渐变 yo 的坐标
  • float x1 : 结束渐变点 x1 的坐标 float y1 : 结束渐变点 y1 的坐标
  • int color(),int color1 分别代表起始颜色和结束颜色,16进制的颜色 [0xAARRGGBB]
  • TileMode : 和bitmapShader一样都是CLAMP(拉伸)、MIRROR(镜像)、REPETA(重复)
  • 另外的参数和SweepGradient参数作用是一样的。


    不多说了,我们先上代码
        mSdPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        mLgShader = new LinearGradient(0, 0, 0, 300, new int[]{
                0xFFFF0000, 
                0xffFF7F00,
                0xffFFFF00,
                0xff00FF00, 
                0xff00FFFF, 
                0xff0000FF,
                0xff8B00FF}, null, Shader.TileMode.CLAMP);  //position为null则均匀分配
        mSdPaint.setShader(mLgShader);
        rect = new Rect(0, 0, 300, 300);

Android Shader 实战_第14张图片
结合图文我们可以很清晰的看出,我们从左上角到左下角着色LinearGradient,所以当着色背景LinearGradient设置的大小大于或者等于当前显示的范围(rect),那么设置TileMode作用就不大了。那么我们来试试当着色背景LinearGradient的高度为当前形状1/3的时候,设置不同的TileMode的效果。

        mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);//用来画分割线
        mPaint.setColor(Color.BLACK);
        mPaint.setStrokeWidth(2);

        mSdPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mLgShader = new LinearGradient(200, 200, 200, 300, new int[]{
                0xFFFF0000,     //红
                0xFFFF7F00,     //橙
                0xFFFFFF00,     //黄
                0xFF00FF00,     //绿
                0xFF00FFFF,     //青
                0xFF0000FF,     //蓝
                0xFF8B00FF      //紫
        }, null, Shader.TileMode.CLAMP);//position为null则均匀分配
        mSdPaint.setShader(mLgShader);
        rect = new Rect(0, 0, 300, 300);

onDraw

        canvas.drawRect(rect, mSdPaint);
        canvas.drawLine(0,100,300,100,mPaint);  //画2条分割线,方便理解
        canvas.drawLine(0,200,300,200,mPaint);


下面分别是CLAMP [拉伸] MIRROR [镜像] REPEAT [重复]

Android Shader 实战_第15张图片 Android Shader 实战_第16张图片 Android Shader 实战_第17张图片



下面我们用LinearGradient开始实战霓虹文字

Android Shader 实战_第18张图片


先放出代码过个瘾

        matrix = new Matrix();  //用来移动Shader的matrix
        mPaint = getPaint();  //获取控件本身的paint,这个很重要,获取控件绘制的paint
        //我们设置一个LgShader,从左边[距离文字显示2倍宽度的距离]
        mShader = new LinearGradient(-2 * w, 0, -w, 0, new int[]{getCurrentTextColor(), Color.RED, Color.YELLOW, Color.BLUE, getCurrentTextColor()}, null, Shader.TileMode.CLAMP);
        mPaint.setShader(mShader);

        ValueAnimator animator = ValueAnimator.ofInt(0, width * 3);  //我们设置value的值为0-getMeasureWidth的3 倍
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mVx = (Integer) animation.getAnimatedValue();
                postInvalidate();
            }
        });
        animator.setRepeatMode(ValueAnimator.RESTART);   //重新播放
        animator.setRepeatCount(-1);                    //无限循环
        animator.setDuration(2000);
        animator.start();

onDraw

        matrix.reset();   //先重置,以免累加移动
        matrix.preTranslate(mVx, 0);
        mShader.setLocalMatrix(matrix);

xml代码

    .radarview.shader.LinearGradientText
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="塞纳河畔 左岸的咖啡\n我手一杯 品尝你的美"
    android:textColor="#ff00ff"
    android:textSize="40sp" />



解刨图
Android Shader 实战_第19张图片


- PS:好气啊,昨晚编辑了这篇文章,写了蛮多的没有保存到线上草稿箱,手抽了一下把浏览器关了…T T,上图原本是有呕心沥血做出,后来被我清除了,大家凑合着看吧。。。。


- 回归正题,我们来分解一下上面这张图,图片做的是一个效果,但是实际上我理一下,当前我设置的文字颜色是#ff00ff即紫色

getCurrentTextColor(), Color.RED, Color.YELLOW, Color.BLUE, getCurrentTextColor()
  • 所以排序应该是 紫紫紫 紫红黄蓝紫 紫紫紫 ,也就是说红色的左边和蓝色的右边填充的颜色应该是紫色(TileMode为CLAMP),所以我们开始translation的时候就能比较柔和的先显示TextView当前的颜色然后再进行过渡。

我们拿LinearGradient来做一个图片倒影



- 先拿效果图精神精神
Android Shader 实战_第20张图片
- 下面我来介绍一下这张图片,女主叫克拉拉,英国籍韩裔女演员,又名李成敏。2004年,获得韩国第一届网络美女照片竞赛第一名...不好意思扯远了首先我们讲一下思路,首先我们需要设置2张bitmap,一张是正序图片,另一张是倒影的图片(都为同张图片),关键是倒影图片设置完之后,我们需要调节一下图片的透明度,这里需要用到Xfermoder的知识,我们用LinearGradient绘制出与bitmap相同大小的RectF(dst目标),然后用Xfermode的SRC_OUT模式绘制倒影的bitmap,再加上先前对LinearGradient的颜色设定就能够做到如图的倒影效果了。

    private Bitmap bitmap;              //克拉拉bmp
    private Bitmap bitmapShadow;        //倒影的克拉拉bmp
    private Paint mBmpShadowPaint;      //绘制倒影RECTF的paint
    private Paint mXfPaint;             //设置Xfermode的paint
    private Xfermode xfermode;          //SRC_OUT Xfermode

    private void init(){
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.kll); //先从资源文件获取克拉拉bmp
        mBmpShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mXfPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT);            //设置XferModer
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        int bmpW = bitmap.getWidth();

        float ratioW = w * 1.0f / bmpW * 2 / 3;

        //压缩克拉拉bmp ,我们设置图片的宽度为屏幕的 2/3
        bitmap = BmpUtils.zoomImg(bitmap, (int) (bitmap.getWidth() * ratioW), (int) (bitmap.getHeight() * ratioW));

        Matrix matrix = new Matrix();
        matrix.setScale(1, -1);      //X轴不变化,y轴旋转180°,即设置倒影的bmp
        bitmapShadow = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); //克拉拉bmp旋转,即旋转的克拉拉bmp

        //我们设置LinearGradient,首先旋转的克拉拉bmp 上面 10%的部分我们透明度设置稍微小一点,10%-30%过渡,最后渐进到透明
        LinearGradient mLgShader = new LinearGradient(0, bitmap.getHeight(), 0, bitmap.getHeight() * 2, new int[]{0x00000000, 0x11000000, 0xaa000000, Color.WHITE}, new float[]{0, 0.1f, 0.4f, 0.6f}, Shader.TileMode.REPEAT);
        mBmpShadowPaint.setShader(mLgShader);

        shadowRectF = new RectF(0, bitmap.getHeight(), bitmap.getWidth(), bitmap.getHeight() * 2); //绘制在 克拉拉bmp图片下方。getHeight*2
    }

    private RectF shadowRectF;


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(getWidth() / 6, getHeight() / 10);//首先我们将图片居中(因为图片是屏幕的2/3,所以我们往右 1/6)

        canvas.drawBitmap(bitmap, 0, 0, null);//先绘制克拉拉

        int layerID = canvas.saveLayer(shadowRectF, null, Canvas.ALL_SAVE_FLAG);//新建图层,用来进行XferMode手术

        canvas.drawRect(shadowRectF, mBmpShadowPaint);   //绘制dst图片
        mXfPaint.setXfermode(xfermode);                  //SRC_OUT
        canvas.drawBitmap(bitmapShadow, 0, bitmap.getHeight(), mXfPaint);  //绘制src图片
        mXfPaint.setXfermode(null);

        canvas.restoreToCount(layerID);//还原。

    }


五、总结

  • 最后还剩下RadialGradient水波纹效果和ComposeShader(Shader),有兴趣的可以捯饬捯饬。


    通过上面三个实战,我们可以发现无论是哪种Shader,都是从左上角开始填充,然后利用Canvas去绘制具体某一区域(也就是说你canvas绘制的就是这相当于满屏的着色背景的一部分。),我们可以通过Shader和Xfermode绘制出比较奇幻的效果,例如RadialGradient和Xfermode的SCREEN可以绘制灯光遮罩效果,还有支付宝的咻一咻,等着大家慢慢发掘。如果有好的idea可以留言给我。

六、源码下载地址

安卓Shader实战

你可能感兴趣的:(《自定义view系列》)