自定义控件其实很简单1/4

尊重原创转载请注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵权必究!

炮兵镇楼

上一回关羽操刀怒砍秦桧子龙拼命相救,岂料刘备这狗贼耍赖以张飞为祭品特殊召唤黑暗大法师消灭了场上所有逗逼,霎时间血流成河,鲜红的血液与冰冷的大地融合交汇在一起焕发出血液的煞气……那么,问题来了,请问这是使用了哪种PorterDuffXfermode?

在上一节的最后一个Example中我们做了一个橡皮擦的View,但是这个View虽然在效果上没有什么问题,但是逻辑确实有问题的!你们发现了么?哥故意挖了个坑让你们往里面跳哦!!!

在对Xfermode和ColorFilter有了深情的了解后我们不能只爱上这俩二货,前方必定还有更多的好货色在等着我们开发……^_^~!今天我们继续向前看看Paint的其他一些“另类”的属性。

笔对于我们来说第一印象一定是能写字对吧,而Android给我们的这支Paint当然也不例外,它也定义了大量关于“写字”的功能,这些方法总数接近Paint的一半!可见Android对Paint写字功能的重视,在讲Paint提供的“写字”方法前我先给大家说一个Android中和字体相关的很重要的类

FontMetrics

FontMetrics意为字体测量,这么一说大家是不是瞬间感受到了这玩意的重要性?那这东西有什么用呢?我们通过源码追踪进去可以看到FontMetrics其实是Paint的一个内部类,而它里面呢就定义了top,ascent,descent,bottom,leading五个成员变量其他什么也没有:

自定义控件其实很简单1/4_第1张图片

这五个成员变量除了top和bottom我们较熟悉外其余三个都很陌生是做什么用的呢?首先我给大家看张图:

自定义控件其实很简单1/4_第2张图片

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

自定义控件其实很简单1/4_第3张图片

然而根据世界范围内已入案的使用语言中能够标注在字符上方或者下方的除了类似的符号肯定是数不胜数的……哥不是语言专家我母鸡啊……而top的意思其实就是除了Baseline到字符顶端的距离外还应该包含这些符号的高度,bottom的意思也是一样,一般情况下我们极少使用到类似的符号所以往往会忽略掉这些符号的存在,但是Android依然会在绘制文本的时候在文本外层留出一定的边距,这就是为什么top和bottom总会比ascent和descent大一点的原因。而在TextView中我们可以通过xml设置其属性android:includeFontPadding="false"去掉一定的边距值但是不能完全去掉。下面我们在Canvas上绘制一段文本并尝试打印文本的top,ascent,descent,bottom和leading:

public class FontView extends View {
	private static final String TEXT = "ap爱哥ξτβбпшㄎㄊěǔぬも┰┠№@↓";
	private Paint mPaint;// 画笔
	private FontMetrics mFontMetrics;// 文本测量对象

	public FontView(Context context) {
		this(context, null);
	}

	public FontView(Context context, AttributeSet attrs) {
		super(context, attrs);

		// 初始化画笔
		initPaint();
	}

	/**
	 * 初始化画笔
	 */
	private void initPaint() {
		// 实例化画笔
		mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
		mPaint.setTextSize(50);
		mPaint.setColor(Color.BLACK);

		mFontMetrics = mPaint.getFontMetrics();

		Log.d("Aige", "ascent:" + mFontMetrics.ascent);
		Log.d("Aige", "top:" + mFontMetrics.top);
		Log.d("Aige", "leading:" + mFontMetrics.leading);
		Log.d("Aige", "descent:" + mFontMetrics.descent);
		Log.d("Aige", "bottom:" + mFontMetrics.bottom);
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		canvas.drawText(TEXT, 0, Math.abs(mFontMetrics.top), mPaint);
	}
}
logcat输出如下:
自定义控件其实很简单1/4_第4张图片

注:Baseline上方的值为负,下方的值为正

如图我们得到了top,ascent,descent,bottom和leading的值,因为只有一行文本所以leading恒为0,那么此时的显示效果是如何的呢?上面我们说到Android中文本的绘制是从Baseline开始的,在屏幕上的体现便是Y轴坐标,所以在

canvas.drawText(TEXT, 0, Math.abs(mFontMetrics.top), mPaint);
中我们将文本绘制的起点Y坐标向下移动Math.abs(mFontMetrics.top)个单位(注:mFontMetrics.top是负数),相当于把文本的Baseline向下移动Math.abs(mFontMetrics.top)个单位,此时文本的顶部刚好会和屏幕顶部重合:


从代码中我们可以看到一个很特别的现象,在我们绘制文本之前我们便可以获取文本的FontMetrics属性值,也就是说我们FontMetrics的这些值跟我们要绘制什么文本是无关的,而仅与绘制文本Paint的size和typeface有关我们来分别更改这两个值看看:

mPaint.setTextSize(70);
自定义控件其实很简单1/4_第5张图片

如图所示所有值都改变了,我们再为Paint设置一个typeface:

mPaint.setTypeface(Typeface.SERIF);
自定义控件其实很简单1/4_第6张图片
同样所有的值也改变了,那么我们知道这样的一个东西有什么用呢?如上所说文本的绘制是从Baseline开始,并且Baseline并非文本的分割线,当我们想让文本绘制的时候居中屏幕或其他的东西时就需要计算Baseline的Y轴坐标,比如我们让我们的文本居中画布:

public class FontView extends View {
	private static final String TEXT = "ap爱哥ξτβбпшㄎㄊ";
	private Paint textPaint, linePaint;// 文本的画笔和中心线的画笔

	private int baseX, baseY;// Baseline绘制的XY坐标

	public FontView(Context context) {
		this(context, null);
	}

	public FontView(Context context, AttributeSet attrs) {
		super(context, attrs);

		// 初始化画笔
		initPaint();
	}

	/**
	 * 初始化画笔
	 */
	private void initPaint() {
		// 实例化画笔
		textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
		textPaint.setTextSize(70);
		textPaint.setColor(Color.BLACK);

		linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
		linePaint.setStyle(Paint.Style.STROKE);
		linePaint.setStrokeWidth(1);
		linePaint.setColor(Color.RED);
	}

	@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);
	}
}
效果如图:
自定义控件其实很简单1/4_第7张图片

Baseline绘制的起点x坐标为画布宽度的一半(中点x坐标)减去文本宽度的一半(这里我们的画布大小与屏幕大小一样),这个很好理解,而y坐标为画布高度的一半(中点y坐标)减去ascent和descent绝对值之差的一半,这一点很多朋友可能不是很好理解,其实很简单,如果直接以画布的中心为Baseline:

baseY = canvas.getHeight() / 2;
那么画出来的效果必定是如下的样子
自定义控件其实很简单1/4_第8张图片

也就是说Baseline和屏幕中线重合,而这样子绘制出来的文本必定不在屏幕中心,因为ascent的距离大于descent的距离(大多数情况下我们没有考虑top和bottom),那么我们就需要将Baseline往下移使绘制出来的文本能在中心

自定义控件其实很简单1/4_第9张图片

那么该下移多少呢?这是一个问题,很多童鞋的第一反应是下移ascent的一半高度,但是你要考虑到已经在中线下方的descent的高度,所以我们应该先在ascent的高度中减去descent的高度再除以二再让屏幕的中点Y坐标(也就是高度的一半)加上这个偏移值

baseY = (int) ((canvas.getHeight() / 2) + ((Math.abs(textPaint.ascent()-Math.abs(textPaint.descent()))) / 2));
这个公式跟我们上面代码中的是一样的,不信大家可以自己算算这里就不多说了。这里我们的需求是让文本绘制在某个区域的中心,实际情况中有很多不同的需求不如靠近某个区域离某个区域需要多少距离等等,熟练地去学会计算文本测绘中的各个值就显得很有必要了!

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

public class StaticLayoutView extends View {
	private static final String TEXT = "This is used by widgets to control text layout. You should not need to use this class directly unless you are implementing your own widget or custom display object, or would be tempted to call Canvas.drawText() directly.";
	private TextPaint mTextPaint;// 文本的画笔
	private StaticLayout mStaticLayout;// 文本布局

	public StaticLayoutView(Context context) {
		this(context, null);
	}

	public StaticLayoutView(Context context, AttributeSet attrs) {
		super(context, attrs);

		// 初始化画笔
		initPaint();
	}

	/**
	 * 初始化画笔
	 */
	private void initPaint() {
		// 实例化画笔
		mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
		mTextPaint.setTextSize(50);
		mTextPaint.setColor(Color.BLACK);
	}

	@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();
	}
}
运行效果如下:

自定义控件其实很简单1/4_第10张图片

好了,对Paint绘制文本的一个简单了解就先到这,我们来看看Paint中到底提供了哪些实用的方法来绘制文本

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)。这些方法在一些结合文本处理的应用里比较常用,比如文本阅读器的翻页效果,我们需要在翻页的时候动态折断或生成一行字符串,这就派上用场了~~~

getFontMetrics (Paint.FontMetrics metrics)

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

getFontMetricsInt()

该方法返回了一个FontMetricsInt对象,FontMetricsInt和FontMetrics是一样的,只不过FontMetricsInt返回的是int而FontMetrics返回的是float

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);
我们将会得到如下效果:

自定义控件其实很简单1/4_第11张图片
这里我用了一个卡通的字体,而另外两个方法也类似的我就不讲了。

说到文本大家第一时间想到的应该是TextView,其实在TextView里我们依然可以找到上面很多方法的影子,比如我们可以从TextView中获取到TextPaint:

TextPaint paint = mTextView.getPaint();
当然也可以设置TextView的字体等等:
Typeface typeface = Typeface.createFromAsset(getAssets(), "kt.ttf");
mTextView.setTypeface(typeface);
更多的雷同点还是留给大家去发掘,下面继续来看

setTextSkewX(float skewX)

这个方法可以设置文本在水平方向上的倾斜,效果类似下图:

// 设置画笔文本倾斜
textPaint.setTextSkewX(-0.25F);
自定义控件其实很简单1/4_第12张图片
这个倾斜值没有具体的范围,但是官方推崇的值为-0.25可以得到比较好的倾斜文本效果,值为负右倾值为正左倾,默认值为0

setTextSize (float textSize)

不说了但是要注意该值必需大于零

setTextScaleX (float scaleX)

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

// 设置画笔文本倾斜
textPaint.setTextScaleX(0.5F);
自定义控件其实很简单1/4_第13张图片
// 设置画笔文本倾斜
textPaint.setTextScaleX(1.5F);
自定义控件其实很简单1/4_第14张图片
大家注意哦!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);
当我们将文本对齐方式设置为CENTER后就相当于告诉Android我们这个文本绘制的时候从文本的中点开始向两端绘制,如果设置为LEFT则从文本的左端开始往右绘制,如果为RIGHT则从文本的右端开始往左绘制:

自定义控件其实很简单1/4_第15张图片

setSubpixelText (boolean subpixelText)

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

setStrikeThruText (boolean strikeThruText)

文本删除线,不扯

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中还新增了两个方法这里就先不讲了,大家可以看到虽然说这些方法很多很多但是效果都是显而易见的,很多方法大家一试就知道所以哥也没有做太多的测试之类什么什么的,这样讲东西是很累的,关于文本也没有什么有趣的Demo可以玩~~~~~so~~~~~Fuck……

下面我们来看一个比较深奧的东西

setDither(boolean dither)

这玩意用来设置我们在绘制图像时的抗抖动,也称为递色,那什么叫抗抖动呢?在Android中我确实不好拿出一个明显的例子,我就在PS里模拟说明一下

自定义控件其实很简单1/4_第16张图片

大家看到的这张七彩渐变图是一张RGB565模式下图片,即便图片不是很大我们依然可以很清晰地看到在两种颜色交接的地方有一些色块之类的东西感觉很不柔和,因为在RGB模式下只能显示2^16=65535种色彩,因此很多丰富的色彩变化无法呈现,而Android呢为我们提供了抗抖动这么一个方法,它会将相邻像素之间颜色值进行一种“中和”以呈现一个更细腻的过渡色:

自定义控件其实很简单1/4_第17张图片

放大来看,其在很多相邻像素之间插入了一个“中间值”:

自定义控件其实很简单1/4_第18张图片

抗抖动不是Android的专利,是图形图像领域的一种解决位图精度的技术。上面说了太多理论性的东西,估计大家都疲惫了,接下来我们来瞅瞅一个比较酷的东西MaskFilter遮罩过滤器!在Paint我们有个方法来设置这东西

setMaskFilter(MaskFilter maskfilter)

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

BlurMaskFilter

Android中的很多自带控件都有类似软阴影的效果,比如说Button


它周围就有一圈很淡的阴影效果,这种效果看起来让控件更真实,那么是怎么做的呢?其实很简单,使用BlurMaskFilter就可以得到类似的效果

public class MaskFilterView extends View {
	private static final int RECT_SIZE = 800;
	private Paint mPaint;// 画笔
	private Context mContext;// 上下文环境引用

	private int left, top, right, bottom;//

	public MaskFilterView(Context context) {
		this(context, null);
	}

	public MaskFilterView(Context context, AttributeSet attrs) {
		super(context, attrs);
		mContext = context;

		// 初始化画笔
		initPaint();

		// 初始化资源
		initRes(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 BlurMaskFilter(20, BlurMaskFilter.Blur.SOLID));
	}

	/**
	 * 初始化资源
	 */
	private void initRes(Context context) {
		/*
		 * 计算位图绘制时左上角的坐标使其位于屏幕中心
		 */
		left = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - RECT_SIZE / 2;
		top = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - RECT_SIZE / 2;
		right = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 + RECT_SIZE / 2;
		bottom = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 + RECT_SIZE / 2;
	}

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

		// 画一个矩形
		canvas.drawRect(left, top, right, bottom, mPaint);
	}
}
代码中我们在画布中央绘制了一个正方形,并设置了了它的模糊滤镜,但是当你运行后发现并没有任何的效果:

自定义控件其实很简单1/4_第19张图片

为什么会这样呢?还记得上一节中我们讲的AvoidXfermode么,在API 16的时候该类已经被标注为过时了,因为AvoidXfermode不支持硬件加速,如果在API 16+上想获得正确的效果就必需关闭应用的硬件加速,当时我们是在AndroidManifest.xml文件中设置android:hardwareAccelerated为false来关闭的,具体有哪些绘制的方法不支持硬件加速可以参考下图

自定义控件其实很简单1/4_第20张图片

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

setLayerType(LAYER_TYPE_SOFTWARE, null);
来关闭单个View的硬件加速功能

再次运行即可得到正确的效果:

自定义控件其实很简单1/4_第21张图片

是不是很酷呢?BlurMaskFilter只有一个含参的构造函数BlurMaskFilter(float radius, BlurMaskFilter.Blur style),其中radius很容易理解,值越大我们的阴影越扩散,比如在上面的例子中我将radius改为50

自定义控件其实很简单1/4_第22张图片

可以明显感到阴影的范围扩大了,这个很好理解。而第二个参数style表示的是模糊的类型,上面我们用到的是SOLID,其效果就是在图像的Alpha边界外产生一层与Paint颜色一致的阴影效果而不影响图像本身,除了SOLID还有三种,NORMAL,OUTER和INNER,NORMAL会将整个图像模糊掉:

自定义控件其实很简单1/4_第23张图片

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

自定义控件其实很简单1/4_第24张图片

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

自定义控件其实很简单1/4_第25张图片

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

public class BlurMaskFilterView extends View {
	private Paint shadowPaint;// 画笔
	private Context mContext;// 上下文环境引用
	private Bitmap srcBitmap, shadowBitmap;// 位图和阴影位图

	private int x, y;// 位图绘制时左上角的起点坐标

	public BlurMaskFilterView(Context context) {
		this(context, null);
	}

	public BlurMaskFilterView(Context context, AttributeSet attrs) {
		super(context, attrs);
		mContext = context;
		// 记得设置模式为SOFTWARE
		setLayerType(LAYER_TYPE_SOFTWARE, null);

		// 初始化画笔
		initPaint();

		// 初始化资源
		initRes(context);
	}

	/**
	 * 初始化画笔
	 */
	private void initPaint() {
		// 实例化画笔
		shadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
		shadowPaint.setColor(Color.DKGRAY);
		shadowPaint.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.NORMAL));
	}

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

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

		/*
		 * 计算位图绘制时左上角的坐标使其位于屏幕中心
		 */
		x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - srcBitmap.getWidth() / 2;
		y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - srcBitmap.getHeight() / 2;
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		// 先绘制阴影
		canvas.drawBitmap(shadowBitmap, x, y, shadowPaint);

		// 再绘制位图
		canvas.drawBitmap(srcBitmap, x, y, null);
	}
}
如代码所示我们通过Bitmap的extractAlpha()方法从原图中分离出一个Alpha通道位图并在计算模糊滤镜的时候使用该位图生成模糊效果:

自定义控件其实很简单1/4_第26张图片

相对于BlurMaskFilter来说

EmbossMaskFilter

的常用性比较低,倒不是说EmbossMaskFilter很没用,只是相对于EmbossMaskFilter实现的效果来说远不及BlurMaskFilter给人的感觉霸气,说了半天那么EmbossMaskFilter到底是做什么的呢?

我们先来看一张图:

自定义控件其实很简单1/4_第27张图片

这么一个看着像巧克力的东西就是用EmbossMaskFilter实现了,正如其名,他可以实现一种类似浮雕的效果,说白了就是让你绘制的图像感觉像是从屏幕中“凸”起来更有立体感一样(在设计软件中类似的效果称之为斜面浮雕)。该类也只有一个含参的构造方法EmbossMaskFilter(float[] direction, float ambient, float specular, float blurRadius),这些参数理解起来要比BlurMaskFilter困难得多,如果你没有空间想象力的话,首先我们来看第一个direction指的是方向,什么方向呢?光照的方向!如果大家接触过三维设计就一定懂,没接触也没关系,我跟你说明白。假设一个没有任何光线的黑屋子里有一张桌子,桌子上有一个小球,这时我们打开桌子上的台灯,台灯照亮了小球,这时候小球的状态与下图类似:

自定义控件其实很简单1/4_第28张图片

PS:略草,凑合看

小球最接近光源的地方肯定是最亮的这个没有异议,在参数中specular就是跟高光有关的,其值是个双向值越小或越大高光越强中间值则是最弱的,那么再看看什么是ambient呢?同样我们看个球,你会发现即便只有一盏灯光,在球底部跟桌面相接的地方依然不会出现大片的“死黑”,这是因为光线在传播的过程中碰到物体会产生反射!这种反射按照物体介质的粗糙度可以分为漫反射和镜面反射,而这里我们的小球之所以背面没有直接光照但仍能有一定的亮度就是因为大量的漫反射在空间传播让光线间接照射到小球背面,这种区别于直接照明的二次照明我们称之为间接照明,产生的光线叫做环境光ambient,参数中的该值就是用来设置环境光的,在Android中环境光默认为白色,其值越大,阴影越浅,blurRadius则是设置图像究竟“凸”出多大距离的很好理解,最难理解的一个参数是direction,上面我们也说了是光照方向的意思,该数组必须要有而且只能有三个值即float[x,y,z],这三个值代表了一个空间坐标系,我们的光照方向则由其定义,那么它是怎么定义的呢?首先x和y很好理解,平面的两个维度嘛是吧,上面我们使用的是[1,1]也就是个45度角,而z轴表示光源是在屏幕后方还是屏幕前方,上面我们是用的是1,正值表示光源往屏幕外偏移1个单位,负值表示往屏幕里面偏移,这么一说如果我把其值改为[1,1,-1]那么我们的巧克力朝着我们的一面应该就看不到了对吧,试试看撒~~~这个效果我就不截图了,因为一片漆黑……但是你依然能够看到一点点灰度~就是因为我们的环境光ambient!,如果我们把值改为[1,1,2]往屏幕外偏移两个单位,那么我们巧克力正面光照将更强:

自定义控件其实很简单1/4_第29张图片

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

额……忘了给代码,大家可以自己去试试

public class EmbossMaskFilterView extends View {
	private static final int H_COUNT = 2, V_COUNT = 4;// 水平和垂直切割数
	private Paint mPaint;// 画笔
	private PointF[] mPointFs;// 存储各个巧克力坐上坐标的点

	private int width, height;// 单个巧克力宽高
	private float coorY;// 单个巧克力坐上Y轴坐标值

	public EmbossMaskFilterView(Context context) {
		this(context, null);
	}

	public EmbossMaskFilterView(Context context, AttributeSet attrs) {
		super(context, attrs);
		// 不使用硬件加速
		setLayerType(LAYER_TYPE_SOFTWARE, null);

		// 初始化画笔
		initPaint();

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

	/**
	 * 初始化画笔
	 */
	private void initPaint() {
		// 实例化画笔
		mPaint = new Paint();
		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) {
		int[] screenSize = MeasureUtil.getScreenSize((Activity) context);

		width = screenSize[0] / H_COUNT;
		height = screenSize[1] / V_COUNT;

		int count = V_COUNT * H_COUNT;

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

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

		// 画矩形
		for (int i = 0; i < V_COUNT * H_COUNT; i++) {
			canvas.drawRect(mPointFs[i].x, mPointFs[i].y, mPointFs[i].x + width, mPointFs[i].y + height, mPaint);
		}
	}
}
上面我们说了EmbossMaskFilter的使用面并不是很大,因为所说其参数稍复杂但是其实现原理是简单粗暴的,简而言之就是根据参数在图像周围绘制一个“色带”来模拟浮雕的效果,如果我们的图像很复杂EmbossMaskFilter很难会正确模拟,所以一般遇到这类图直接call美工 = = 哈哈哈。

setRasterizer (Rasterizer rasterizer)

设置光栅,光栅这东西涉及太多太多物理知识,不讲了一讲又是一大堆,而且该方法同样不支持HW在API 21中遗弃了~~~我们还是来看看对我们来说更好玩有趣的方法

setPathEffect(PathEffect effect)

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

自定义控件其实很简单1/4_第30张图片

这六个子类分别可以实现不同的路径效果:

自定义控件其实很简单1/4_第31张图片

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

public class PathEffectView extends View {
	private float mPhase;// 偏移值
	private Paint mPaint;// 画笔对象
	private Path mPath;// 路径对象
	private PathEffect[] mEffects;// 路径效果数组

	public PathEffectView(Context context, AttributeSet attrs) {
		super(context, attrs);

		/*
		 * 实例化画笔并设置属性
		 */
		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 <= 30; 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);

			// 每绘制一条将画布向下平移250个像素
			canvas.translate(0, 250);
		}

		// 刷新偏移值并重绘视图实现动画效果
		mPhase += 1;
		invalidate();
	}
}

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

mEffects[1] = new CornerPathEffect(50);

Look Pic是不是更平滑了呢?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…………………………]这么一个状态,当然如果你无聊,也可以:

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

而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)则会把两种路径效果加起来再作用于路径,具体区别大家去试试吧…………哥累了睡会~~~囧……

记得在1/12中我们绘制了了一个圆环并让其实现动画的效果,当时我们使用了线程来使其产生动画,但是我们是不是也可以像上面的例子一样直接在onDraw中invalidate()来产生动画呢?这个问题留给大家。

在1/12中我们还说过尽量不要在onDraw中使用new关键字来生成对象,但是上例的代码中我们却在频繁地使用,但是六个PathEffect的子类中除了构造方法什么都没有,我们该如何避免频繁地去new对象呢?这个问题也留给大家思考。

Path应用的广泛性注定了PathEffect应用的广泛,所谓一人得道鸡犬升天就是这么个道理,只要是Path能存在的地方都可以考虑使用,下面我们来模拟一个类似心电图的路径小动画:

自定义控件其实很简单1/4_第32张图片

自定义控件其实很简单1/4_第33张图片

这种效果呢也是非常非常地简单,说白了就是无数条短小精悍的小“Path”连接成一条完整的心电路径:

public class ECGView extends View {
	private Paint mPaint;// 画笔
	private Path mPath;// 路径对象

	private int screenW, screenH;// 屏幕宽高
	private float x, y;// 路径初始坐标
	private float initScreenW;// 屏幕初始宽度
	private float initX;// 初始X轴坐标
	private float transX, moveX;// 画布移动的距离

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

	public ECGView(Context context, AttributeSet set) {
		super(context, set);

		/*
		 * 实例化画笔并设置属性
		 */
		mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
		mPaint.setColor(Color.GREEN);
		mPaint.setStrokeWidth(5);
		mPaint.setStrokeCap(Paint.Cap.ROUND);
		mPaint.setStrokeJoin(Paint.Join.ROUND);
		mPaint.setStyle(Paint.Style.STROKE);
		mPaint.setShadowLayer(7, 0, 0, Color.GREEN);

		mPath = new Path();
		transX = 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
	public void onDraw(Canvas canvas) {
		canvas.drawColor(Color.BLACK);

		mPath.lineTo(x, y);

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

		// 计算坐标
		calCoors();

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

	/**
	 * 计算坐标
	 */
	private void calCoors() {
		if (isCanvasMove == true) {
			transX += 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;
							}
						}
					}
				}
			}

		}
	}
}
我们在onSizeChanged(int w, int h, int oldw, int oldh)方法中获取屏幕的宽高,该方法的具体用法我们会在7/12学习View的测绘时具体说明,这里就先不说了

上面在设置Paint属性的时候我们使用到了一个

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 static final int RECT_SIZE = 800;// 方形大小
	private Paint mPaint;// 画笔

	private int left, top, right, bottom;// 绘制时坐标

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

		// 初始化画笔
		initPaint();

		// 初始化资源
		initRes(context);
	}

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

	/**
	 * 初始化资源
	 */
	private void initRes(Context context) {
		/*
		 * 计算位图绘制时左上角的坐标使其位于屏幕中心
		 */
		left = MeasureUtil.getScreenSize((Activity) context)[0] / 2 - RECT_SIZE / 2;
		top = MeasureUtil.getScreenSize((Activity) context)[1] / 2 - RECT_SIZE / 2;
		right = MeasureUtil.getScreenSize((Activity) context)[0] / 2 + RECT_SIZE / 2;
		bottom = MeasureUtil.getScreenSize((Activity) context)[1] / 2 + RECT_SIZE / 2;
	}

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

		// 先绘制位图
		canvas.drawRect(left, top, right, bottom, mPaint);
	}
}
radius表示阴影的扩散半径,而dx和dy表示阴影平面上的偏移值,shadowColor就不说了阴影颜色,最后提醒一点setShadowLayer同样不支持HW哦!

自定义控件其实很简单1/4_第34张图片

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

至此,Paint下的几乎所有方法我们都已经学习了,正如我之前所说,工欲善其事必先利其器,自定义View很重要的一部分就是如何去画一个Perfect的图形,Android给我们提供了绝大部分的方法和类来模拟现实中真正的画笔,我们只需要学会如何灵活运用即可,牛逼的人不需要复杂的技术即可实现复杂的效果~~这就是实力、才是真大神~~~~So~共勉

PS:这一节讲得有点戳~~Because something happen in my life maybe changed my life,还有最近忙着改的一个相机应用的开源项目,所以很多事 = =,讲得不是很好大家见谅,下一期补上更精彩的内容

源码地址:传送门

温馨提示:自定义控件其实很简单系列文章每周一、周四更新一篇~

下集精彩预告:Paint中的方法几乎都已经概述了一遍,但是有个方法我们还没说setShader(Shader shader),这个方法很重要吗?其实一般,但是其涉及到的一个东西对我们来说相当重要:Matrix,这个神秘的东西究竟是做什么用的呢?卖个关子先,下一节我们将会结束整个Paint的学习,是不是有点想跃跃一试的冲动?下期我将会给大家带来两个好玩的例子,这两个例子来自于群里朋友的提问,不多说了,上图:

自定义控件其实很简单1/4_第35张图片 自定义控件其实很简单1/4_第36张图片 自定义控件其实很简单1/4_第37张图片 自定义控件其实很简单1/4_第38张图片

锁定本台敬请关注:自定义控件其实很简单1/3

你可能感兴趣的:(android,view,android自定义控件,custom,android自定义View)