前言:再重复一遍我很喜欢的一句话,送给大家:迷茫,本就是青春该有的样子 ,但不要让未来的你,讨厌现在的自己
上篇给大家讲解了如何控件添加阴影效果,但是在为Bitmap图片添加阴影时,却没办法添加具有指定颜色的阴影,这篇我们就来使用自定义的控件及自定义属性来初步封装下控件。前方高能预警——本篇内容涉及内容较多,难度较大,需要多加思考。
一、使用BlurMaskFilter为图片构造定色阴影效果
上面我们讲了通过setShadowLayer为图片添加阴影效果,但是图片的的阴影是用原图形的副本加上边缘发光效果组成的。我们怎么能给图片添加一个灰色的阴影呢?
我们来分析一下setShadowLayer的阴影形成过程(假定阴影画笔是灰色),对于文字和图形,它首先产生一个跟原型一样的灰色副本。然后对这个灰色副本应用BlurMaskFilter,使其内外发光;这样就形成了所谓的阴影!当然最后再偏移一段距离。
所以,我们要给图片添加灰色阴影效果,所以我们能不能仿一下这个过程:先绘制一个跟图片一样大小的灰色图形,然后给这个灰色图形应用BlurMaskFilter使其内外发光,然后偏移原图形绘制出来,不就可以了么
所以,这里涉及到三个点:
- 绘制出一个一样大小的灰色图形
- 对灰色图形应用BlurMaskFilter使其内外发光
- 偏移原图形一段距离绘制出来
下面我们就通过例子来一步步看是怎么实现出来的吧
1、绘制出一个一样大小的灰色图像
首先,我们来看怎么能绘出一个指定Bitmap所对应的灰色图像。我们知道canvas.drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)中的paint的画笔颜色对画出来的bitmap是没有任何影响的,因为原来Bitmap长什么样,无论你画笔是什么颜色,画出来的图片还是跟原图片长的一样。所以如果我们需要画一张对应的灰色图像,我们需要新建一个一样大小的空白图,但是新图片的透明度要与原图片保持一致。所以如何从原图片中抽出Alpha值成为了关键。即我们只需要创建一个与原图片一样大小且Alpha相同的图片即可。
其实Bitmap中已经存在抽取出只具有Alpha值图片的函数:
public Bitmap extractAlpha();
extraAlpha()函数的功能是:新建一张空白图片,图片具有与原图片一样的Alpha值,这个新建的Bitmap做为结果返回。这个空白图片中每个像素都具有与原图片一样的Alpha值,而且具体的颜色时,只有在使用canvas.drawBitmap绘制时,由传入的paint的颜色指定。
所以总结来讲:
- extractAlpha()新建一张仅具有Alpha值的空白图像
- 这张图像的颜色,是由canvas.drawBitmap时的画笔指定的。
(1)、extractAlpha()使用示例
下面,我们就用个例子先来看下extractAlpha()函数的用法
我们拿一张图片来做试验,下面这张PNG图片中,一只小猫和一只小狗,其余地方都是透明色。
下面我们分别利用extractAlpha()画出它对应的红色和绿色的阴影图
对应的代码为:
public class ExtractAlphaView extends View {
private Paint mPaint;
private Bitmap mBitmap,mAlphaBmp;
public ExtractAlphaView(Context context) {
super(context);
init();
}
public ExtractAlphaView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ExtractAlphaView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init(){
setLayerType(LAYER_TYPE_SOFTWARE,null);
mPaint = new Paint();
mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.blog12);
mAlphaBmp = mBitmap.extractAlpha();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = 200;
int height = width * mAlphaBmp.getHeight()/mAlphaBmp.getWidth();
mPaint.setColor(Color.RED);
canvas.drawBitmap(mAlphaBmp,null,new Rect(0,0,width,height),mPaint);
mPaint.setColor(Color.GREEN);
canvas.drawBitmap(mAlphaBmp,null,new Rect(0,height,width,2*height),mPaint);
}
}
首先看init函数:
private void init(){
setLayerType(LAYER_TYPE_SOFTWARE,null);
mPaint = new Paint();
mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.blog12);
mAlphaBmp = mBitmap.extractAlpha();
}
首先是禁用硬件加速,这基本上是我们做自定义控件的标配!为了防止功能不好用,记得每次都加上这个函数!然后是利用extratAlpha()来生成仅具有透明度的空白图像。
最后看OnDraw函数:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = 200;
int height = width * mAlphaBmp.getHeight()/mAlphaBmp.getWidth();
mPaint.setColor(Color.RED);
canvas.drawBitmap(mAlphaBmp,null,new Rect(0,0,width,height),mPaint);
mPaint.setColor(Color.GREEN);
canvas.drawBitmap(mAlphaBmp,null,new Rect(0,height,width,2*height),mPaint);
}
这里分别将画笔的颜色设置为红色和绿色,然后两次把mAlphaBmp画出来。上面我们已经提到,在画仅具有透明度的空白图像时,图像的颜色是由画笔颜色指定的。所以从效果图中也可以看出画出来的图像分别红色的绿色的。
这就是Bitmpa.extraAlpha()的用法!
2、对灰色图形应用BlurMaskFilter使其内外发光
在第一步完成了之后,我们进行第二步,将阴影添加内外发光效果。就形成了阴影的模样。
代码很简单,只需要使用Paint.setMaskFilter函数添加发光效果即可,代码如下:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = 200;
int height = width * mAlphaBmp.getHeight()/mAlphaBmp.getWidth();
mPaint.setColor(Color.RED);
mPaint.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.NORMAL));
canvas.drawBitmap(mAlphaBmp,null,new Rect(0,0,width,height),mPaint);
mPaint.setColor(Color.GREEN);
canvas.drawBitmap(mAlphaBmp,null,new Rect(0,height,width,2*height),mPaint);
}
明显可以看出这里只添加了一行代码:mPaint.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.NORMAL));
就是添加内外发光效果,难度不大,不再细讲。
3、偏移原图形一段距离绘制出来
这段比较简单了,只需要先把阴影画出来,然后再把原图像盖上去,但需要注意的是,阴影需要相对原图像偏移一段距离。完整代码如下:
public class ExtractAlphaView extends View {
private Paint mPaint;
private Bitmap mBitmap,mAlphaBmp;
public ExtractAlphaView(Context context) {
super(context);
init();
}
public ExtractAlphaView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ExtractAlphaView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init(){
setLayerType(LAYER_TYPE_SOFTWARE,null);
mPaint = new Paint();
mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.blog12);
mAlphaBmp = mBitmap.extractAlpha();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = 200;
int height = width * mAlphaBmp.getHeight()/mAlphaBmp.getWidth();
//绘制阴影
mPaint.setColor(Color.RED);
mPaint.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.NORMAL));
canvas.drawBitmap(mAlphaBmp,null,new Rect(10,10,width,height),mPaint);
mPaint.setColor(Color.GREEN);
canvas.drawBitmap(mAlphaBmp,null,new Rect(10,height+10,width,2*height),mPaint);
//绘制原图像
mPaint.setMaskFilter(null);
canvas.drawBitmap(mBitmap,null,new Rect(0,0,width,height),mPaint);
canvas.drawBitmap(mBitmap,null,new Rect(0,height,width,2*height),mPaint);
}
}
关键部分在onDraw函数中,先画出来阴影,然后再画出来原图像,需要注意的是在画原图像时,需要利用mPaint.setMaskFilter(null);将发光效果去掉。只有阴影需要发光效果,原图像是不需要发光效果的。另一点注意的是,阴影要偏移一点位置,这里是偏移了10个像素。
效果图如下:
二、封装控件
上面我们初步实现了图片的阴影效果,但这只是本篇内容的一小部分,最最重要的,如何将它封装成一个控件,具有如下功能:
- 让用户定义图片内容
- 让用户定义偏移距离
- 让用户定义阴影颜色和阴影模糊程度
- 可以使用wrap_content属性自适应大小
1、自定义控件属性
有关自定义控件属性,大家首先需要看下这篇文章《PullScrollView详解(一)——自定义控件属性》,在这篇文章中讲解了自定义控件属性的方法与提取方法。下面将会直接用到自定义属性的内容,下面涉及到的时候就自认为大家已经学会了自定义控件属性的方法了。
在这里,我们需要自定义四个属性,分别对应: 自定义图片内容、自定义偏移距离、自定义阴影颜色、自定义阴影模糊程度 这四个需求,所以我们先利用declare-styleable标签来定义这些属性
attr.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="BitmapShadowView">
<attr name="src" format="reference" />
<attr name="shadowDx" format="integer" />
<attr name="shadowDy" format="integer" />
<attr name="shadowColor" format="color"/>
<attr name="shadowRadius" format="float"/>
</declare-styleable>
</resources>
这里定义了五个xml属性,src来引用图片资源,仿照setShadowLayer另外定义shadowDx、shadowDy、shadowColor、shadowRadius来定义阴影的边距、颜色和模糊半径。
然后在布局中使用:(main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/com.harvic.BlogBitmapShadow"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<com.harvic.BlogBitmapShadow.BitmapShadowView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center_horizontal"
app:src="@drawable/blog12"
app:shadowDx="10"
app:shadowDy="10"
app:shadowRadius="10.0"
app:shadowColor="@android:color/holo_red_dark"/>
</LinearLayout>
在布局中使用很简单,直接定义控件所使用的图片资源、阴影相关参数就可以了,难度不大就不再讲了,下面我们来看如何在代码中中提取用户传入的这些属性。
BitmapShadowView中提取属性值并绘阴影
先列出完整代码,然后再细讲:
public class BitmapShadowView extends View {
private Paint mPaint;
private Bitmap mBmp,mShadowBmp;
private int mDx = 10,mDy = 10;
private float mRadius = 0;
private int mShadowColor;
public BitmapShadowView(Context context, AttributeSet attrs) throws Exception{
super(context, attrs);
init(context,attrs);
}
public BitmapShadowView(Context context, AttributeSet attrs, int defStyle) throws Exception{
super(context, attrs, defStyle);
init(context,attrs);
}
private void init(Context context,AttributeSet attrs) throws Exception {
setLayerType(LAYER_TYPE_SOFTWARE,null);
/**
* 提取属性定义
*/
TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.BitmapShadowView);
int BitmapID = typedArray.getResourceId(R.styleable.BitmapShadowView_src,-1);
if (BitmapID == -1){
throw new Exception("BitmapShadowView 需要定义Src属性,而且必须是图像");
}
mBmp = BitmapFactory.decodeResource(getResources(),BitmapID);
mDx = typedArray.getInt(R.styleable.BitmapShadowView_shadowDx,0);
mDy = typedArray.getInt(R.styleable.BitmapShadowView_shadowDy,0);
mRadius = typedArray.getFloat(R.styleable.BitmapShadowView_shadowRadius,0);
mShadowColor = typedArray.getColor(R.styleable.BitmapShadowView_shadowColor,Color.BLACK);
typedArray.recycle();
/**
* 其它定义
*/
mPaint = new Paint();
mShadowBmp = mBmp.extractAlpha();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth()-mDx;
int height = width * mBmp.getHeight()/mBmp.getWidth();
//绘制阴影
mPaint.setColor(mShadowColor);
mPaint.setMaskFilter(new BlurMaskFilter(mRadius, BlurMaskFilter.Blur.NORMAL));
canvas.drawBitmap(mShadowBmp,null,new Rect(mDx,mDy,width,height),mPaint);
//绘制原图像
mPaint.setMaskFilter(null);
canvas.drawBitmap(mBmp,null,new Rect(0,0,width,height),mPaint);
}
}
在这段代码中分两部分,首先根据属性来初始化各变量,然后再利用这些变量画出bitmap与阴影。
首先看初始化部分:
private void init(Context context,AttributeSet attrs) throws Exception {
setLayerType(LAYER_TYPE_SOFTWARE,null);
/**
* 提取属性定义
*/
TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.BitmapShadowView);
int BitmapID = typedArray.getResourceId(R.styleable.BitmapShadowView_src,-1);
if (BitmapID == -1){
throw new Exception("BitmapShadowView 需要定义Src属性,而且必须是图像");
}
mBmp = BitmapFactory.decodeResource(getResources(),BitmapID);
mDx = typedArray.getInt(R.styleable.BitmapShadowView_shadowDx,0);
mDy = typedArray.getInt(R.styleable.BitmapShadowView_shadowDy,0);
mRadius = typedArray.getFloat(R.styleable.BitmapShadowView_shadowRadius,0);
mShadowColor = typedArray.getColor(R.styleable.BitmapShadowView_shadowColor,Color.BLACK);
typedArray.recycle();
/**
* 其它定义
*/
mPaint = new Paint();
mShadowBmp = mBmp.extractAlpha();
}
初始化的时候,首先是利用TypedArray来初始化各项参数,由于我们是做图片的阴影,所以图片资源必须赋值,所以我们在提取图片资源时,对其添加容错:
int BitmapID = typedArray.getResourceId(R.styleable.BitmapShadowView_src,-1);
if (BitmapID == -1){
throw new Exception("BitmapShadowView 需要定义Src属性,而且必须是图像");
}
当提取失败时,抛出异常,终止程序,这样用户在写代码时就可以及时发现问题,而不必等上线以后才发现没有bitmap;
有关其它属性值的提取,这里就不再细讲了。
在提取完属性以后,就是定义画笔paint和根据源图像利用extractAlpha()来生成阴影图像;
在初始化以后就是利用这些属性来进行绘图了:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth()-mDx;
int height = width * mBmp.getHeight()/mBmp.getWidth();
//绘制阴影
mPaint.setColor(mShadowColor);
mPaint.setMaskFilter(new BlurMaskFilter(mRadius, BlurMaskFilter.Blur.NORMAL));
canvas.drawBitmap(mShadowBmp,null,new Rect(mDx,mDy,width,height),mPaint);
//绘制原图像
mPaint.setMaskFilter(null);
canvas.drawBitmap(mBmp,null,new Rect(0,0,width,height),mPaint);
}
首先,图片宽度与控件宽度操持一致(但需要把阴影的位置空出来),所以宽度为:int width = getWidth()-mDx
根据图片的宽高比换算出图片的高度:int height = width * mBmp.getHeight()/mBmp.getWidth()
我们依控件左上角(0,0)显示原图像,阴影在(mDx,mDy)处显示;
到这里自定义属性的定义与提取就结束了,最终效果图为:
从效果图中可以明显看出,明显给原图片添加了红色的阴影效果。
目前,我们初步实现了可以让用户自定义控件属性的功能,但我们在使用这个控件时,必须强制设置指定的宽高或者fill_parent来强制平屏,这样明显是不可取的,我们需要它能够让用户使用wrap_conetent时,自己计算宽高;
2、wrap_content自适应宽高
在自适应宽高时,需要了解onMeasure()、onLayout()与onDraw()的知识,以前在写FlowLayout时,已经单独写过一篇:《FlowLayout详解(一)——onMeasure()与onLayout()》,这里就不再细讲onMeasure()的原理了,如果不理解onMeasure用法的同学需要提前把这篇文章看完再回来;在第三篇中我还会重新讲解一遍onMeasure()、onLayout()与onDraw(),这里涉及内容不多,看完上一篇然后再理解以下内容应该不会有问题
在看完上面的文章,大家就应该知道,对于View控件的自适应宽高,只需要在上面的代码中重写onMeasure()方法就可以了:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int width = mBmp.getWidth();
int height = mBmp.getHeight();
setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height);
}
在onMeasure方法中,当用户指定属性是wrap_content时,就使用图片的宽高做为控件的宽高。
此时整个自定义控件的完整代码为:
public class BitmapShadowView extends View {
private Paint mPaint;
private Bitmap mBmp,mShadowBmp;
private int mDx = 10,mDy = 10;
private float mRadius = 0;
private int mShadowColor;
public BitmapShadowView(Context context, AttributeSet attrs) throws Exception{
super(context, attrs);
init(context,attrs);
}
public BitmapShadowView(Context context, AttributeSet attrs, int defStyle) throws Exception{
super(context, attrs, defStyle);
init(context,attrs);
}
private void init(Context context,AttributeSet attrs) throws Exception {
setLayerType(LAYER_TYPE_SOFTWARE,null);
/**
* 提取属性定义
*/
TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.BitmapShadowView);
int BitmapID = typedArray.getResourceId(R.styleable.BitmapShadowView_src,-1);
if (BitmapID == -1){
throw new Exception("BitmapShadowView 需要定义Src属性,而且必须是图像");
}
mBmp = BitmapFactory.decodeResource(getResources(),BitmapID);
mDx = typedArray.getInt(R.styleable.BitmapShadowView_shadowDx,0);
mDy = typedArray.getInt(R.styleable.BitmapShadowView_shadowDy,0);
mRadius = typedArray.getFloat(R.styleable.BitmapShadowView_shadowRadius,0);
mShadowColor = typedArray.getColor(R.styleable.BitmapShadowView_shadowColor,Color.BLACK);
typedArray.recycle();
/**
* 其它定义
*/
mPaint = new Paint();
mShadowBmp = mBmp.extractAlpha();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int width = mBmp.getWidth();
int height = mBmp.getHeight();
setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth()-mDx;
int height = width * mBmp.getHeight()/mBmp.getWidth();
//绘制阴影
mPaint.setColor(mShadowColor);
mPaint.setMaskFilter(new BlurMaskFilter(mRadius, BlurMaskFilter.Blur.NORMAL));
canvas.drawBitmap(mShadowBmp,null,new Rect(mDx,mDy,width,height),mPaint);
//绘制原图像
mPaint.setMaskFilter(null);
canvas.drawBitmap(mBmp,null,new Rect(0,0,width,height),mPaint);
}
}
所以当我们对这个自定义的控件使用如下布局(使用wrap_content):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/com.harvic.BlogBitmapShadow"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<com.harvic.BlogBitmapShadow.BitmapShadowView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
app:src="@drawable/blog12"
app:shadowDx="10"
app:shadowDy="10"
app:shadowRadius="10.0"
app:shadowColor="@android:color/holo_red_dark"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="test BTN"/>
</LinearLayout>
效果图如下:
所以,这时候如果我们需要产生灰色阴影,只需要把xml中的app:shadowColor的值改一下即可:(为了方便看阴影,我把Activiy背景改成了白色)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/com.harvic.BlogBitmapShadow"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@android:color/white">
<com.harvic.BlogBitmapShadow.BitmapShadowView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
app:src="@drawable/blog12"
app:shadowDx="10"
app:shadowDy="10"
app:shadowRadius="10.0"
app:shadowColor="@android:color/darker_gray"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="test BTN"/>
</LinearLayout>
效果图如下:
到这里,整个控件的封装就结束了,但细心的同学可以发现,BitmapShadowView的构造函数默认有三个,而我这里只写了两个具有AttributeSet attrs参数的,而下面这个构造函数却是没有实现的:
public BitmapShadowView(Context context){
super(context);
}
因为当从XML中生成控件时,都会调用具有AttributeSet attrs参数的方法,而从代码中生成控件则会调用上面仅具有context函数的方法,所以如果需要从代码中生成需要添加上这个方法,并且需要在代码中提供接口供外部设置各种属性才好,我这里就略去了这部分内容了,大家可以自己来填充这个控件,使其更完整。
好了,整篇文章到这里就结束了,源码在文章底部给出。
如果本文有帮到你,记得加关注哦
源码下载地址:http://download.csdn.net/detail/harvic880925/9573981
转载请标明出处,http://blog.csdn.net/harvic880925/article/details/51889104谢谢
如果你喜欢我的文章,那么你将会更喜欢我的微信公众号,将定期推送博主最新文章与收集干货分享给大家(一周一次)