自定义View(二-番外1)

From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige

跟着爱哥打天下

自定义控件其实很简单1:
invalidate()得在UI线程中被调动,在工作者线程中可以通过Handler来通知UI线程进行界面更新。
而postInvalidate()在工作者线程中被调用

自定义控件其实很简单2:

setColorFilter(ColorFilter filter)

ColorFilter:颜色过滤器
有三个子类:ColorMatrixColorFilter、LightingColorFilter和PorterDuffColorFilter
ColorMatrixColorFilter:色彩矩阵颜色过滤器

ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
        1, 0, 0, 0, 0,  
        0, 1, 0, 0, 0,  
        0, 0, 1, 0, 0,  
        0, 0, 0, 1, 0,  
});  

第一行表示的R(红色)的向量,第二行表示的G(绿色)的向量,第三行表示的B(蓝色)的向量,最后一行表示A(透明度)的向量,这一顺序必须要正确不能混淆!这个矩阵不同的位置表示的RGBA值,其范围在0.0F至2.0F之间,1为保持原图的RGB值。每一行的第五列数字表示偏移值

LightingColorFilter:光照颜色过滤

LightingColorFilter (int mul, int add)  

mul全称是colorMultiply意为色彩倍增,而add全称是colorAdd意为色彩添加,这两个值都是16进制的色彩值0xAARRGGBB。这个方法使用也是非常的简单。还是拿上面那张图片来说吧,比如我们想要去掉绿色:

// 设置颜色过滤  
mPaint.setColorFilter(new LightingColorFilter(0xFFFF00FF, 0x00000000));  

星星的点击变色和还原:

setOnClickListener(new OnClickListener() {  
    @Override  
    public void onClick(View v) {  
        /* 
         * 判断控件是否被点击过 
         */  
        if (isClick) {  
            // 如果已经被点击了则点击时设置颜色过滤为空还原本色  
            mPaint.setColorFilter(null);  
            isClick = false;  
        } else {  
            // 如果未被点击则点击时设置颜色过滤后为黄色  
            mPaint.setColorFilter(new LightingColorFilter(0xFFFFFFFF, 0X00FFFF00));  
            isClick = true;  
        }  

        // 记得重绘  
        invalidate();  
    }  
});  

PorterDuffColorFilter:

PorterDuffColorFilter(int color, PorterDuff.Mode mode)

这个构造方法也接受两个值,一个是16进制表示的颜色值这个很好理解,而另一个是PorterDuff内部类Mode中的一个常量值,这个值表示混合模式。那么什么是混合模式呢?混合混合必定是有两种东西混才行,第一种就是我们设置的color值而第二种当然就是我们画布上的元素了!,比如这里我们把Color的值设为红色,而模式设为PorterDuff.Mode.DARKEN变暗:

mPaint.setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.DARKEN));

setXfermode(Xfermode xfermode)

Xfermode国外有大神称之为过渡模式,这种翻译比较贴切但恐怕不易理解,大家也可以直接称之为图像混合模式,因为所谓的“过渡”其实就是图像混合的一种,这个方法跟我们上面讲到的setColorFilter蛮相似的
它有三个子类:
AvoidXfermode, PixelXorXfermode和PorterDuffXfermode
AvoidXfermode:(过时
这个API因为不支持硬件加速在API 16已经过时了,如果想在高于API 16的机子上测试这玩意,必须现在应用或手机设置中关闭硬件加速,在应用中我们可以通过在AndroidManifest.xml文件中设置application节点下的android:hardwareAccelerated属性为false来关闭硬件加速

android:hardwareAccelerated="false"  
AvoidXfermode(int opColor, int tolerance, AvoidXfermode.Mode mode)

AvoidXfermode有三个参数,第一个opColor表示一个16进制的可以带透明通道的颜色值例如0x12345678,第二个参数tolerance表示容差值,那么什么是容差呢?你可以理解为一个可以标识“精确”或“模糊”的东西,待会我们细讲,最后一个参数表示AvoidXfermode的具体模式,其可选值只有两个:AvoidXfermode.Mode.AVOID或者AvoidXfermode.Mode.TARGET

AvoidXfermode.Mode.TARGET:
在该模式下Android会判断画布上的颜色是否会有跟opColor不一样的颜色,比如我opColor是红色,那么在TARGET模式下就会去判断我们的画布上是否有存在红色的地方,如果有,则把该区域“染”上一层我们画笔定义的颜色,否则不“染”色,而tolerance容差值则表示画布上的像素和我们定义的红色之间的差别该是多少的时候才去“染”的,比如当前画布有一个像素的色值是(200, 20, 13),而我们的红色值为(255, 0, 0),当tolerance容差值为255时,即便(200, 20, 13)并不等于红色值也会被“染”色,容差值越大“染”色范围越广反之则反。


// 先绘制位图  
canvas.drawBitmap(bitmap, x, y, mPaint);  

// “染”什么色是由我们自己决定的  
mPaint.setARGB(255, 211, 53, 243);  

// 设置AV模式  
mPaint.setXfermode(avoidXfermode);  

// 画一个位图大小一样的矩形  
canvas.drawRect(x, y, w, h, mPaint); 

AvoidXfermode.Mode.AVOID

则与TARGET恰恰相反,TARGET是我们指定的颜色是否与画布的颜色一样,而AVOID是我们指定的颜色是否与画布不一样,其他的都与TARGET类似

那么这玩意究竟有什么用呢?比如说当我们只想在白色的区域画点东西或者想把白色区域的地方替换为另一张图片的时候就可以采取这种方式!

PixelXorXfermode:(过时
与AvoidXfermode一样也在API 16过时了,该类也提供了一个含参的构造方法PixelXorXfermode(int opColor),该类的计算实现很简单,从官方给出的计算公式来看就是:op ^ src ^ dst,像素色值的按位异或运算,如果大家感兴趣,可以自己用一个纯色去尝试,并自己计算异或运算的值是否与得出的颜色值一样,这里我就不讲了,Because it was deprecated and useless。

PorterDuffXfermode:(唯一一个未过时的
该类同样有且只有一个含参的构造方法:

PorterDuffXfermode(PorterDuff.Mode mode)

里面有18中模式,图形图像学中的16种+android中的2中

这里写图片描述
这里面包含了16中组合,但是我自己绘制怎么也绘制不出来,大部分都是出错的,在网上爬了爬,说有两种方法:
1,使用saveLayer

public ModeOne2View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mDstPaint = new Paint();

        mDstPaint.setColor(0xFFFFCC44);
        mDstPaint.setColor(0xFF66AAFF);

        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        //背景色
        canvas.drawARGB(255,255,156,161);

        canvas.saveLayer(0,0,getWidth(),getHeight(),null,Canvas.ALL_SAVE_FLAG);
        mDstPaint.setColor(0xFFFFCC44);
        canvas.drawCircle(100, 100, 100, mDstPaint);

        mDstPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SCREEN));


        mDstPaint.setColor(0xFF66AAFF);

        canvas.drawRect(100, 100, 300, 300, mDstPaint);


    }

2、使用两个Bitmap进行绘制,但是我失败了

public class ModeOneView extends View {
    private Paint mDstPaint, mSrcPaint;

    //目标画布
    private Canvas mCanvas, mDstCanvas;
    //目标位图
    private Bitmap mSrcBitmap, mDstBitmap;

    public ModeOneView(Context context) {
        super(context);
    }

    public ModeOneView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);

    }

    public ModeOneView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mDstPaint = new Paint();
        mSrcPaint = new Paint();

        mDstPaint.setColor(0xFFFFCC44);
        mSrcPaint.setColor(0xFF66AAFF);

        setLayerType(View.LAYER_TYPE_SOFTWARE, null);

        mSrcBitmap = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
        mCanvas = new Canvas(mSrcBitmap);

        mDstBitmap = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
        mDstCanvas = new Canvas(mDstBitmap);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        //背景色
        canvas.drawARGB(255,255,156,161);

        //src
        mCanvas.drawARGB(0,0,0,0);
        mCanvas.drawRect(0, 0, 200, 200, mSrcPaint);
        canvas.drawBitmap(mSrcBitmap, 100, 100, mSrcPaint);

        mSrcPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SCREEN));

        //dst
        mDstCanvas.drawARGB(0,0,0,0);
        mDstCanvas.drawCircle(100, 100, 100, mDstPaint);
        canvas.drawBitmap(mDstBitmap, 0, 0, mDstPaint);

    }
}

第一个按照一定的原理来理解还可以理解,
第二个画了差不多8个多小时,但是失败了,这个先留着吧,等以后再仔细探索一番
参考一
参考二
参考三
参考四
参考五
参考六

自定义控件其实很简单3:

FontMetrics:字体测量
定义了五个成员变量:top,ascent,descent,bottom,leading

这里写图片描述

这张图很简单但是也很扼要的说明了top,ascent,descent,bottom,leading这五个参数。首先我们要知道Baseline基线,在Android中,文字的绘制都是从Baseline处开始的,Baseline往上至字符最高处的距离我们称之为ascent(上坡度),Baseline往下至字符最底处的距离我们称之为descent(下坡度),而leading(行间距)则表示上一行字符的descent到该行字符的ascent之间的距离,top和bottom文档描述地很模糊,其实这里我们可以借鉴一下TextView对文本的绘制,TextView在绘制文本的时候总会在文本的最外层留出一些内边距,为什么要这样做?因为TextView在绘制文本的时候考虑到了类似读音符号,可能大家很久没写过拼音了已经忘了什么叫读音符号了吧……下图中的A上面的符号就是一个拉丁文的类似读音符号的东西:

这里写图片描述

top的意思其实就是除了Baseline到字符顶端的距离外还应该包含这些符号的高度,bottom的意思也是一样,一般情况下我们极少使用到类似的符号所以往往会忽略掉这些符号的存在,但是Android依然会在绘制文本的时候在文本外层留出一定的边距,这就是为什么top和bottom总会比ascent和descent大一点的原因。而在TextView中我们可以通过xml设置其属性android:includeFontPadding=”false”去掉一定的边距值但是不能完全去掉。

在我们绘制文本之前我们便可以获取文本的FontMetrics属性值,也就是说我们FontMetrics的这些值跟我们要绘制什么文本是无关的,而仅与绘制文本Paint的size和typeface有关

TextPaint:专门为绘制文字而打造
Paint有一个唯一的子类TextPaint就是专门为文本绘制量身定做的“笔”,而这支笔就如API所描述的那样能够在绘制时为文本添加一些额外的信息,这些信息包括:baselineShift,bgColor,density,drawableState,linkColor,这些属性都很简单大家顾名思义或者自己去尝试下即可这里就不多说了,那么这支笔有何用呢?最常用的用法是在绘制文本时能够实现换行绘制!在正常情况下Android绘制文本是不能识别换行符之类的标识符的,这时候如果我们想实现换行绘制就得另辟途径使用StaticLayout结合TextPaint实现换行,StaticLayout是android.text.Layout的一个子类,很明显它也是为文本处理量身定做的,其内部实现了文本绘制换行的处理
自动换行

@Override  
protected void onDraw(Canvas canvas) {  
    super.onDraw(canvas);  
    mStaticLayout = new StaticLayout(TEXT, mTextPaint, canvas.getWidth(), Alignment.ALIGN_NORMAL, 1.0F, 0.0F, false);  
    mStaticLayout.draw(canvas);  
    canvas.restore();  
}  

ascent()

顾名思义就是返回上坡度的值,我们已经用过了
descent()
下坡度

breakText (CharSequence text, int start, int end, boolean measureForwards, float maxWidth, float[] measuredWidth)

这个方法让我们设置一个最大宽度在不超过这个宽度的范围内返回实际测量值否则停止测量,参数很多但是都很好理解,text表示我们的字符串,start表示从第几个字符串开始测量,end表示从测量到第几个字符串为止,measureForwards表示向前还是向后测量,maxWidth表示一个给定的最大宽度在这个宽度内能测量出几个字符,measuredWidth为一个可选项,可以为空,不为空时返回真实的测量值。同样的方法还有breakText (String text, boolean measureForwards, float maxWidth, float[] measuredWidth)和breakText (char[] text, int index, int count, float maxWidth, float[] measuredWidth)。这些方法在一些结合文本处理的应用里比较常用,比如文本阅读器的翻页效果,我们需要在翻页的时候动态折断或生成一行字符串,这就派上用场了~~~

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int pos=mPaint.breakText(TEXT,0,TEXT.length(),false,200,null);
    String str=TEXT.substring(0,pos);

    canvas.drawText(str,0,100,mPaint);
}

getFontMetrics (Paint.FontMetrics metrics)

这个和我们之前用到的getFontMetrics()相比多了个参数,getFontMetrics()返回的是FontMetrics对象而getFontMetrics(Paint.FontMetrics metrics)返回的是文本的行间距,如果metrics的值不为空则返回FontMetrics对象的值。

getFontMetricsInt(Paint.FontMetricsInt fmi)

getFontSpacing()

返回字符行间距
setUnderlineText(boolean underlineText)

设置下划线

setTypeface(Typeface typeface)

设置字体类型,上面我们也使用过,Android中字体有四种样式:BOLD(加粗),BOLD_ITALIC(加粗并倾斜),ITALIC(倾斜),NORMAL(正常);而其为我们提供的字体有五种:DEFAULT,DEFAULT_BOLD,MONOSPACE,SANS_SERIF和SERIF,这些什么类型啊、字体啊之类的都很简单大家自己去试试就知道就不多说了。但是系统给我们的字体有限我们可不可以使用自己的字体呢?答案是肯定的!Typeface这个类中给我们提供了多个方法去个性化我们的字体

defaultFromStyle(int style)

最简单的,简而言之就是把上面所说的四种Style封装成Typeface
create(String familyName, int style)和create(Typeface family, int style)

两者大概意思都一样,比如

textPaint.setTypeface(Typeface.create("SERIF", Typeface.NORMAL));  
textPaint.setTypeface(Typeface.create(Typeface.SERIF, Typeface.NORMAL));  

两者效果是一样的
createFromAsset(AssetManager mgr, String path)、createFromFile(String path)和createFromFile(File path)

这三者也是一样的,它们都允许我们使用自己的字体比如我们从asset目录读取一个字体文件:

// 获取字体并设置画笔字体  
Typeface typeface = Typeface.createFromAsset(context.getAssets(), "kt.ttf");  
textPaint.setTypeface(typeface); 

setTextSkewX(float skewX)

这个方法可以设置文本在水平方向上的倾斜:

// 设置画笔文本倾斜  
textPaint.setTextSkewX(-0.25F);

这个倾斜值没有具体的范围,但是官方推崇的值为-0.25可以得到比较好的倾斜文本效果,值为负右倾值为正左倾,默认值为0

setTextSize (float textSize)

setTextScaleX (float scaleX)

将文本沿X轴水平缩放,默认值为1,当值大于1会沿X轴水平放大文本,当值小于1会沿X轴水平缩放文本

// 设置画笔文本倾斜  
textPaint.setTextScaleX(0.5F);

大家注意哦!setTextScaleX不仅放大了文本宽度同时还拉伸了字符!这是亮点~~

setTextLocale (Locale locale)

设置地理位置,这个不讲,我们会在屏幕适配系列详解什么是Locale,这里如果你要使用,直接传入Locale.getDefault()即可

setTextAlign (Paint.Align align)

设置文本的对其方式,可供选的方式有三种:CENTER,LEFT和RIGHT,其实从这三者的名字上看我们就知道其意思,但是问题是这玩意怎么用的?好像没什么用啊……我们的文本大小是通过size和typeface确定的(其实还有其他的因素但这里影响不大忽略~~),一旦baseline确定,对不对齐好像不相干吧……但是,你要知道一点,文本的绘制是从baseline开始没错,但是是从哪边开始绘制的呢?左端还是右端呢?而这个Align就是为我们定义在baseline绘制文本究竟该从何处开始,上面我们在进行对文本的水平居中时是用Canvas宽度的一半减去文本宽度的一半:

@Override  
protected void onDraw(Canvas canvas) {  
    super.onDraw(canvas);  

    // 计算Baseline绘制的起点X轴坐标  
    baseX = (int) (canvas.getWidth() / 2 - textPaint.measureText(TEXT) / 2);  

    // 计算Baseline绘制的Y坐标  
    baseY = (int) ((canvas.getHeight() / 2) - ((textPaint.descent() + textPaint.ascent()) / 2));  

    canvas.drawText(TEXT, baseX, baseY, textPaint);  

    // 为了便于理解我们在画布中心处绘制一条中线  
    canvas.drawLine(0, canvas.getHeight() / 2, canvas.getWidth(), canvas.getHeight() / 2, linePaint);  
}  

实际上我们大可不必这样计算,我们只需设置Paint的文本对齐方式为CENTER,drawText的时候起点x = canvas.getWidth() / 2即可:

textPaint.setTextAlign(Align.CENTER);  
canvas.drawText(TEXT, canvas.getWidth() / 2, baseY, textPaint);  

setSubpixelText (boolean subpixelText)

设置是否打开文本的亚像素显示,什么叫亚像素显示呢?你可以理解为对文本显示的一种优化技术,如果大家用的是Win7+系统可以在控制面板中找到一个叫ClearType的设置,该设置可以让你的文本更好地显示在屏幕上就是基于亚像素显示技术。具体我们在设计色彩系列将会细说,这里就不扯了

setStrikeThruText (boolean strikeThruText)

文本删除线

mPaint.setStrikeThruText(true);

这里写图片描述

setLinearText (boolean linearText)

设置是否打开线性文本标识,这玩意对大多数人来说都很奇怪不知道这玩意什么意思。想要明白这东西你要先知道文本在Android中是如何进行存储和计算的。在Android中文本的绘制需要使用一个bitmap作为单个字符的缓存,既然是缓存必定要使用一定的空间,我们可以通过setLinearText (true)告诉Android我们不需要这样的文本缓存。

setFakeBoldText (boolean fakeBoldText)

设置文本仿粗体

measureText (String text),measureText (CharSequence text, int start, int end),measureText (String text, int start, int end),measureText (char[] text, int index, int count)

测量文本宽度,上面我们已经使用过了,这四个方法都是一样的只是参数稍有不同这里就不撤了!Paint对文本的绘制方法就上面那些,API 21中还新增了两个方法

setDither(boolean dither)

这玩意用来设置我们在绘制图像时的抗抖动,也称为递色(在两种颜色交接的地方,中和一下)

setMaskFilter(MaskFilter maskfilter)

MaskFilter类中没有任何实现方法,而它有两个子类BlurMaskFilter和EmbossMaskFilter,前者为模糊遮罩滤镜(比起称之为过滤器哥更喜欢称之为滤镜)而后者为浮雕遮罩滤镜

BlurMaskFilter:

不支持硬件加速的绘制方法:
这里写图片描述

但是大家想过没如果在AndroidManifest.xml文件中关闭硬件加速那么我们整个应用都将不支持硬件加速,这显然是不科学的,如果可以只针对某个View关闭硬件加速那岂不是很好么?当然,Android也给我们提供了这样的功能,我们可以在View中通过

setLayerType(LAYER_TYPE_SOFTWARE, null);

来关闭单个View的硬件加速功能

BlurMaskFilter只有一个含参的构造函数BlurMaskFilter(float radius, BlurMaskFilter.Blur style),其中radius很容易理解,值越大我们的阴影越扩散;
而第二个参数style表示的是模糊的类型,上面我们用到的是SOLID,其效果就是在图像的Alpha边界外产生一层与Paint颜色一致的阴影效果而不影响图像本身,除了SOLID还有三种,NORMAL,OUTER和INNER,NORMAL会将整个图像模糊掉

这里写图片描述

而OUTER会在Alpha边界外产生一层阴影且会将原本的图像变透明:

这里写图片描述

INNER则会在图像内部产生模糊:

这里写图片描述

INNER效果其实并不理想,实际应用中我们使用的也少,我们往往会使用混合模式和渐变和获得更完美的内阴影效果。如上所说BlurMaskFilter是根据Alpha通道的边界来计算模糊的,如果是一张图片(注:上面我们说过Android会把拷贝到资源目录的图片转为RGB565,具体原因具体分析我会单独开一篇帖子说,这里就先假设所有提及的图片格式为RGB565)你会发现没有任何效果,那么假使我们需要给图片加一个类似阴影的效果该如何做呢?其实很简单,我们可以尝试从Bitmap中获取其Alpha通道,并在绘制Bitmap前先以该Alpha通道绘制一个模糊效果不就行了?

public class BlurMaskFilterView extends View {
    /**
     * 画笔
     */
    private Paint mPaint;

    /**
     * 位图和阴影图片
     */
    private Bitmap srcBitmap, shadowBitmap;

    /**
     * 构造函数
     */
    public BlurMaskFilterView(Context context) {
        this(context, null);
    }

    public BlurMaskFilterView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BlurMaskFilterView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        initPaint();

        initRes(context);
    }

    /**
     * 初始化画笔
     */
    private void initPaint() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        mPaint.setStyle(Paint.Style.FILL);
        //设置画笔颜色
        mPaint.setColor(Color.DKGRAY);

        //设置画笔遮罩滤镜
        mPaint.setMaskFilter(new BlurMaskFilter(20, BlurMaskFilter.Blur.NORMAL));

    }

    /**
     * 初始化资源
     */
    private void initRes(Context context) {
        //获取位图
        srcBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.bitmap);

        //获取位图的Alpha通道图
        shadowBitmap = srcBitmap.extractAlpha();

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //先绘制阴影
        canvas.drawBitmap(shadowBitmap, 100, 100, mPaint);

        //再绘制位图
        canvas.drawBitmap(srcBitmap, 100, 100, null);
    }
}

如代码所示我们通过Bitmap的extractAlpha()方法从原图中分离出一个Alpha通道位图并在计算模糊滤镜的时候使用该位图生成模糊效果:

这里写图片描述

相对于BlurMaskFilter来说
EmbossMaskFilter

的常用性比较低,倒不是说EmbossMaskFilter很没用,只是相对于EmbossMaskFilter实现的效果来说远不及BlurMaskFilter给人的感觉霸气,说了半天那么EmbossMaskFilter到底是做什么的呢?
我们先来看一张图:
这里写图片描述

这么一个看着像巧克力的东西就是用EmbossMaskFilter实现了,正如其名,他可以实现一种类似浮雕的效果,说白了就是让你绘制的图像感觉像是从屏幕中“凸”起来更有立体感一样(在设计软件中类似的效果称之为斜面浮雕)。该类也只有一个含参的构造方法:

EmbossMaskFilter(float[] direction, float ambient, float specular, float blurRadius)

第一个direction指的是方向,什么方向呢?光照的方向
ambient叫做环境光,参数中的该值就是用来设置环境光的,在Android中环境光默认为白色,其值越大,阴影越浅
specular就是跟高光有关的,其值是个双向值越小或越大高光越强中间值则是最弱的
blurRadius则是设置图像究竟“凸”出多大距离的很好理解

最难理解的一个参数是direction,上面我们也说了是光照方向的意思,该数组必须要有而且只能有三个值即float[x,y,z],这三个值代表了一个空间坐标系,我们的光照方向则由其定义,那么它是怎么定义的呢?首先x和y很好理解,平面的两个维度嘛是吧,上面我们使用的是[1,1]也就是个45度角,而z轴表示光源是在屏幕后方还是屏幕前方,上面我们是用的是1,正值表示光源往屏幕外偏移1个单位,负值表示往屏幕里面偏移,这么一说如果我把其值改为[1,1,-1]那么我们的巧克力朝着我们的一面应该就看不到了对吧,试试看撒~~~这个效果我就不截图了,因为一片漆黑……但是你依然能够看到一点点灰度~就是因为我们的环境光ambient!,如果我们把值改为[1,1,2]往屏幕外偏移两个单位,那么我们巧克力正面光照将更强

这里要提醒一点[x,y,z]表示的是空间坐标,代表光源的位置,那么一旦这个位置确定,[ax,ay,az]则没有意义,也就是说同时扩大三个轴向值的倍数是没有意义的,最终效果还是跟[x,y,z]一样!懂了不?

public class EmbossMaskFilterView extends View {
    /**
     * 画笔
     */
    private Paint mPaint;

    /**
     * 水平和垂直切割数
     */
    public static final int H_COUNT = 2, V_COUNT = 4;

    /**
     * 存储在各个巧克力坐上坐标的点
     */
    private PointF[] mPointF;

    /**
     * 单个巧克力的宽高
     */
    private int width, height;

    /**
     * 单个巧克力坐上Y轴坐标值
     */
    private float coorY;

    /**
     * 构造函数
     */
    public EmbossMaskFilterView(Context context) {
        this(context, null);
    }

    public EmbossMaskFilterView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public EmbossMaskFilterView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        initPaint();

        // 计算参数
        cal(context);
    }

    /**
     * 初始化画笔
     */
    private void initPaint() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        mPaint.setStyle(Paint.Style.FILL);
        //设置画笔颜色
        mPaint.setColor(0xFF603811);

        //设置画笔遮罩滤镜
        mPaint.setMaskFilter(new EmbossMaskFilter(new float[]{1, 1, 1F}, 0.1F, 10F, 20F));

    }

    /**
     * 计算参数
     */
    private void cal(Context context) {

        width = 768 / H_COUNT;
        height = 1280 / V_COUNT;

        int count = V_COUNT * H_COUNT;

        mPointF = new PointF[count];
        for (int i = 0; i < count; i++) {
            if (i % 2 == 0) {
                coorY = i * height / 2F;
                mPointF[i] = new PointF(0, coorY);
            } else {
                mPointF[i] = new PointF(width, coorY);
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 画矩形
        for (int i = 0; i < V_COUNT * H_COUNT; i++) {
            canvas.drawRect(mPointF[i].x, mPointF[i].y, mPointF[i].x + width, mPointF[i].y + height, mPaint);
        }
    }
}

这里写图片描述

setRasterizer (Rasterizer rasterizer)

设置光栅,光栅这东西涉及太多太多物理知识,不讲了一讲又是一大堆,而且该方法同样不支持HW在API 21中遗弃了

setPathEffect(PathEffect effect)

PathEffect见文知意很明显就是路径效果的意思~~那这玩意肯定跟路径Path有关咯?那是必须的撒!PathEffect跟上面的很多类一样没有具体的实现,但是其有六个子类:

CornerPathEffect、DiscretePathEffect、DashPathEffect、PathDashPathEffect、ComposePathEffect、SumPathEffect

这里写图片描述

上图从上往下分别是没有PathEffect、CornerPathEffect、DiscretePathEffect、DashPathEffect、PathDashPathEffect、ComposePathEffect、SumPathEffect的效果,代码的实现也非常简单:

public class PathEffectView extends View {
    /**
     * 画笔
     */
    private Paint mPaint;

    /**
     * 偏移值
     */
    private float mPhase;

    /**
     * 路径对象
     */
    private Path mPath;

    /**
     * 路径效果数组
     */
    private PathEffect[] mEffects;

    /**
     * 构造函数
     */
    public PathEffectView(Context context) {
        this(context, null);
    }

    public PathEffectView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PathEffectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        initPaint();

    }

    /**
     * 初始化画笔
     */
    private void initPaint() {
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);
        //设置画笔颜色
        mPaint.setColor(Color.DKGRAY);

        //实例化路径
        mPath = new Path();

        //定义路径的起点
        mPath.moveTo(0, 0);

        //定义路径的各个点
        for (int i = 0; i < 31; i++) {
            mPath.lineTo(i * 35, (float) (Math.random() * 100));
        }

        /**
         * 创建路径效果数组
         */
        mEffects = new PathEffect[7];

    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //实例化各类特效
        mEffects[0] = null;
        mEffects[1] = new CornerPathEffect(10);
        mEffects[2] = new DiscretePathEffect(3.0F, 5.0F);
        mEffects[3] = new DashPathEffect(new float[]{20, 10, 5, 10}, mPhase);

        Path path = new Path();
        path.addRect(0, 0, 8, 8, Path.Direction.CCW);

        mEffects[4] = new PathDashPathEffect(path, 12, mPhase, PathDashPathEffect.Style.ROTATE);
        mEffects[5] = new ComposePathEffect(mEffects[2], mEffects[4]);
        mEffects[6] = new SumPathEffect(mEffects[4], mEffects[3]);

        //绘制路径
        for (int i = 0; i < mEffects.length; i++) {
            mPaint.setPathEffect(mEffects[i]);
            canvas.drawPath(mPath, mPaint);

            //下移
            canvas.translate(0, 150);
        }

        mPhase += 1;
        invalidate();

    }
}

这里写图片描述

当我们不设置路径效果的时候路径的默认效果就如上图第一条线那样直的转折生硬;而CornerPathEffect则可以将路径的转角变得圆滑如图第二条线的效果,这六种路径效果类都有且只有一个含参的构造方法,CornerPathEffect的构造方法只接受一个参数radius,意思就是转角处的圆滑程度

mEffects[1] = new CornerPathEffect(50); 

CornerPathEffect相对于其他的路径效果来说最简单了;DiscretePathEffect离散路径效果相对来说则稍微复杂点,其会在路径上绘制很多“杂点”的突出来模拟一种类似生锈铁丝的效果如上图第三条线,其构造方法有两个参数,第一个呢指定这些突出的“杂点”的密度,值越小杂点越密集,第二个参数呢则是“杂点”突出的大小,值越大突出的距离越大反之反之,大家可以去自己去试下我就不演示了;DashPathEffect的效果相对与上面两种路径效果来说要略显复杂,其虽说也是包含了两个参数,但是第一个参数是一个浮点型的数组,那这个数组有什么意义呢?其实是这样的,我们在定义该参数的时候只要浮点型数组中元素个数大于等于2即可,也就是说上面我们的代码可以写成这样的:

mEffects[3] = new DashPathEffect(new float[] {20, 10}, mPhase);  

float[] {20, 10}的偶数参数20(注意数组下标是从0开始哦)定义了我们第一条实线的长度,而奇数参数10则表示第一条虚线的长度,如果此时数组后面不再有数据则重复第一个数以此往复循环,比如我们20,10后没数了,那么整条线就成了[20,10,20,10,20,10…………………………]这么一个状态

而DashPathEffect的第二个参数我称之为偏移值,动态改变其值会让路径产生动画的效果,上面代码已给出大家可以自己去试试;PathDashPathEffect和DashPathEffect是类似的,不同的是PathDashPathEffect可以让我们自己定义路径虚线的样式,比如我们将其换成一个个小圆组成的虚线

Path path = new Path();  
path.addCircle(0, 0, 3, Direction.CCW);  
mEffects[4] = new PathDashPathEffect(path, 12, mPhase, PathDashPathEffect.Style.ROTATE);  

这里写图片描述

ComposePathEffect和SumPathEffect都可以用来组合两种路径效果,唯一不同的是组合的方式,ComposePathEffect(PathEffect outerpe, PathEffect innerpe)会先将路径变成innerpe的效果,再去复合outerpe的路径效果,即:outerpe(innerpe(Path));而SumPathEffect(PathEffect first, PathEffect second)则会把两种路径效果加起来再作用于路径

public class ECGView extends View {
    /**
     * 画笔
     */
    private Paint mPaint;

    /**
     * 路径对象
     */
    private Path mPath;

    /**
     * 路径初始坐标
     */
    private float x, y;

    /**
     * 屏幕 宽高
     */
    private int screenW, screenH;// 屏幕宽高

    /**
     * 屏幕初始宽度
     */
    private float initScreenW;

    /**
     * 初始X轴坐标
     */
    private float initX;

    /**
     * 画布移动的距离
     */
    private float tranX, moveX;

    /**
     * 画布是否需要平移
     */
    private boolean isCanvasMove;

    /**
     * 构造函数
     */
    public ECGView(Context context) {
        this(context, null);
    }

    public ECGView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ECGView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        initPaint();

    }

    /**
     * 初始化画笔
     */
    private void initPaint() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);
        //设置画笔颜色
        mPaint.setColor(Color.GREEN);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        //阴影层效果
        mPaint.setShadowLayer(7, 0, 0, Color.GREEN);

        //实例化路径
        mPath = new Path();

        tranX = 0;

        isCanvasMove = false;

    }

    @Override
    public void onSizeChanged(int w, int h, int oldw, int oldh) {
        /*
         * 获取屏幕宽高
         */
        screenW = w;
        screenH = h;

        /*
         * 设置起点坐标
         */
        x = 0;
        y = (screenH / 2) + (screenH / 4) + (screenH / 10);

        // 屏幕初始宽度
        initScreenW = screenW;

        // 初始X轴坐标
        initX = ((screenW / 2) + (screenW / 4));

        moveX = (screenW / 24);

        mPath.moveTo(x, y);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawColor(Color.BLACK);

        mPath.lineTo(x, y);

        // 向左平移画布
        canvas.translate(-tranX, 0);

        // 计算坐标
        calCoors();

        // 绘制路径
        canvas.drawPath(mPath, mPaint);
        invalidate();

    }

    /**
     * 计算坐标
     */
    private void calCoors() {
        if (isCanvasMove == true) {
            tranX += 4;
        }

        if (x < initX) {
            x += 8;
        } else {
            if (x < initX + moveX) {
                x += 2;
                y -= 8;
            } else {
                if (x < initX + (moveX * 2)) {
                    x += 2;
                    y += 14;
                } else {
                    if (x < initX + (moveX * 3)) {
                        x += 2;
                        y -= 12;
                    } else {
                        if (x < initX + (moveX * 4)) {
                            x += 2;
                            y += 6;
                        } else {
                            if (x < initScreenW) {
                                x += 8;
                            } else {
                                isCanvasMove = true;
                                initX = initX + initScreenW;
                            }
                        }
                    }
                }
            }

        }
    }
}

这里写图片描述

基本逻辑:
主要是在计算坐标那:
这里写图片描述

最开始isCanvasMove=false;可以看到在最后一个else中才改变,所以暂时不管,反正这个if语句中的内容暂时不执行;
这里写图片描述

setStrokeCap(Paint.Cap cap)

方法,该方法用来设置我们画笔的笔触风格,上面的例子中我使用的是ROUND,表示是圆角的笔触,那么什么叫笔触呢,其实很简单,就像我们现实世界中的笔,如果你用圆珠笔在纸上戳一点,那么这个点一定是个圆,即便很小,它代表了笔的笔触形状,如果我们把一支铅笔笔尖削成方形的,那么画出来的线条会是一条弯曲的“矩形”,这就是笔触的意思。除了ROUND,Paint.Cap还提供了另外两种类型:SQUARE和BUTT,具体大家自己去try~~
setStrokeJoin(Paint.Join join)

这个方法用于设置结合处的形态,就像上面的代码中我们虽说是花了一条心电线,但是这条线其实是由无数条小线拼接成的,拼接处的形状就由该方法指定。
上面的例子中我们还使用到了一个方法
setShadowLayer(float radius, float dx, float dy, int shadowColor)

该方法为我们绘制的图形添加一个阴影层效果:

public class ShadowView extends View {
    /**
     * 画笔
     */
    private Paint mPaint;

    /**
     * 构造函数
     */
    public ShadowView(Context context) {
        this(context, null);
    }

    public ShadowView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ShadowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // setShadowLayer不支持HW
        setLayerType(LAYER_TYPE_SOFTWARE, null);

        initPaint();
    }

    /**
     * 初始化画笔
     */
    private void initPaint() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setShadowLayer(10, 3, 3, Color.DKGRAY);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawRect(100, 200, 600, 700, mPaint);
    }
}

这里写图片描述

上面我们讲MaskFilter的时候曾用其子类BlurMaskFilter模拟过类似效果,跟BlurMaskFilter比起来这方法是不是更简捷呢?但是BlurMaskFilter能做的setShadowLayer却不一定能做到哦!


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值