第10章 CustomView Android画布

除了重写onDraw()、dispatchDraw()函数,还可以通过以下方法获得画布:

● 通过Bitmap创建。

● 通过SurfaceView的SurfaceHolder.lockCanvas()函数获取。

另外,通过创建Drawable对象,然后将画好的Drawable对象画在画布上,也是创建Bitmap的一种方式。

public abstract class Drawable
extends Object

java.lang.Object
   ↳android.graphics.drawable.Drawable
Known direct subclasses

AdaptiveIconDrawableAnimatedImageDrawableAnimatedVectorDrawableBitmapDrawable

ColorDrawableColorStateListDrawableDrawableContainerDrawableWrapperGradientDrawable

LayerDrawableNinePatchDrawablePictureDrawableShapeDrawableVectorDrawable

如上,Drawable类有很多的派生类。这些派生类都可以通过Drawable的draw(Canvas canvas)函数将其画到画布上。这里只以最常用的ShapeDrawable为例来进行讲解。

既然提到了ShapeDrawable,就不得不提及<shape>标签。shape标签可以实现的效果与ShapeDrawable类似,虽不及ShapeDrawable功能强大,但在shape标签的基础上理解ShapeDrawable的用法却非常容易。

这里需要注意,<shape>标签所对应的Java类是GradientDrawable,而不是ShapeDrawable。所以ShapeDrawable shapeDrawable = (ShapeDrawable) textView.getBackground();的代码来获取<shape>标签的实例,会强转出错。神奇的是,ShapeDrawable与GradientDrawable的用法基本一样,所以学会了ShapeDrawable后,也就知道GradientDrawable怎么用了。

一、ShapeDrawable

 shape标签与GradientDrawable:

shape标签所对应的类是GradientDrawable而不是ShapeDrawable,但是GradientDrawable并不能完成shape标签的所有功能,因为 GradientDrawable 的构造函数如下所示。

GradientDrawable()
GradientDrawable(GradientDrawable.Orientation orientation, int[] colors)
Create a new gradient drawable given an orientation and an array of colors for the gradient.

从构造函数中可以明显看出,GradientDrawable所对应的是gradient标签的功能,并不能完成shape标签所能完成的构造矩形、椭圆等功能;而神奇的是,通过ShapeDrawable却可以完成shape标签的所有功能!我们只需要知道在代码中得到shape标签实例的时候要强转
GradientDrawable就可以了。

获取shape标签的实例:

实现这样一个功能:在单击按钮时,给原有的<shape>标签添加圆角。

drawable/shape_solid.xml:(一个内部填充为红色并且带描边的矩形)

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#ff0000" />
    <stroke
        android:width="2dp"
        android:color="#00ff00"
        android:dashWidth="5dp"
        android:dashGap="5dp" />
</shape>

layout/shape_instance.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent">

    <Button
            android:id="@+id/add_shape_corner"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="添加圆角"/>

    <TextView
            android:id="@+id/shape_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Shape标签实例"
            android:padding="10dp"
            android:layout_margin="20dp"
            android:background="@drawable/shape_solid"
            />
</LinearLayout>
public class ShapeInstanceActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.shape_instance);

        final TextView tv = (TextView) findViewById(R.id.shape_tv);
        findViewById(R.id.add_shape_corner).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                GradientDrawable drawable = (GradientDrawable) tv.getBackground();
                drawable.setCornerRadius(20);
            }
        });
    }
}

ShapeDrawable的构造函数:

ShapeDrawable()
setShape(Shape s)

ShapeDrawable(Shape s)

public abstract class Shape
extends Object implements Cloneable

java.lang.Object
   ↳android.graphics.drawable.shapes.Shape
Known direct subclasses

PathShapeRectShape

Known indirect subclasses

ArcShapeOvalShapeRoundRectShape

abstract voiddraw(Canvas canvas, Paint paint)

Draws this shape into the provided Canvas, with the provided Paint.

 

如上,Shape类只是一个基类,其中的draw()函数是一个虚函数。每个子类可以根据不同的需求来绘出不同的图形。所以,在构造ShapeDrawable时,并不能直接传递Shape类型的对象,因为Shape对象里并没有实现draw()函数,而是需要传入已经实现draw()函数的Shape类的派生类。

每个派生类的具体含义如下:

● PathShape:构造一个可根据路径绘制的Shape。

● RectShape:构造一个矩形Shape。

● ArcShape:(extends RectShape)构造一个扇形Shape。

● OvalShape:(extends RectShape)构造一个椭圆Shape。

● RoundRectShape:(extends RectShape)构造一个圆角矩形Shape,可带有镂空矩形效果。

1.RectShape

1)RectShape实例

public class ShapeView extends View {
    private ShapeDrawable mShapeDrawable;

    public ShapeView(Context context) {
        super(context);
        init();
    }

    public ShapeView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public ShapeView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mShapeDrawable = new ShapeDrawable(new RectShape());
        mShapeDrawable.setBounds(new Rect(50, 50, 200, 100));
        mShapeDrawable.getPaint().setColor(Color.YELLOW);// 获取自带画笔,并设置画笔颜色
    }

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

(1)通过mShapeDrawable = new ShapeDrawable(new RectShapeO);生成一个ShapeDrawable实例,并且将这个ShapeDrawable实例的形状通过RectShape定义为矩形。即画出来的形状一定是矩形,而不是其他形状。
(2)通过mShapeDrawable.setBounds(new Rect(50,50,200,100));来指定ShapeDrawable在当前控件中的显示位置。这里的意思是mShapeDrawable会在ShapeView中(50,50,200,100)这两个点所定义的矩形区域显示。需要强调一点:这里的矩形位置是在当前控件中的位置,而不是全屏幕的位置。
(3)通过mShapeDrawable.getPaint().setColor(Color.YELLOW);拿到ShapeDrawable自带的画笔,并利用Paint.setColor(Color.YELLOW)将整个Drawable填充为黄色。

<com.example.customwidgets.ShapeView
        android:layout_width="250px"
        android:layout_height="150px"
        android:layout_margin="100px"
        android:background="@android:color/white"/>

注意,布局文件中单位用的是px以便和代码单位一致,便于观察。

     

当前控件是长250px,宽150px,白色的矩形区域。绘制的黄色矩形是以白色矩形为参照系的。

从效果图中可以看出:
● 矩形区域所在位置的显示,这就印证了我们的结论,即ShapeDrawable.setBounds()函数所设置的矩形位置是指所在控件中的位置,而不是以屏幕左上角点为坐标的。
● 通过mShapeDrawable.getPaint()函数得到ShapeDrawable自带的画笔,并对其进行操作,效果将直接显示在ShapeDrawable中。

2)Drawable的画布问题

我们调用mShapeDrawable.getPaint().setColor(Color.YELLOW);将画笔填充为黄色,那么ShapeDrawable的矩形区域的黄色是什么时候被填充上去的呢?

是在onDraw()函数中画上去的?

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    mShapeDrawable.draw(canvas);
}

        我们在这里虽然调用了mShapeDrawable .draw(canvas),但它的意思是将ShapeDrawable画到当前控件ShapeView上,并没有绘制 ShapeDrawable本身。
        其实,ShapeDrawable是自带画笔的,而这个画笔就是我们通过mShapeDrawable.getPaint()函数得到的。只要我们改变了Paint的内容,它就会立刻在ShapeDrawable中重画。而此时ShapeDrawable中的样式己经改变了。
        最后,我们在ShapeView的onDraw()函数中调用mShapeDrawable.draw(canvas);将重绘过的ShapeDrawable画到ShapeView上,就可以看到修改Paint以后ShapeDrawable的效果了。

2.OvalShape

mShapeDrawable = new ShapeDrawable(new OvalShape());
mShapeDrawable.setBounds(new Rect(50, 50, 200, 100));
mShapeDrawable.getPaint().setColor(Color.YELLOW);

(根据setBounds()函数所指定的矩形生成了对应的椭圆Shape)

3.ArcShape

ArcShape是在OvalShape所形成的椭圆的基础上,将其进行角度切割所形成的扇形。其中扇形开始的0°在椭圆的X轴正方向上。

public ArcShape(float startAngle, float sweepAngle)
● startAngle:开始角度,扇形开始的0°在椭圆X轴正方向上,即椭圆中心点向右正方向。
● sweepAngle:扇形所扫过的角度(顺时针扫)。
mShapeDrawable = new ShapeDrawable(new ArcShape(0, 300));
mShapeDrawable.setBounds(new Rect(50, 50, 200, 100));
mShapeDrawable.getPaint().setColor(Color.YELLOW);

4.RoundRectShape

如上图,RoundRectShape可以实现单纯的带圆角的矩形,也可以实现带有镂空的圆角矩形,而且中间的镂空矩形也可以带有圆角。

public RoundRectShape(float[] outerRadii, RectF inset, float[] innerRadii)
● outerRadii:外围矩形的各个角的角度大小,需要填充8个数字,每两个数字一组,
  分别对应(左上角,右上角,右下角,左下角)4个角的角度。
  每两个一组的数字构造一个椭圆,第一个数字代表椭圆X轴半径,第二个数字代表椭圆Y轴半径。
  例如:outerRaddi = new float[]{40,20, 12,12, 0,0, 0,0};
  则(40,20)表示左上角的角度,40表示所形成角度的椭圆X轴半径,20表示椭圆Y轴半径。
  如果不需要指定外围矩形的各个角度,则可以传入null。
● inset:表示内部矩形与外部矩形各边的边距。RectF的4个值left、top、right、bottom
  分别对应4条边的边距。如果不需要内部矩形的镂空效果,则可以传入null。
● innerRadii:含义同outerRaddi,只是它指定的是内围矩形。
float[] outerR = new float[] {12,12, 12,12, 0,0, 0,0};
RectF inset = new RectF(6, 6, 6, 6);
float[] innerR = new float[] {50,12, 0,0, 12,50, 0,0};
mShapeDrawable = new ShapeDrawable(new RoundRectShape(outerR, inset, innerR));
mShapeDrawable.setBounds(new Rect(50, 50, 200, 100));
mShapeDrawable.getPaint().setColor(Color.YELLOW);

5.PathShape

PathShape的含义是构造一个可根据路径绘制的Shape。

public PathShape(Path path, float stdWidth, float stdHeight)
● path:所要画的路径
● stdWidth:标准宽度,即将整个ShapeDrawable的宽度分成多少份。Path中的moveTo(x,y)、lineTo(x2,y2)
  这些函数中的数值在这里其实都是以每一份的位置来计算的。当ShapeDrawable动态变大、变小时,每一份会变小,
  而根据这些份的数值画出来的Path图形就会动态缩放。
● stdHeight:标准高度,即将ShapeDrawable的高度分成多少份。
Path path = new Path();
path.moveTo(0, 0);// 数字的单位是:份
path.lineTo(100, 0);// 数字的单位是:份
path.lineTo(100, 100);// 数字的单位是:份
path.lineTo(0, 100);// 数字的单位是:份
path.close();// 封闭前面所绘制的路径
mShapeDrawable = new ShapeDrawable(new PathShape(path, 100, 100));// 数字的单位是:份
mShapeDrawable.setBounds(new Rect(0, 0, 250, 150));
mShapeDrawable.getPaint().setColor(Color.YELLOW);

只修改上面代码中new ShapeDrawable()参数为new PathShape(path, 100, 200)后,填充效果就成了右边图。

6.自定义Shape

前面讲到,各个Shape派生类只不过实现了Shape中的draw()函数。

我们阅读一下PathShape的源码:

public class PathShape extends Shape {
    private Path mPath;
    private float mStdWidth;
    private float mStdHeight;

    private float mScaleX; // cached from onResize
    private float mScaleY; // cached from onResize

    public PathShape(Path path, float stdWidth, float stdHeight) {
        mPath = path;
        mStdWidth = stdWidth;
        mStdHeight = stdHeight;
    }

    @Override
    public void draw(Canvas canvas, Paint paint) {
        canvas.save();
        canvas.scale(mScaleX, mScaleY);
        canvas.drawPath(mPath, paint);
        canvas.restore();
    }

    @Override
    protected void onResize(float width, float height) {
        mScaleX = width / mStdWidth;
        mScaleY = height / mStdHeight;
    }
}

onResize()函数中根据当前尺寸动态调整每份值大小。

其实上面代码就是在创建PathShape的时候传入一个Path实例,然后在draw()函数中将其画出来而已。

仿照上面代码,我们自定义一个构造区域的Shape:

public class RegionShape extends Shape {
    private Region mRegion;
    public RegionShape(Region region) {
        asset(region != null);
        mRegion = region;
    }
    @Override
    public void draw(Canvas canvas, Paint paint) {
        RegionIterator it = new RegionIterator(mRegion);
        Rect r = new Rect();
        while (it.next(r)) {
            canvas.drawRect(r, paint);
        }
    }
}

代码很简单,就是在初始化时把要画的Region对象传进来,然后在draw()函数中将其画出来。

再来看一下Shape的使用方法,代码如下:

public class ShapeView extends View {
    private ShapeDrawable mShapeDrawable;
    // 省略ShapeView构造函数
    ...
    private void init() {
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        Rect rect1 = new Rect(50, 0, 90, 150);
        Rect rect2 = new Rect(0, 50, 250, 100);
        // 构造两个区域
        Region region = new Region(rect1);
        Region region2 = new Region(rect2);
        // 去除两个区域的交集
        region.op(region2, Region.Op.XOR);
        mShapeDrawable = new ShapeDrawable(new RegionShape(region));
        mShapeDrawable.setBounds(new Rect(0, 0, 250, 150));
        mShapeDrawable.getPaint().setColor(Color.YELLOW);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mShapeDrawable.draw(canvas);
    }
}

通过自定义Shape,我们可以调用Canvas的任何绘图方法,并将其绘制在ShapeDrawable中。也就是说,通过自定义Shape,我们可以在ShapeDrawable上随意绘制。但是我们一般不这么做,因为自定义Shape太过麻烦。其实,当我们需要使用ShapeDrawable无法完成的功能时,一般会通过自定义Drawable来实现。

常用函数:

来看看ShapeDrawable中的几个常用函数,有些函数是从Drawable父类中继承而来的。

1.setBounds()

它用来指定当前ShapeDrawable在当前控件中的显示位置。

setBounds(int left, int top, int right, int bottom)
setBounds(Rect bounds)

2.getPaint()

1)概述

ShapeDrawable是自带画笔的,只要通过ShapeDrawable.getPaint()函数得到ShapeDrawable的Paint对象,并对其进行操作,效果就会立刻显示在ShapeDrawable上。

这个看似简单的功能,其实是很可怕的。因为这就意味着我们可以调用Paint中的所有函数,比如setColor()、setPathEffect()、setShader()等。

在自定义Shape时,我们讲到,通过自定义Shape,可以调用Canvas的所有绘图方法。而这里又说,通过getPaint()函数可以调用Paint的所有方法。所以,ShapeDrawable可以调用Paint和Canvas的所有方法,实现绘图的所有功能。

有关Paint有一个需要注意的地方:我们在讲解Shader的时候提到,Shader是从当前画布左上角开始绘图的所以,当ShapeDrawable的Paint调用Shader时,Shader是从ShapeDrawable所在区域的左上角开始绘制的。

2)Paint.setShader()

public class ShapeShaderView extends View {
	private ShapeDrawable mShapeDrawable;
	public ShapeShaderView(Context context) {
		super(context);
		init();
	}
	public ShapeShaderView(Context context, AttributeSet attrs) {
		super(context, attrs);
		init();
	}
	public ShapeShaderView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		init();
	}
	private void init() {
		setLayerType(LAYER_TYPE_SOFTWARE, null);
		mShapeDrawable = new ShapeDrawable(new RectShape());
		mShapeDrawable.setBounds(new Rect(100, 100, 800, 800));
		Bitmap bitmap = BitmapFactory.decodeResource(getResources() ,R.drawable.avator);
		BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
		mShapeDrawable.getPaint().setShader(bitmapShader);
	}
	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		mShapeDrawable.draw(canvas);
	}
}

在初始化的时候创建了一个矩形的ShapeDrawable,然后给这个ShapeDrawable设置了一个BitmapShapder。注意,这个BitmapShapder 空白区域的填充方式是边缘填充。而mShapeDrawable则在ShapeShaderView控件的Rect(100,100,300,300)位置。

<com.harvic.ShapeShaderView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:layout_margin="50dp" />


从效果图中可以看出,头像是在ShapeShaderView控件的Rect(100,100,300,300)位置绘制的,并不是从ShapeShaderView的左上角开始绘制的,也不是从屏幕左上角开始绘制的。
这就证明了我们的结论:当给ShapeDrawable应用Shader时,Shader是从ShapeDrawable的左上角开始绘制的(setBounds()函数指定的区域)。这一点对于Shader非常重要!
3.其他函数

setAlpha(int alpha)
■ 设置透明度,取值0~255。
setColorFilter(ColorFilter colorFilter)
■ 设置ColorFilter。这是ShapDrawable自带的函数,也可以通过getPaint().setColorFilter()函数设置。
setIntrinsicHeight(int height)
■ 设置默认高度。当Drawable以setBackgroundDrawable()及setImageDrawable()方式使用时,
  会使用默认宽度和默认高度来计算当前Drawable的大小与位置。如果不设置,则默认的宽高都是-1px。
setIntrinsicWidth(int width)
■ 设置默认宽度。
setPadding(Rect padding)
■ 设置边距

4.放大镜效果

public class CustomView extends View {
    private Bitmap bitmap;
    private ShapeDrawable shppeDrawable;
    private static final int RADIUS = 80;// 放大镜的半径
    private static final int FACTOR = 3;// 放大倍数
    private final Matrix matrix = new Matrix();

    public CustomView(Context context) {
        super(context);
        init();
    }

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        setLayerType(LAYER_TYPE_SOFTWARE, null);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int x = (int) event.getX();
        final int y = (int) event.getY();
        // 这个位置表示的是,画shader的起始位置
        matrix.setTranslate(RADIUS - x * FACTOR, RADIUS - y * FACTOR);
        // 设置Shader的坐标变换矩阵,决定着色器的作用区域,注意它针对的x,y是针对3倍的图像
        // 假如触点(x,y)=(0,0),则Shader在屏幕上画图区域为(0,0,RADIUS,RADIUS),画像来源于3倍源图像
        shppeDrawable.getPaint().getShader().setLocalMatrix(matrix);
        // bounds,就是那个圆的外切矩形
        shppeDrawable.setBounds(x - RADIUS, y - RADIUS, x + RADIUS, y + RADIUS);
        invalidate();
        // return true表示完全拦截了所有的Touch事件,因为这里完全不需要使用View默认的处理行为。
        return true;
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (bitmap == null) {// 只在首次执行一次
            Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.scenery);
            bitmap = Bitmap.createScaledBitmap(bmp, getWidth(), getHeight(), false);
            // shader是三倍图像大小,所以它所对应的坐标与底图坐标要对应好
            BitmapShader shader = new BitmapShader(Bitmap.createScaledBitmap(bitmap,
                    bitmap.getWidth() * FACTOR, bitmap.getHeight() * FACTOR, true),
                    Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
            shppeDrawable= new ShapeDrawable(new OvalShape());
            shppeDrawable.getPaint().setShader(shader);
            shppeDrawable.setBounds(0, 0, RADIUS * 2, RADIUS * 2);
        }
        canvas.drawBitmap(bitmap, 0, 0, null);// 画布上画上全屏底图,每次invalide()都重画,因为是在代码里面设置的背景图
        shppeDrawable.draw(canvas);// 画布上画上shapeDrawabe
    }
}

Shader:着色器,即用它来绘制图像。就像Photoshop图章工具一样,只是设置了样式,与原画布没有任何关系,只是画时用此图章画而已。 

无论利用绘图函数绘制多大的图像、在哪里绘制,都与Shader无关!因为Shader总是从控件的左上角开始的。我们绘制的只是显示出来的部分而己。没有绘制的部分虽然已经生成,但是不会显示出来。

自定义Drawable:

前提提到过,在Drawable的子类(比如ShapeDrawable、GradientDrawable等)无法通过已有的函数完成指定的绘图功能时,一般会选择自定义Drawable。

这里通过自定义Drawable来实现圆角功能。

1.概述

public class CustomDrawable extends Drawable {
    @Override
    public void draw(Canvas canvas) {
    }
    @Override
    public void setAlpha(int alpha) {
    }
    @Override
    public void setColorFilter(ColorFilter cf) {
    }
    @Override
    public int getOpacity() {
        return 0;
    }
}

这4个函数是Drawable类里的虚函数,是必须实现的。

● draw()函数是我们将会用到的,与View类似,传入的参数是一个Canvas对象,我们只需要调用Canvas的一些方法,效果就会直接显示在Drawable上。
● setAlpha()和setColorFilter()函数是非常容易实现的。当外层调用CustomDrawable的这两个函数时,我们只需要将对应的参数传给CustomDrawable的Paint即可。
● getOpacity():当外部需要知道我们自定义的CustomDrawable的显示模式时会调用这个函数。它有4个取值:PixelFormat.UNKNOWN、TRANSLUCENT、TRANSPARENT、OPAQUE[opaque[oʊˈpeɪk]不透明的]。
其中,
PixelFormat.TRANSLUCENT表示当前CustomDrawable的绘图是具有Alpha通道的,即使用CustomDrawable后,其底部的图像仍有可能看得到;
PixelFormat.TRANSPARENT表示当前CustomDrawable是完全透明的,其中什么都没画,如果使用CustomDrawable,则将完全显示其底部图像;
PixelFormat.OPAQUE表示当前的CustomDrawable是完全没有Alpha通道的,使用CustomDrawable后,其底层的图像将被完全覆盖,而只显示CustomDrawable本身的图像;
PixelFormat.UNKNOWN表示未知。一般而言,如果我们不知道该如何返回,则直接返回PixelFormat.TRANSLUCENT是最靠谱的做法。

2.实现圆角Drawable

public class CustomDrawable extends Drawable {
    private Paint mPaint;
    private Bitmap mBitmap;
    private BitmapShader bitmapShader;
    private RectF mBound;

    public CustomDrawable(Bitmap bitmap) {
        mBitmap = bitmap
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
    }

    @Override
    public void draw(Canvas canvas) {
        canvas.drawRoundRect(mBound, 20, 20, mPaint);
    }

    @Override
    public void setAlpha(int alpha) {
        mPaint.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(ColorFilter cf) {
        mPaint.setColorFilter(cf);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    @Override
    public void setBounds(int left, int top, int right, int bottom) {
        super.setBounds(left, top, right, bottom);
        bitmapShader = new BitmapShader(Bitmap.createScaledBitmap(mBitmap, right-left, bottom-top, true), 
                                        Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        mPaint.setShader(bitmapShader);
        mBound = new RectF(left, top, right, bottom);
    }

    @Override
    public int getIntrinsicWidth() {
        return mBitmap.getWidth();
    }

    @Override
    public int getIntrinsicHeight() {
        return mBitmap.getHeight();
    }
}

有关setAlpha()和setColorFilter()函数没什么好讲的,就是把上层传入的参数设置给Paint;而关于getOpacity()函数,因为在构造圆角后,所显示的CustomDrawable是否具有透明度是由传入的Bitmap所决定的,如果Bitmap具有透明度,那么CustomDrawable也可能具有透明度。所以,在getOpacity()函数中,我们返回可能具有透明度的PixelFormat.TRANSLUCENT(其实这是标准返回方式,在不知道怎么返回时,一般可以返回这个标识)。
在这里,我们又多写了几个函数。
(1)getIntrinsicWidth()和getlntrinsicHeight():用于设置CustomDrawable的默认宽、高。这里设置图片的宽、高为默认宽、高。
(2)setBounds():它的含义就是给Drawable设定边界,即这块Drawable画布的大小。在setBounds()函数中,我们根据边界创建一个与Drawable相同大小的Bitmap作为Drawable的Shader。
在这里,在显示时,Bitmap会根据Drawable的大小动态拉伸,以完全覆盖这个Drawable。最后将边界保存起来,以便在绘图时使用。
(3)draw(Canvascanvas):我们知道,Shader始终是从画布的左上角开始平铺的,而canvas.drawXXX系列函数只用来指定哪部分显示出来。所以,我们只需在draw()函数中调用canvas.drawRoundRect()函数将BitmapShader以圆角矩形的方式显示出来即可。

3.Drawable的使用方法

Drawable一般有两种使用方法:一种是通过ImageView的setImageDrawable(drawable)函数将其设置为ImageView的源图片;另一种是通过View的setBackgroundDrawable(drawable)函数将其设置为背景。

1)setImageDrawable(drawable)函数

<ImageView
    android:id="@+id/img"
    android:layout_width="100dp"
    android:layout_height="50dp"
    android:background="#ffffff"
    android:scaleType="center" />

第一,我们把整个ImageView的背景设置为白色;第二,源图片的缩放方式为scaleType="center"。

Bitmap bitmap = BitmapFactory.decodeResource(getResource(), R.drawable.avator);
ImageView iv = (ImageView) findViewById(R.id.img);
CustomDrawable drawable = new CustomDrawable(bitmap);
iv.setImageDrawable(drawable);

在这里,我们使用setlmageDrawable(drawable)函数来设置数据源,这与在XML中给ImageView设置android:src="@drawable/xxx"效果是一样的。而源图片的显示大小是与ImageView的scaleType相关的,因为这里设置scaleType="center",所以ImageView必然会居中缩放源图片,然后将图片的显示位置通过setBounds()函数设置给CustomDrawable。

scaleType取不同值时的效果,如下:

程序运行流程:很明显,在除fitXY以外的模式下,ImageView会根据CustomDrawable的getlntrinsicWidth()和getlntrinsicHeight()函数中返回的默认宽、高来进行等比拉伸,以适配ImageView。在计算出CustomDrawable的显示位置以后,通过setBounds()函数传递给CustomDrawable显示。而对于fitXY模式,则将整个ImageView的区域通过setBounds()函数设置给CustomDrawable。

2)setBackgroundDrawable(drawable)函数

通过一个TextView举例,看一下自定义Drawable在使用setBackgroundDrawable()函数设置背景时,又是怎样计算setBounds()函数所需要的边界的。

<TextView
    android:id="@+id/tv"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="10dp"
    android:text="欢迎光临启舰的blog"
    android:textColor="#ff0000" />
Bitmap bitmap = BitmapFactory.decodeResource(getResource(), R.drawable.avator);
CustomDrawable drawable = new CustomDrawable(bitmap);
TextView iv = (TextView) findViewById(R.id.tv);
tv.setBackgroundDrawable(drawable);

从效果图中可以明显看出,宽度使用的是TextView的宽度,而高度则使用的是CustomDrawable的默认高度。
之所以会出现这样的效果,是因为在使用setBackgroundDrawable()函数设置自定义Drawable时,自定义Drawable的宽度和高度计算是将View的宽、高和自定义Drawable的宽、高进行对比,哪个值大就用哪个值作为控件的宽、高的。而这个最终值就会通过setBounds()函数传递给自定义Drawable。

解决这个问题很简单,只需在自定义Drawable时不重写getlntrinsicWidth()和getlntrinsicHeight()函数,即不指定自定义 Drawable的默认宽、高,让它返回默认的-1px。效果如下图所示。

总结:
● 当使用setImageDrawable(drawable)函数来设置ImageView数据源时,自定义Drawable的位置和大小与ImageView的scaleType有关。
● 当使用setBackgroundDrawable(drawable)函数来设置View背景时,自定义Drawable的宽、高与控件大小一致,控件的宽、高则选取本身宽、高和自定义Drawable宽、高中的最大值。

4.自定义Drawable与自定义View的区别

自定义Drawable中也有draw(Canvas canvas)函数,就以为自定义Drawable可以跟自定义View相互替换。事实并非如此!

虽然自定义Drawable类似于砍掉手势交互功能的自定义View,但自定义Drawable的使用场景很明确,要么使用在可以设置Drawable的函数中(比如setlmageDrawable()等),要么替代Bitmap用于View中(比如放大镜效果)。

而自定义View的功能十分强大,自定义Drawable和Bitmap无法完成的功能可以使用自定义View来完成。

自定义View与自定义Drawable根本不在一个维度,完全没有可比性。我们只需考虑在哪些情况下可以使用Drawable即可。

之前说过,自定义Drawable可以在自定义View中代替Bitmap使用,下面我们就来对比一下Drawable与Bitmap。

Drawable与Bitmap对比:

1.定义对比

Bitmap和Drawable在定义时就注定不是同一个东西。

Bitmap称作位图,一般位图的文件格式扩展名为.bmp,当然编码器也有很多,如RGB565、RGB8888。作为一种逐像素的显示对象,其执行效率高;但缺点也很明显,即存储效率低。我们将其理解为一种存储对象比较好。

Drawable作为Android下通用的图形对象,它可以装载常用格式的图像,比如GIF、PNG、JPG,当然也支持BMP,还提供了-些高级的可视化对象,比如渐变、图形等。

也就是说,Bitmap是Drawable,而Drawable不一定是Bitmap。我们这里虽然只着重讲了ShapeDrawable,但是所有的Drawable类型都是相通的。

2.指标对比

从上面的对比中可以看出,Drawable在占用内存和绘制速度这两个非常关键的点上胜过Bitmap,这也是在Android UI系统中普遍使用 Drawable的原因之一。

3.绘图的便利性对比

Drawable有很多派生类,通过这些派生类可以很容易地生成渐变、层叠等效果。单从这一方面而言,Drawable比Bitmap有优势。

但如果仅仅用作空白画布来绘图,那么Drawable构造和使用起来则不如Bitmap方便。

4.使用简易性对比

ShapeDrawable中是自带画笔的,只需要通过ShapeDrawable.getPaint()函数获取到画布的Paint对象,然后对其进行操作,其效果就会直接更新到ShapeDrawable上。这一原则是所有的Drawable通用的。因为getPaint()函数是从Drawable类中继承而来的,所以我们调用Paint的函数很方便。一般的Drawable子类使用Canvas的函数并不方便,所以Drawable的子类一般只用来完成它固有的功能。如果想要使用Drawable绘图,则建议自定义Drawable。

而如果想在Bitmap上作画,则一般使用类似如下的代码:

Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setColor(Color.RED);
canvas.drawCircle(0, 0, 100, paint);

从代码中可以看到,如果Bitmap想要作为画布,则需要通过Canvas canvas = new Canvas(bitmap);来创建Canvas对象,而通过生成的Canvas对象,所绘内容是直接画在Bitmap上的。而且画笔也是可以随意定义的。

所以,就使用简易性而言,Bitmap确实要比Drawable易用。

5.使用方式对比

Bitmap主要靠在View中通过Canvas.drawBitmap()函数画出来;而Drawable不仅能在View中通过Drawable.draw(Canvas canvas)函数画出来,也可以通过setlmageBackground()、setBackgroundDrawable()等设置Drawable资源的函数来设置。

总结:
• Bitmap在占用内存和绘制速度上不如Drawable有优势。
• Bitmap绘图方便;而Drawable调用Paint方便,但调用Canvas并不方便。
• Drawable有一些子类,可以方便地完成一些绘图功能,比如ShapeDrawable、GradientDrawable等。

那么,Drawable、Bitmap、自定义View在哪些情况下才会使用呢?

(1)Bitmap只在一种情况下使用,即在View中需要自己生成图像时,才会使用Bitmap绘图。绘图后的结果保存在这个Bitmap中,供自定义View使用。比如根据源Bitmap生成它的倒影,在使用Xfermode来融合倒转的图片原图与渐变的图片时,就需要根据图片大小生成一张同样大小的渐变图片,这时必须使用Bitmap。
(2)当使用Drawable的子类能完成一些固有功能时,优先选用Drawable。
(3)当需要使用setImageBackground()、setBackgroundDrawable()等可以直接设置Drawable资源的函数时,只能选用Drawable。
(4)当在自定义View中在指定位置显示图像功能时,既可以使用Drawable,也可以使用Bitmap。
(5)除Drawable和Bitmap以外的地方,都可以使用自定义View来实现。

由此可以看出,决定使用Drawable、Bitmap、自定义View的主要因素是用途。

二、Bitmap

Bitmap在绘图中是一个非常重要的概念,在我们熟知的Canvas中就保存着一个Bitmap对象,我们调用Canvas的各种绘图函数,最终还是绘制到其中的Bitmap上的。我们知道,在自定义View时,一般都会重写一个函数onDraw(Canvas canvas),在这个函数中是自带Canvas参数的,只需要将需要画的内容调用Canvas的函数画出来,就会直接显示在对应的View上。其实,真正的原因是,View对应着一个Bitmap,而onDraw()函数中的Canvas参数就是通过这个Bitmap创建出来的。有关Bitmap与Canvas、View、Drawable的关系,我们会在本节末尾讲解。我们先来看一下Bitmap的一般使用方法。

概述:

1.Bitmap在绘图中的使用

Bitmap在绘图中相关的使用主要有两种:第一种是转换为BitmapDrawable对象使用;第二种是当作画布来使用。

1)转换为BitmapDrawable对象使用

Bitmap bitmap = BitmapFactory.decodeResource(getResource(), R.drawable.pic);
BitmapDrawable bmpDraw = new BitmapDrawable(bitmap);
ImageView iv = (ImageView) findViewById(R.id.img);
iv.setImageDrawable(bmpDraw);

2)当作画布使用

方式一:使用默认画布

class CustomView extends View {
    ...
    public void onDraw(Canvas canvas) {
        ...
        RectF rect = new RectF(120, 10, 210, 100);
        canvas.drawRect(rect, paint);
    }
}

看似在onDraw(Canvas canvas)里的Canvas对象跟Bitmap并没有什么关系,其实 Canvas里保存的就是一个Bitmap,我们调用Canvas的各种绘图函数,最终都是画在这个Bitmap上的,而这个Bitmap就是默认画布。

方式二:自建画布

Bitmap bitmap = Bitmap.createBitmap(200, 100, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.BLACK);

在上面的代码中,我们先创建一个空白的Bitmap,然后再利用这个Bitmap创建一个Canvas对象,那么,调用Canvas的任何绘图函数最终都将画在这个Bitmap上。最后,我们可以将这个Bitmap保存到本地,也可以画到View上。

2.Bitmap格式

我们都知道Bitmap是位图,也就是由一个个像素点组成的。所以,它肯定涉及两个问题:第一,如何存储每个像素点;第二,相关的像素点之间是否能够压缩,这也就涉及压缩算法的问题。

1)如何存储每个像素

一张位图所占用的内存 = 图片长度(px) × 图片宽度(px) × 一个像素点占用的字节数。
在Android中,存储一个像素点所使用的字节数是用枚举类型Bitmap.Config中的各个参数来表示的,如下表所示。

Enum values

Bitmap.Config ALPHA_8

Each pixel is stored as a single translucency (alpha) channel. 

Bitmap.Config ARGB_8888

Each pixel is stored on 4 bytes. 

Bitmap.Config HARDWARE

Special configuration, when bitmap is stored only in graphic memory. 

Bitmap.Config RGBA_F16

Each pixels is stored on 8 bytes. 

Bitmap.Config RGB_565

Each pixel is stored on 2 bytes and only the RGB channels are encoded: red is stored with 5 bits of precision (32 possible values), green is stored with 6 bits of precision (64 possible values) and blue is stored with 5 bits of precision. 

• ALPHA_8:表示8位Alpha位图,即A=8,表示只存储Alpha位,不存储颜色值。一个像素点占用1字节,很明显,它没有颜色,只有透明度。
• ARGB_8888:表示32位ARGB位图,即A、R、G、B各占8位,一个像素点占8+8+8+8=32位,4字节。
• RGB_565:表示16位RGB位图,即R占5位,G占6位,B占5位,它没有透明度,一个像素点占5+6+5=16位,2字节。

注意:
(1)一般我们建议使用ARGB_8888格式来存储Bitmap。
(2)假如对图片没有透明度要求,则可以改成RGB_565格式,相比ARG_88888格式将节省一半的内存开销。

如何计算 Bitmap 所占的内存大小:

在讲解Bitmap所占内存大小之前,我们先明确一个概念:内存中存储的Bitmap对象与文件中存储的Bitmap图片不是-个概念。文件中存储的Bitmap图片是经过我们在后面讲到的压缩算法压缩过的;而内存中存储的Bitmap对象是通过BitmapFactory或者Bitmap的Create方法创建的,它保存在内存中,而且具有明确的宽和高。所以,很明显,内存中存储的一个Bitmap对象,它所占的内存大小=Bitmap的宽xBitmap的高×每像素所占内存大小。

很多开发人员一旦需要画布,就会创建一个全屏幕大小的Bitmap作为画布。我们现在就来算一下在一个分辨率是1024像素×768像素的屏幕上,创建一个与屏幕同样大小的Bitmap,到底需要多少内存?也就是说,这个屏幕长度上有1024个像素,宽度上有768个像素(格外注意,这里是像素,而不是dp)。我们假设每个像素使用ARGB_8888格式来存储,也就是一个像素占32位,那么要全屏显示这张图片所占的内存大小=1024×768×32B=25165824B=24MB。全屏显示-张图片要用24MB!而且更恐怖的是,有些人还会循环创建!这也就是在有些人自定义的控件中经常出现OOM的原因。所以,我们在创建画布时,应尽量根据需要的大小来创建。

2)Bitmap压缩格式

如果要将Bitmap存储在硬盘上,那么必然存在如何压缩图片的问题。在Android中,压缩格式使用枚举类Bitmap.CompressFormat 中的成员变量表示,如下表所示。

Enum values

Bitmap.CompressFormat JPEG

Compress to the JPEG format. 

Bitmap.CompressFormat PNG

Compress to the PNG format. 

Bitmap.CompressFormat WEBP_LOSSLESS

Compress to the WEBP lossless format. 

Bitmap.CompressFormat WEBP_LOSSY

Compress to the WEBP lossy format. 

创建Bitmap方法之一:BitmapFactory

public static Bitmap decodeResource(Resources res, int id)
public static Bitmap decodeResource(Resources res, int id, Options opts)
public static Bitmap decodeFile(String pathName)
public static Bitmap decodeFile(String pathName, Options opts)
public static Bitmap decodeByteArray(byte[] data, int offset, int length)
public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts)
public static Bitmap decodeFileDescriptor(FileDescriptor fd)
public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts)
public static Bitmap decodeStream(InputStream is)
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)
public static Bitmap decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, Options opts)

从这些函数中就可以看出,BitmapFactory的功能很强大,可以针对资源、文件、字节数组、FileDescriptor和InputStream数据流解析出对应的Bitmap对象,如果解析不出来,则返回null。而且每个函数都有两个实现,两个实现之间只差一个Optionsopts参数。

1.decodeResource(Resource res, int id)

Bitmap bmp = BitmapFactory.decodeResource(getResource(), R.drawable.ic_launcher);
ImageView iv = (ImageView) findViewById(R.id.img);
iv.setImageBitmap(bmp);

代码很简单,先从Drawable中拿到图片资源,然后通过BitmapFactory.decodeResource()函数解析为Bitmap,最后将Bitmap设置到ImageView中。

2.decodeFile(String pathName)

String fileName = "/data/data/demo.jpg";
Bitmap bm = BitmapFactory.decodeFile(fileName);
if (bm == null) {
    //TODO 文件不存在
}

Android文件存储系统:

3.decodeByteArray(byte[] data, int offset, int length)

参数:
• data:压缩图像数据的字节数组。
• offset:图像数据偏移量,用于解码器定位从哪里开始解析。
• length:字节数,从偏移量开始,指定取多少字节进行解析。

它的一般使用步骤如下。
(l)开启异步线程去获取网络图片。
(2)网络返回InputStream。
(3)把InputStream转换成byte[]。
(4)解析:Bitmap bm = BitmapFactory.decodeByteArray(myByte, 0, myByte.length);。

final ImageView iv = (ImageView) findViewByid(R.id.img);
new Thread(new Runnable() {
	@Override
	public void run(){
		try {
			byte[] data = getImage(path);
			int length = data.length;
			final Bitmap bitMap = BitmapFactory.decodeByteArray(data, 0, length);
			iv.post(new Runnable() {
				@Override
				public void run() {
					iv.setimageBitmap(bitMap);
				}
			});
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}).start();
—————————————————————————————————————————————————————————————
public static byte[] getlmage(String path) throws Exception {
	URL url = new URL(path) ;
	HttpURLConnection httpURLconnection = (HttpURLConnection) url.openConnection();
	httpURLconnection.setRequestMethod("GET");
	httpURLconnection.setReadTimeout(6 * 1000);
	InputStream in = null;
	if (httpURLconnection.getResponseCode() == 200) {
		in = httpURLconnection.getInputStream();
		byte[] result = readStream(in);
		in.close () ;
		return result ;
	}
	return null;
}

public static byte[] readStream(InputStream in) throws Exception {
	ByteArrayOutputStream outputStream =new ByteArrayOutputStream();
	byte[] buffer = new byte[l024];
	int len = -1;
	while ((len = in.read(buffer)) != -1) {
		outputStream.write(buffer , 0 , len);
	}
	outputStream.close();
	in.close() ;
	return outputStream.toByteArray();
}

这里有两点需要注意:第一,请求网络必须在子线程中:第二,在子线程中是不能更新UI的,所以,我们在这里使用View.post()函数来更新UI。

先将lnputStream的内容读到Byte数组中,然后再将Byte数组写到OutputStream中,最后outputStream.toByteArray()返回byte数组。

疑问:直接将InputStream的内容读到Byte数组中,返回这个Byte数组不可以吗?

为什么还要多此一举地将Byte数组写到OutputStream中,之后再通过outputStream.toByteArray()函数返回呢?这是因为BitmapFactory.decodeByteArray()函数所需的data字节数组并不是想象中的数组,而是把输入流转换为字节内存输出流的字节数组格式。如果不经过OutputStream转换,直接返回从InputStream中读取到的Byte数组,那么decodeByteArray()函数将一直返回null。

4.decodeFileDescriptor

1)概述

public static Bitmap decodeFileDescriptor(FileDescriptor fd)
public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts)
● fd:包含解码位图数据的文件路径
● outPadding:返回矩形的内边距。如果Bitmap没有被解析成功,则返回(-1, -1, -1, -1);
              如果不需要,可以传入null。这个参数一般不使用。
String path = "/data/data/demo.jpg";
FileInputStream is = new FileInputStream(path);
Bitmap bm = BitmapFactory.decodeFileDescriptor(is.getFD());
if (bm == null) {
    //TODO 文件不存在
}

上述代码根据FileDescriptor对象解析出对应的Bitmap,而FileDescriptor的一般获取方式是通过构造FileInputStream对象。有些读者可能会有疑问:构造FileInputStream对象需要的是文件路径,我们拿到文件路径后,为什么不直接使用BitmapFactory.decodeFile(path)来解析Bitmap呢?

确实是这样的,构造FileInputStream,要么使用文件路径,要么使用File对象,这两种方法都是可以直接拿到文件路径的,在拿到文件路径之后,通过BitmapFactory.decodeFile(path)来解析Bitmap确实更方便。但是,通过BitmapFactory.decodeFileDescriptor解析的方式比使用BitmapFactory.decodeFile(path)更节省内存

2)decodeFileDescriptor与decodeFile

我们从源码的角度来看一下为什么decodeFileDescriptor要比decodeFile更节省内存。

public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts) {
	Bitmap bm = nativeDecodeFileDescriptor(fd, outPadding, opts);
	if (bm == null && opts != null && opts.inBitmap != null) {
		throw new IllegalArgumentExceptioη("Problem decoding into existing bitmap");
	}
	return finishDecode(bm , outPadding , opts);
}

其中,nativeDecodeFileDescriptor()函数是AndroidNative里的函数,被封装在SO里。从这段代码中可以看出,decodeFileDescriptor()函数调用的是Native层的函数,直接由Native层解析出Bitmap返回。

public static Bitmap decodeFile(String pathName, Options opts) {
	Bitmap bm = null;
	InputStream stream = null;
	stream = new FileInputStream(pathName);
	bm = decodeStream(stream, null , opts);
	return bm;
}

可以看到,decodeFile()函数最终还是调用decodeStream()函数来解析Bitmap的。我们再来跟踪一下decodeStream()函数。

public static Bitmap decodeStream(InputStrean is, Rect outPadding, Options opts) {
	if (!is.markSupported()) {
		is = new BufferedinputStream(is, 16 * 1024);// 申请空间
	}
	is.mark(l024);
	Bitmap bm;
	byte[] tempStorage = null;
	if (opts != null) {
		tempStorage = opts.inTempStorage;
	}
	if (tempStorage ==null) {
		tempStorage =new byte[l6 * 1024);// 申请空间
	}
	bm = nativeDecodeStream(is, tempStorage, outPadding, opts);
	retum finishDecode(bm, outPadding, opts);
}

从这里可以看出,在最终调用nativeDecodeStream()函数之前,最多可能会申请两次空间。

所以说,decodeFileDescriptor要比decodeFile更节省内存。也就是说,decodeFile要比decodeFileDescriptor更容易导致OOM(OutOfMemeory,内存溢出)。

5.decodeStream

public static Bitmap decodeStream(InputStream is)
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)
final ImageView iv = (ImageView) findViewByid(R.id.img);
new Thread(new Runnable() {
	@Override
	public void run(){
		try {
			InputStream inputStream = getImage(path);
			final Bitmap bitMap = BitmapFactory.decodeStream(inputStream);
			iv.post(new Runnable() {
				@Override
				public void run() {
					iv.setimageBitmap(bitMap);
				}
			});
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}).start();
—————————————————————————————————————————————————————————————
public static byte[] getlmage(String path) throws Exception {
	URL url = new URL(path) ;
	HttpURLConnection httpURLconnection = (HttpURLConnection) url.openConnection();
	httpURLconnection.setRequestMethod("GET");
	httpURLconnection.setReadTimeout(6 * 1000);
	if (httpURLconnection.getResponseCode() == 200) {
		return httpURLconnection.getInputStream();
	}
	return null;
}

BitmapFactory.Options:

 

这个参数的作用非常大,它可以设置Bitmap的采样率,通过改变图片的宽度、高度、缩放比例等,以达到减少图片的像素的目的。总的来说,通过设置这个值,可以更好地控制、显示、使用Bitmap(位图)。我们在实际开发中可以灵活使用该值,以降低OOM的发生概率。

下面列出BitmapFactory.Options常用的部分成员变量。

public boolean inJustDecodeBounds;
public int inSampleSize;
public int inDensity;
public int inTargetDensity;
public int inScreenDensity;
public boolean inScaled;
public Bitmap.Config inPreferredConfig;
public int outWidth;
public int outHeight;
public String outMimeType;
◆ 以in开头的代表的就是设置某某参数
◆ 以out开头的代表的就是获取某某参数
比如,inSampleSize就是设置Bitmap的缩放比例;outWidth就是获取Bitmap的宽度。

1.inJustDecodeBounds

如果将这个字段设置为壮ue,则表示只解析图片信息,不获取图片,不分配内存。能获取的信息有图片的宽度、高度和图片的MIME类型。图片的宽度、高度通过options.outWidth(图片的原始宽度)和options.outHeight(图片的原始高度〉返回:图片的MIME类型通过options.outMimeType返回。

我们在压缩图片时,经常会用到这个字段。一般而言,当图片过大时,经常会造成OOM。所以,当图片的尺寸大于我们想要的尺寸时,我们就要进行压缩。这个问题的关键在于,如何不将图片加载到内存中,依然可以得知它的尺寸?而这个参数就可以很好地完成这个需求。我们只需将inJustDecodeBounds设置为true,而不需要将图片加载到内存中,就可以得知它的宽和高。然后跟我们想要的尺寸进行对比,如果大于我们想要的尺寸,就将这张图片压缩后显示。

Options options = new Options();
options.inJustDecodeBounds = true;
Bitmap bitmap = decodeResource(getResources(), R.drawable.ic_launcher, options);
Log.d("TAG ", "bitmap :" + bitmap );
Log.d("TAG", "realwidth:" + options.outWidth +" realheight:"+ options.outHeight + " mimeType:" + options.outMimeType);
D/TAG: bitmap :null
D/TAG: realwidth:72 realheight:72 mimeType:image/png

从结果中可以看出,返回的Bitmap是null,而获取到的width和height都是有值的。这就证明了我们的结论:inJustDecodeBounds只会解析Bitmap的宽/高度参数,而不会解析Bitmap,整个过程是不占内存的。

2.inSampleSize压缩图片

这个字段表示采样率。采样率的全称是采样频率,是指每隔多少个样本采样一次作为结果。比如,将这个字段设置为4,意思就是从原本图片的4个像素中取一个像素作为结果返回,其余的都被丢弃,这样,结果图片的宽和高都为原来的1/4。同样,如果将这个字段设置为16,意思就是从每16个像素中取一个像素返回,同样,宽和高都为原来的1/16。很明显,采样率越大,图片越小,同时图片越失真。

针对inSampleSize的值,官方建议取2的幕数,比如1、2、4、8、16等,否则会被系统向下取整并找到一个最接近的值。不能取小于l的值,否则系统将一直使用l来作为采样率。

所以,这个参数主要用来对图像进行压缩。那么问题来了:我们应该怎么确定一张图片的采样率呢?

比如,我们的ImageView的大小为100px×100px,要显示的图片大小为300px×400px,此时应该将inSampleSize设为多少呢?

通过计算可以得到图片宽度是ImageView的3倍,而图片高度是ImageView的4倍。那么,应该将图片宽高缩小为原来的1/4吗?假如我们把图片宽高缩小为原来的1/4,那么现在图片大小为75px×l00px,ImageView大小为100px×100px,图片要显示在ImageView中需要进行拉伸,而拉伸可能会导致图片更加失真。因为我们在使用采样率来压缩图片时,本来就是一种失真的压缩方法,所以就不要再更加失真了。我们应该把图片宽高变为原来的1/3,以保证它不小于ImageView的大小,这样尽管多占用一些内存,但不会造成图片质量的下降,还是很有必要的。通过以上分析,我们知道,在设置inSampleSize时应该注意使得缩放后的图片尺寸尽量大于等于相应的ImageView大小。

一般计算inSampleSize的步骤如下。

第一步,获取图片的原始宽高。通过将Options对象的inJustDecodeBounds属性设为true后调用decodeResource()函数,可以实现不真正加载图片而只获取图片的尺寸信息。代码如下:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), resid, options);

→ 现在原始宽高存储在Options对象的outWidth和outHeight实例域中

第二步,根据原始宽高和目标宽高计算出inSampleSize。代码如下:

// dstWidth和dstHeight分别为目标ImageView的宽和高
public static int calSampleSize(BitmapFactory.Options options, int dstWidth, int dstHeight) {
	int rawWidth = options.outWidth ;
	int rawHeight = options.outHeight ;
	int inSampleSize = l ;
	if (rawWidth > dstWidth || rawHeight > dstHeight) {
		float ratioHeight = (float) rawHeight / dstHeight ;
		float ratioWidth = (float) rawWidth / dstHeight;
		inSampleSize = (int) Math.min(ratioWidth, ratioHeight);
	}
	return inSampleSize;
}

拿原始宽高与目标宽高相比,然后取宽高比中的最小值,这个最小值就是采样率。

第三步,根据采样率解析出压缩后的Bitmap。代码如下:

BitmapFactory.Options options2 = new BitmapFactory.Options();
options2.inSampleSize = sampleSize;
try {
    Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.scenery, options2);
    iv.setImageBitmap(bmp);
} catch (OutOfMemoryError err) {
    //TODO OOM
}

3.加载一个Bitmap文件究竟要占多少空间

如果我们从资源文件或者本地文件中加载一个Bitmap文件需要多少内存?有些读者认为,Bitmap文件都是有明确尺寸的,用电脑上的图片浏览工具打开,就可以看到宽多少像素、高多少像素。直接利用公式“一张位图所占用的内存=图片高度(px)×图片宽度(px)×一个像素点占用的字节数”计算一下,不就知道这个Bitmap文件所占的内存了吗?真的有这么简单吗?虽然我们利用图片浏览工具可以明确地看到图片文件的宽和高的像素数,但是Android系统在加载时会根据需要动态缩放这张图片所占的像素数,也就是会动态缩放这张图片的尺寸。

在第一版Android系统出来时,Android的开发人员就己经考虑到以后的屏幕可能会有各种尺寸,所以预先准备了几个资源、文件夹来适配不同的屏幕分辨率,有drawable、drawalbe-ldpi、drawable-mdpi、drawable-hdpi、drawable-xhdpi和drawable-xxhdpi这6个文件夹。

● density:表示dpi与px的换算比例,1dpi=1 density px。

● densityDpi:表示在对应的分辨率下每英寸有多个个dpi。

从表格中可以看出,每个文件夹都对应一个屏幕分辨率,densityDpi表示屏幕上每英寸所对应的dpi数,而dpi再转换成px则需要乘以density,所以屏幕上l英寸长所对应的px数=每英寸的dpi数(densityDpi)×dpi所对应的px数(density)。

每个文件夹都是为了适配不同分辨率的屏幕的,把图片资源放到对应的文件夹中,意思就是,当屏幕是这个分辨率时,就会直接从这个文件夹中获取图片,无须再对图片进行处理。

但是,Android是可以定制的,这就注定它会有不止这些屏幕分辨率,当遇到跟这些文件夹不同分辨率的屏幕时该怎么办?

这就需要动态缩放图片了,缩放的比例就等于屏幕分辨率/文件所在文件夹的分辨率。这个公式很好理解,上面我们假定,屏幕分辨率与文件所在文件夹的分辨率相等,所以是不需要缩放的,也就是说缩放比例是1。而当屏幕分辨率跟文件所在文件夹的分辨率不同时,就需要缩放,缩放比例当然是屏幕分辨率/文件所在文件夹的分辨率。

比如,我们只出了一套图,放在xhdpi文件夹下,这个文件夹所对应的屏幕分辨率是480dpi,而当真实的屏幕分辨率是720dpi的时候,就需要放大这些图片,以适配这个屏幕,放大倍数就是720/480=1.5。也就是说,在xhdpi文件夹下原本大小是lOOpx×200px的一张图片,在显示的时候,被加载到内存中会被放大1.5倍,实际生成的Bitmap对象的尺寸是150px×300px。

那么又一个问题来了:原本宽高100px×200px是怎么被放大到150px×100px的呢?

因为宽度方向平白多出来50像素,而高度方向平白多出来100像素,这些像素要怎么填充呢?这又涉及Android的图片填充算法。我们采用类似setDither的抗抖动算法。比如,在宽度方向上,将原来的100像素平铺,多出来的空白利用相邻两个颜色生成“中间值”来过渡。正是由于被填充的中间值是通过相邻的两个颜色生成的,所以被放大的图像看起来很模糊。这也就是在只出一套图像来适配所有屏幕时,会导致在大屏幕上看起来很模糊的原因。这也就是在只出一套图像来适配所有屏幕时,会导致在大屏幕上看起来很模糊的原因。

看过放大,再来看缩小就很简单了。同样的道理,如果我们还是只出一套图放在xhdpi文件夹下,这个文件夹所对应的屏幕分辨率是480dpi,而当在小屏手机上显示时,比如屏幕分辨是300dpi,那么这张图片就得被缩小,比例是300/480=0.625。所以,原本放在对1dpi文件夹下宽高是100px×200px的图像,在加入内存生成Bitmap时,这个Bitmap的宽高就变成了62px×125px。缩小图片是很简单的,只需减少像素即可,依据inSampleSize采样算法间隔取点即可缩小图片。

如果我们加载的是本地SD卡中的图片怎么办?它并不在分辨率文件夹中。针对本地SD卡中的图片,Android的处理策略是:不进行缩放!原本的图片宽高是多少像素,生成的Bitmap还是多少像素。

下面举一个例子来看一下从分辨率文件夹中加载图片与从本地文件中加载图片的区别。

首先,准备一张图片,这张风景图在讲解放大镜效果时用过,在电脑上的图片浏览器看到,它的尺寸是640px×800px。然后将它放在xhdpi文件夹下,再复制一份放到手机SD卡根目录下,采用两种不同的方式读取,代码如下:

→ 从Drawable里读取是存在缩放的
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.scenery);
Log.d("TAG", "drawableBmp_width:" + bitmap.getWidth() + " height :" + bitmap.getHeight() 
      + " 内存:" + bitmap.getByteCount());

→ 直接从文件中读取是不存在缩放的,原本是多大尺寸,读进来就是多大尺寸
File file = Environment.getExternalStorageDirectory();
String path = file.getAbsolutePath() + "/scenery.png";
Bitmap bmp = BitmapFactory.decodeFile(path);
Log.d("TAG", "fileBmp_width:" + bmp.getWidth() + " height:" + bmp.getHeight() 
      + " 内存:" + bmp.getByteCount());
D/TAG: drawableBmp_width:2560 height :3200 内存:32768000
D/TAG: fileBmp_width:640 height:800 内存:2048000

从结果中可以看出,通过Drawable加载的图像,宽、高都被放大了;而通过本地文件加载的图像,宽、高都没有改变。这就印证了我们的观点:在Drawable文件夹中,会根据屏幕分辨率动态缩放图片大小;而通过文件加载的图像是不会被缩放的。有关所占内存的计算是很简单的,因为Bitmap默认使用ARGB_8888格式来存储,也就是每个像素占4字节,所以用Bitmap的宽×高×4就可以得到其所占的内存字节数。从Drawable读取的Bitmap所占内存=2560×3200×4=32768000通过文件加载的Bitmap所占的内存=640×800×4=2048000,与代码中得到的结果一致。

总结:
(1)不同名称的资源文件夹是为了适配不同的屏幕分辨率的,当屏幕分辨率与文件所在资源文件夹对应的分辨率相同时,直接使用图片,不会对图片进行缩放。
(2)当屏幕分辨率与图片所在文件夹所对应的分辨率不同时,会进行缩放,缩放比例是:屏幕分辨率/文件夹所对应的分辨率。
(3)当从本地文件中加载图片时,不会对图片进行缩放。

4.inScaled、inDensity、inTargetDensity、inScreenDensity

1)inScaled

我们知道,只有在一种情况下Bitmap图像才会被缩放:当图片所在资源文件夹所对应的屏幕分辨率与真实显示的屏幕分辨率不相同时。

而这个参数表示,在需要缩放时,是否对当前文件进行缩放。如果inScaled设置为false,则不进行缩放;如果inScaled设置为true或者不设置,则会根据文件夹分辨率和屏幕分辨率动态缩放。inScaled默认设置为true。

仍以原大小为640px×800px风景图来举例。我们把风景图放在xhdpi文件夹下,然后分别在inScaled参数不设置和设置为false的情况下来看生成的Bitmap对象的大小。

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R. drawable.scenery);
Log.d ("TAG", "drawableBmp_width :" + bitmap.getWidth() + " height :" + bitmap.getHeight() 
       + " 内存:" + bitmap.getByteCount()) ;
BitmapFactory.Options options= new BitmapFactory.Options();
options.inScaled = false ;
Bitmap noScaleBmp = BitmapFactory.decodeResource(getResources() , R. drawable.scenery , options) ;
Log.d ("TAG", "drawableBmp width :"+noScaleBmp.getWidth()+ " height :"+ noScaleBmp.getHeight()
       + " 内存:"+noScaleBmp.getByteCount()) ;
D/TAG: drawableBmp_width:2560 height :3200 内存:32768000
D/TAG: fileBmp_width:640 height:800内存:2048000

很明显,在没有设置inScaled参数时,图片被放大了;而在设置inScaled为false时,图片并没有被缩放,而是保持原本的大小。

2)inDensity、inTargetDensity、inScreenDensity

● inDensity:用于设置文件所在资源文件夹的屏幕分辨率。

● inTargetDensity:表示真实显示的屏幕分辨率。

● inScreenDensity:从字面意思它应该是真实的屏幕分辨率,但事实上,这个参数根本没什么用。

我们知道,一张图片的缩放比例是通过屏幕真实的分辨率/所在资源文件夹所对应的分辨率得出来的,在这里,也就是缩放比例:
scale = inTargetDensity/inDensity。

所以这两个参数的作用就是:可以通过手动设置文件所在资源文件夹的分辨率和真实显示的屏幕分辨率来指定图片的缩放比例。

仍以原大小为640px×800px的风景图来举例。我们将分别采用从Drawable和本地文件中加载的方式,通过手动设置inDensity和
mTargetDensity,将图片放大两倍。代码如下:

// 从 Drawable 中加载
BitmapFactory.Options options = new BitmapFactory.Options();
options.inDensity = 1;
options.inTargetDensity = 2;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.scenery, options);
Log.d("TAG", "drawableBmp_width:" + bitmap.getWidth() + " height: " + bitmap.getHeight()
       + " 内存:" + bitmap.getByteCount());
// 直接从本地文件中加载
File file = Environment.getExternalStorageDirectory();
String path = file.getAbsolutePath() + "/scenery.png";
Bitmap bmp = BitmapFactory.decodeFile(path, options);
Log.d("TAG", "fileBmp_width:" + bmp.getWidth() + " height: " + bmp.getHeight()
       + " 内存:" + bmp.getByteCount());
D/TAG: drawableBmp_width:1280 height: 1600 内存:8192000
D/TAG: fileBmp_width:1280 height: 1600 内存:8192000

我们需要注意的是,由于inDensity和inTargetDensity只是用来计算缩放比例的,所以,只要它们的比值是我们的缩放比例就可以了,不必在乎它们的值是不是真的是屏幕分辨率,比如这里分别设置inDensity=1、inTargetDensity=2都是没问题的。

5.inPreferredConfig

这个参数是用来设置像素的存储格式的。我们在讲Bitrnap.Config时提到,图片的像素存储格式有ALPHA_8、RGB_565、ARGB_8888,默认使用ARGB_8888。
下面举一个例子,分别使用默认的ARGB_888和设置为RGB_565,来看一下内存的占用情况。代码如下:

// ARGB_8888
Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.scenery);
Log.d("TAG", "ARGB888_width:" + bmp.getWidth() + " height:" + bmp.getHeight()
      + " 内存:" + bmp.getByteCount());

// RGB_565
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap= BitmapFactory.decodeResource(getResources(), R.drawable.scenery , options);
Log.d("TAG","RGB565_width:" + bitmap.getWidth() + " height:" + bitmap.getHeight()
      + " 内存:" + bitmap.getByteCount());
D/TAG: ARGB888_width:2560 height:3200 内存:32768000
D/TAG: RGB565_width:2560 height:3200 内存:16384000

可以看到RGB_565格式,相比ARG_88888格式将节省一半的内存开销。

要注意,在更改单个像素的存储格式以后,图片的宽高是不会改变的。这一点一定要搞清楚。

创建Bitmap方法之二:Bitmap静态方法

static Bitmap createBitmap(int width, int height, Bitmap.Config config)
static Bitmap createBitmap(Bitmap src)
static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height)
static Bitmap createBitmap(Bitmap source, i nt x, int y, int width, int height, Matrix m, boolean filter)
static Bitmap createBitmap(int[] colors, int width, int height, Bitmap.Config config)
static Bitmap createBitmap(int[] colors, int offset, int stride, int width, int height, Bitmap.Config config)
static Bitmap createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter)
// 在API 17中添加
static Bitmap createBitmap(DisplayMetrics display, int width, int height, Bitmap.Config config)
static Bitmap createBitmap(DisplayMetrics display, int[] colors, int width, int height, Bitmap.Config config)
static Bitmap createBitmap(DisplayMetrics display, int[] colors, int offset, int stride, int width, int height, Bitmap.Config config)

1.createBitmap(int width, int height, Bitmap.Config config)

这个函数可以创建一幅指定大小的空白图像。

● width、height用于指定所创建空白图像的尺寸,单位是px。

● config用于指定单个像素的存储格式,取值有ALPHA_8、RGB_565、ARGB_8888,默认取值为ARGB_8888。

public class CustomView extends View {
    private Bitmap mDestBmp;
    private Paint mPaint;

    public CustomView(Context context) {
        super(context);
        init();
    }

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        mPaint = new Paint();

        int width = 500;
        int height = 300;
        mDestBmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);

        Canvas canvas = new Canvas(mDestBmp);
        Paint paint = new Paint();
        LinearGradient linearGradient = new LinearGradient(width / 2, 0, width / 2, height, 0xff00ff00, 0x0000ff00, Shader.TileMode.CLAMP);
        paint.setShader(linearGradient);
        canvas.drawRect(0, 0, width, height, paint);
    }

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

        canvas.drawBitmap(mDestBmp, 0, 0, mPaint);

        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);
        canvas.drawRect(0, 0, mDestBmp.getWidth(), mDestBmp.getHeight(), mPaint);
    }
}

首先创建一个高宽500px、高300px的空白图像;然后利用新生成的mDestBmp生成一个Canvas对象,我们对这个Canvas对象的所有操作都会直接表现在mDestBmp上;接着利用LinearGradient生成一个从纯绿色(0xff00ff00)到纯透明(0x0000ff00)的色彩渐变;最后通过canvas.drawRect()函数将渐变画到mDestBmp上,此时的mDestBmp己经被画上了绿色透明渐变。

2.createBitmap(Bitmap src)

这个函数的作用很明显,就是根据一幅图像创建一份完全一样的Bitmap实例。

3.createBitmap(Bitmap source, int x, int y, int width, int height)

这个函数主要用于裁剪图像。

● source:用于裁剪的源图像

● x,y:开始裁剪的位置点坐标

● width,height:裁剪的宽度和高度

4.createBitmap(Bitmap source, int x, int y, int width, int height, Matrix matrix, boolean filter)

相比上一个函数,多了两个参数:Matrix matrix和boolean filter。它不仅能实现裁剪,还能给裁剪后的图像添加矩阵。

● matrix:给裁剪后的图像添加矩阵

● filter:对应paint.setFilterBitmap(filter),即是否给图像添加滤波效果。如果设置为true,则能够减少图像中由于噪声引起的突兀的孤立像素点或像素块。

将小狗图像裁剪后,宽度方向放大两倍。代码如下:

Matrix matrix= new Matrix();
matrix.setScale(2, 1);
Bitmap src = BitmapFactory.decodeResource (getResources(), R.drawable.dog);
Bitmap cutedBmp = Bitmap.createBitmap(src, src.getWidth()/3, src.getHeight()/3, src.getWidth()/3, src.getHeight()/3, 
                                      matrix, true);
ImageView iv = (ImageView ) findViewById(R.id.bmp_img2);
iv.setimageBitmap(cutedBmp);

(顶部一张是原图;中间一张是只裁剪,没有矩阵效果的;底部一张是既裁剪也X轴方向放大了2倍)

5.指定色彩创建图像

static Bitmap createBitmap(int[] colors, int width, int height, Bitmap.Config config)
static Bitmap createBitmap(int[] colors, int offset, int stride, int width, int height, Bitmap.Config config)

这两个函数基本不会用到,它们的意思是通过指定每个像素的颜色值来创建图像。由于需要指定每个像素的颜色值来创建Bitmap,难度过大,所以我们只简单看一下第一个构造函数的含义及用法。其参数如下。

● colors:当前图像所对应的每个像素的数组,数组长度必须大于width×height

● width,height:需要创建的图像的宽高

● config:指定每个像素的存储格式,取值ALPHA_8、RGB_565、ARGB_8888,默认值ARGB_8888

private int[] initColors(int width, int height) {
    int[] colors = new int[width * height];
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            int r = x * 255 / (width - 1);
            int g = y * 255 / (width - 1);
            int b = 255 - Math.min(r, g);
            int a = Math.max(r, g);
            colors[y * width + x] = Color.argb(a, r, g, b);
        }
    }
    return colors;
}

private void createBmpByColors() {
    int width = 300, height = 200;
    int[] colors = initColors(width, height);
    Bitmap bmp = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888);

    ImageView iv = (ImageView) findViewById(R.id.bmp_img);
    iv.setImageBitmap(bmp);
}

在生成ARGB值后,需要生成对应的颜色,并赋给colors数组里对应的变量。生成颜色很简单,只需调用Color.argb()函数即可。而难点在于,如何根据当前像素所在图片中的位置,找到它在colors数组中的位置。从图片的遍历方法上看,我们先遍历宽度,然后再遍历高度。所以,当一个像素在图片中的(x,y)位置时,它对应数组的位置下标就是[y×width + x],表示己经遍历了这么多完整的行,再加上本行己经遍历的数目x,即为当前像素在colors数组中的位置。

6.createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter)

该函数用于缩放Bitmap。

● src:需要缩放的源图像

● dstWidth,dstHeight:缩放后的目标宽高

● filter:是否给图像添加滤波效果,对应paint.setFilterBitmap(filter)

Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.scenery);
Bitmap bitmap = Bitmap.createScaledBitmap(src, 300, 200, true);

ImageView iv = (ImageView) findViewById(R.id.bmp_img);
iv.setImageBitmap(bitmap);

这里将风景图片缩放到宽为300px、高为200px。

到这里,有关创建Bitmap的方法介绍完毕,总结如下:

● 加载图像可以使用BitmapFactory和Bitmap的相关方法。
● 通过配置BitmapFactory.Options,可以实现缩放图片、获取图片信息、配置缩放比例等功能。
● 如果需要裁剪或者缩放图片,则只能使用Bitmap的Create系列函数。
● 一定要注意,在加载或创建Bitmap时,必须如下面代码所示,通过try...catch语句捕捉OutOfMemoryError,以防出现OOM问题。这里的示例代码限于篇幅就没有添加异常捕捉,大家在现实使用中一定要添加。

    try {
        Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.scenery);
        Bitmap bitmap = Bitmap.createScaledBitmap(src, 300, 200, true);
    } catch (OutOfMemoryError error) {
        error.printStackTrace();
    }

常用函数:

1.copy(Config config, boolean isMutable)

这个函数的含义是根据源图像创建一个副本,但可以指定副本的像素存储格式。

● config:像素在内存中的存储格式。取值有ALPHA_8、RGB_565、ARGB_8888

● isMutable:新创建的Bitmap是否可以更改其中的像素

 Mutable的本意是可变的、可更改的。前面讲解了很多种加载和创建图像的方法,但是,并不是每种方法加载出来的图像的像素都是可更改的。我们可以使用下面的方法来判断当前的Bitmap是不是像素可更改的。

boolean isMutable();

如果Bitmap的isMutable属性值是false,即像素不可更改的,而你仍要利用setPixel()等函数设置其中的像素值,就会报错。

那么问题来了:哪些方法加载的Bitmap是像素可更改的,而哪些方法加载的Bitmap是像素不可更改的呢?

答案是:通过BitmapFactory加载的Bitmap都是像素不可更改的,只有通过Bitmap中的几个函数创建的Bitmap才是像素可更改的。这些函数如下:

copy(Bitmap.Config config, boolean isMutable)
createBitmap(int width, int height, Bitmap.Config config)
createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter)
// 在API 17中引入
createBitmap(DisplayMetrics display, int width, int height, Bitmap.Config config)

其中,copy()函数可以根据源图像原样复制一份图像;而createBitmap()函数则用于生成一幅纯空白的图像;createScaledBitmap()函数则根据源图像进行缩放。

使用createScalec!Bitmap()函数时需要注意,当指定的目标缩放宽高与源图像宽高一样时,就会返回源图像,而不会生成新的图像。所以,如果源图像是像素不可更改的,那么返回的图像依然是像素不可更改的,此时必须实际进行缩放,才会生成新的图像,而新生成的图像是像素可更改的。

再重复一遍,在所有的Bitmap解析和创建方法中,只有这几个函数所返回的Bitmap是像素可更改的。大家一定要谨记!对于像素不可更改的图像,是不能作为画布的,比如下面的代码就会报错:

Bitmap srcBmp = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
Canvas canvas = new Canvas(srcBmp);
canvas.drawColor(Color.RED);

显然,srcBmp是像素不可更改的。然而,当其作为Canvas以后,如果要向其中填充颜色,则必然会改变它的像素值,肯定会报错。

2.extraAlpha()

这个函数的主要作用是从Bitmap中抽取出Alpha值,生成一幅只含有Alpha值的图像,像素存储格式是ALPHA_8。它有两个构造函数,下面分别讲解。

1)Bitmap extraAlpha()

下面示例,将源图像(从上到下是有透明渐变的)的透明通道抽取出来,并当成蓝色。

Bitmap srcBmp = BitmapFactory.decodeResource(getResources(), R. drawable.cat_dog);

Bitmap bitmap = Bitmap.createBitmap(srcBmp.getWidth(), srcBmp.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setColor(Color.CYAN);
canvas.drawBitmap(srcBmp.extractAlpha(), 0, 0, paint);

ImageView iv = (ImageView) findViewByid(R.id.img);
iv.setimageBitmap(bitmap);

srcBmp.recycle();

从效果图中可以看出,源图像的Alpha值并没有改变,而只改变了颜色部分。

2)extractAlpha(Paint paint, int[] offsetXY)

● paint:具有MaskFilter效果的Paint对象,一般使用BlurMaskFilter模糊效果。

● offsetXY:返回在添加BlurMaskFilter效果以后原点的偏移量。比如,我们使用一个半径为6的BlurMaskFilter效果,那么在源图像被模糊以后,图像的上、下、左、右4条边都会多出6px的模糊效果。所以,要想完全显示这幅图像,就不应该从源图像左上角(0,0)开始绘制,而应从(-6,-6)点开始绘制,而offsetXY就是相对源图像的建议绘制起始位置,所以此时offsetXY的值就是[-6,-6]。注意,offsetXY只是建议的绘制起始位置,其取值并不一定与BlurMaskFilter的模糊半径一致。当模糊半径比较大时,一般offsetXY的值会偏小。因为当模糊半径比较大的时候,边缘的效果就已经不明显了,所以起始位置就不必按照模糊半径来计算了。

Bitmap srcBmp = BitmapFactory.decodeResource(getResources(), R.drawable.cat_dog);
// 获取Alpha Bitmap
Paint alphaPaint = new Paint();
BlurMaskFilter blurMaskFilter = new BlurMaskFilter(20, BlurMaskFilter.Blur.NORMAL);
alphaPaint.setMaskFilter(blurMaskFilter);
int[] offsetXY = new int[2];
Bitmap alphaBmp = srcBmp.extractAlpha(alphaPaint, offsetXY);
Log.d("TAG", "offsetX:" + offsetXY[0] + " offsetY:" + offsetXY[1]);
// 创建Bitmap
Bitmap bitmap = Bitmap.createBitmap(alphaBmp.getWidth(), alphaBmp.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setColor(Color.CYAN);
canvas.drawBitmap(alphaBmp, 0, 0, paint);
// 绘制源图像
canvas.drawBitmap(srcBmp, -offsetXY[0], -offsetXY[1], null);
// 设置图像并回收没用的图像资源
ImageView iv =(ImageView) findViewById(R.id.img);
iv.setImageBitmap(bitmap);
srcBmp.recycle();
D/TAG: offsetX:-33 offsetY:-33

  注释掉绘制源图像一行代码效果是:

就是在模糊背景绘制出来以后,把源图像绘制上去。正因为我们的画布大小与Alpha图像大小相同,所以可以反推出,-offseXY[0],-offsetXY[l]所对应的坐标就是源图像原来所在的位置。

3)示例:单击描边效果

在这里实现的效果是根据图片的形状显示出对应的描边。

原理其实很简单,就是通过extractAlpha()函数生成一个纯色背景,然后在单击的时候,将生成的纯色背景作为ImageView的背景填充即可。

要实现这个效果,就会涉及如下两个问题:

(1)如何向ImageView添加单击事件?

(2)怎样才能让背景图像比要显示的源图像大,以显示描边?

针对第一个问题,如果能给ImageView动态添加一个selector标签,那么在单击的时候,用我们生成的图像作为背景即可。因为selector标签所对应的Java类是StateListDrawable,所以只需在lmgeView初始化的时候,将构造好的StateListDrawable实例通过ImageView.setBackgroundDrawable()函数设置给ImageView即可。

针对第二个问题,用过ImageView.setBackgroundDrawable()函数的读者应该都比较清楚,这个函数在设置背景的时候,所设置的背景会忽略源图像中的padding属性。所以,只要我们给源图像添加padding,而背景没有padding,背景图像自然比源图像要大。

首先,创建一个派生自ImageView的自定义控件。

public class StrokeImage extends ImageView {
    public StrokeImage(Context context) {
        super(context);
    }
    public StrokeImage(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public StrokeImage(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
    ...
}

其次,在onFinishInflate()函数中,向ImageView添加背景。

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    Paint p = new Paint();
    p.setColor(Color.CYAN);
    setStateDrawable(this, p);
}

setStateDrawable(ImageView v,Paint paint),它的主要作用就是实现向ImageView添加背景。这里着重要说的是设置ImageView的代码为什么添加在onFinishlnflate()函数中。onFinishlnflate()函数的调用时机是在系统将XML解析出对应的控件实例的时候。这时候,控件己经生成,但还没有被使用,所以,如果需要对控件进行一些基础设置,则是最佳时机。

private void setStateDrawable(AppCompatImageView v, Paint paint) {
    //拿到源图像
    BitmapDrawable bd = (BitmapDrawable) v.getDrawable();
    Bitmap srcBmp = bd.getBitmap();

    Bitmap bitmap = Bitmap.createBitmap(bd.getIntrinsicWidth(), bd.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    canvas.drawBitmap(srcBmp.extractAlpha(), 0, 0, paint);

    //添加state
    StateListDrawable sld = new StateListDrawable();
    sld.addState(new int[]{android.R.attr.state_pressed}, new BitmapDrawable(bitmap));

    //setBackgroundDrawable函数,会移除原有的padding值,如果需要padding,则需调用setPadding
    v.setBackgroundDrawable(sld);
}

注意:Bitmap.createBitmap(bd.getIntrisicWidth(), bd.getIntrisicHeight(), Config.ARGB_8888);

如果换成Bitmap.createBitmap(srcBmp.getWidth(), srcBmp.getHeight(), Config.ARGB_8888);会放大和位置偏移的问题。

Log.d("TAG", srcBmp.getWidth() + "," + srcBmp.getHeight() + " # " 
             + bd.getIntrinsicWidth() + "," + bd.getIntrinsicHeight());
————————————————————————————————————————————————————————————————————————
375,478 # 984,1255

给在XML里给ImageView设置一张图片: android:src="@drawable/abc"

android:src="@drawable/abc"
———————————————————————————
int intrinsicWidth = mImageView.getDrawable().getIntrinsicWidth();
int intrinsicHeight = mImageView.getDrawable().getIntrinsicHeight();
打印出来的结果发现大于图片的实际宽高?
原来我的测试设备是1080p,对应的图片资源文件应该是drawable-xxhdpi,但是这张图片位于drawable-xhdpi目录下:

You said the drawable is from your /res folder. Which folder is it in?
/res/drawable
/res/drawable-mdpi
/res/drawable-hdpi
etc..
And what is the density of the device you are testing on? Is it a Nexus S with general density of 240dpi? Because if your source drawable is located in the drawable-mdpi folder and you are testing on a 240dpi device, then the Android system will automatically scale the drawable up by a factor of 1.5 so that the physical size will be consistent with the baseline device density at 160dpi.
When you call getIntrinsicWidth() what is returned is the size the drawable wants to be after Android scales the drawable. You'll notice that 2880 = 1920 * 1.5.
这里主要讲解StateListDrawable的使用。


StateListDrawable sld =new StateListDrawable() ;
sld.addState(new int[] {android R.attr.state pressed}, new BitmapDrawable(bitmap));

addState()函数是它最基本的一个函数,用于向其中添加状态和对应的Drawable资源。它的函数声明如下:

public void addState(int[] stateSet, Drawable drawable)
● stateSet:填写对应的状态数组
● drawable:这些状态所对应的资源

比如现在这个<selector>标签的<item>:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true"
          android:state_checked="true"
          android:drawable="@drawable/dog"/>
    ...
</selector>

它所对应的addState()函数的写法如下:

StateListDrawable sld = new StateListDrawable();
int[] states = {android.R.attr.state_pressed, android.R.attr.state_checked};
sld.addState(states, new BitmapDrawable(bitmap));
<com.example.customwidgets.Strokeimage
    android:layout_width="wrap_content"
    android:layout_height="wrap content"
    android:src=" @drawable/cat"
    android:scaleType="fitCenter"
    android:padding="3dp"
    android:clickable="true" />

下面实现一个extends ViewGroup的FixedGridLayout:

public class FixedGridLayout extends ViewGroup {
    int mCellWidth;
    int mCellHeight;

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

    public FixedGridLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        // Read the resource attributes.
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FixedGridLayout);
        mCellWidth = a.getDimensionPixelSize(R.styleable.FixedGridLayout_cellWidth, -1);
        mCellHeight = a.getDimensionPixelSize(R.styleable.FixedGridLayout_cellHeight, -1);
        a.recycle();
    }

    public void setCellWidth(int px) {
        mCellWidth = px;
        requestLayout();
    }

    public void setCellHeight(int px) {
        mCellHeight = px;
        requestLayout();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int cellWidthSpec = MeasureSpec.makeMeasureSpec(mCellWidth, MeasureSpec.AT_MOST);
        int cellHeightSpec = MeasureSpec.makeMeasureSpec(mCellHeight, MeasureSpec.AT_MOST);

        int count = getChildCount();
        for (int index=0; index<count; index++) {
            final View child = getChildAt(index);
            child.measure(cellWidthSpec, cellHeightSpec);
        }
        // Use the size our parents gave us
        setMeasuredDimension(resolveSize(mCellWidth*count, widthMeasureSpec),
                             resolveSize(mCellHeight*count, heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int cellWidth = mCellWidth;
        int cellHeight = mCellHeight;
        int columns = (r - l) / cellWidth;
        if (columns < 0) {
            columns = 1;
        }
        int x = 0;
        int y = 0;
        int i = 0;
        int count = getChildCount();
        for (int index=0; index<count; index++) {
            final View child = getChildAt(index);

            int w = child.getMeasuredWidth();
            int h = child.getMeasuredHeight();

            int left = x + ((cellWidth-w)/2);
            int top = y + ((cellHeight-h)/2);

            child.layout(left, top, left+w, top+h);
            if (i >= (columns-1)) {
                // advance to next row
                i = 0;
                x = 0;
                y += cellHeight;
            } else {
                i++;
                x += cellWidth;
            }
        }
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        int cnt = getChildCount();
        Paint p = new Paint();
        p.setColor(Color.CYAN);

        for (int i=0; i<cnt; i++) {
            View v = getChildAt(i);
            setStateDrawable((ImageView)v, p);
        }
    }

    private void setStateDrawable(ImageView v, Paint p) {
        BitmapDrawable bd = (BitmapDrawable) v.getDrawable();
        Bitmap b = bd.getBitmap();
        Bitmap bitmap = Bitmap.createBitmap(bd.getIntrinsicWidth(), bd.getIntrinsicHeight(), Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        canvas.drawBitmap(b.extractAlpha(), 0, 0, p);

        StateListDrawable sld = new StateListDrawable();
        sld.addState(new int[]{android.R.attr.state_pressed}, new BitmapDrawable(bitmap));

        v.setBackgroundDrawable(sld);
    }
<?xml version="1.0" encoding="utf-8"?>
<com.example.customwidgets.FixedGridLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/grid"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    app:cellHeight="100dp"
    app:cellWidth="120dp">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:padding="3px"
        android:scaleType="fitCenter"
        android:src="@drawable/gimp" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:padding="3px"
        android:src="@drawable/looknfeel" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:padding="3px"
        android:src="@drawable/penguin" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:padding="3px"
        android:src="@drawable/pinguimroot3" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:padding="3px"
        android:src="@drawable/pinguimuser" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:padding="3px"
        android:src="@drawable/pinguimuser2" />
</com.example.customwidgets.FixedGridLayout>

3.分配空间获取

获取Bitmap的分配空间有三个函数。

int getAllocationByteCount()
● 获取Bitmap所分配的内存。此函数在API 19引入。
int getByteCount()
● 获取Bitmap所分配的内存。此函数在API 12引入。
int getRowBytes()
● 获取每行所分配的内存大小。Bitmap所占内存=getRowBytes()×bitmap.getHeight()。
public int getBitmapSize(Bitmap bitmap) {
    // API 19
    if (Build.VERSION.SOK INT >= Build.VERSION_CODES.KITKAT) {
	    return bitmap.getAllocationByteCount();
    }
    // API 12
    if (Build.VERSION.SOK INT >= Build.VERSION_CODES.HONEYCOMB_MRl) {
	    return bitmap.getByteCount();
    }
    // 更早版本
    return bitmap.getRowBytes() * bitmap.getHeight();
}

使用这三种方法获取Bitmap对象所占的内存大小是一致的。

4.recycle()、isRecycled()

public void recycle()
● 强制回收Bitmap所占的内存。
public final boolean isRecycled()
● 判断当前Bitmap的内存是否被回收。

如果要回收内存,则代码一般这样写:

if (bmp != null && !bmp.isRecycled()) {
    bmp.recycle(); // 回收图片所占的内存
    bitmap = null;
    system.gc(); // 提醒系统及时回收内存
}

注意一:使用内存已经被回收的Bitmap引起Crash。

java.lang.RuntimeException : Canvas: trying to use a recycled bitmap android.graphics.Bitmap@44c093b8

注意二:是否应该使用recycle()函数主动回收内存?

在Android2.3.3之前,Bitmap的像素级数据(Pixel Data)被存放在Native内存空间中。这些数据与Bitmap本身是隔离的,Bitmap本身被存放在Dalvik堆中。我们无法预测在Native内存中的像素级数据何时会被释放,这就意味着程序容易超过它的内存限制并且崩渍。而自Android3.0开始,像素级数据与Bitmap本身一起被存放在Dalvik堆中,可以通过Java回收机制自动回收。

所以,在Android2.3.3及更低版本中,推荐使用recycle()函数。因为Native级数据如果不主动释放,则将不会被释放,从而造成内存泄漏,最终导致OOM的发生。而在Android3.0以后,可以不再调用recycle()函数来主动释放内存,让Java虚拟机自动在GCC内存回收)时回收即可。如果你有很多Bitmap不去手动释放而等待系统GC的时候去释放,那么你的应用程序在GC的时候会变得非常卡顿,这样体验不好。但是,如果手动释放内存,则很可能会出现上面讲解的使用内存己经被回收的Bitmap时引起Crash。所以,如果你的技术不到位,则建议在Android3.0以上版本中不要使用recycle()函数。

综上,得出结论:在Android3.0及以前的版本中,必须强制调用recycle()函数来释放内存:从Android3.0开始,不再强制调用recycle()函数来释放内存。

5.setDensity()、getDensity()

在BitmapFactory中,我们讲过几个Density值,如inDensity、inTargetDensity,而这里Bitmap的setDensity()、getDensity()函数所对应的就是inDensity。
inDensity用于表示该Bitmap适合的屏幕dpi。当目标屏幕的dpi(inTargetDensity)不等于它时,将会缩放图像以适应目标机器。

public void setDensity(int density)
● density:对应inDensity,用于设置图像建议的屏幕尺寸。
  inDensity:用于设置文件所在资源文件夹的屏幕分辨率。
public int getDensity()
● 获取Bitmap的inDensity值。

举一个例子,先获取Bitmap的原始Density, 然后将Density放大两倍,这样在显示屏幕分辨率不变的情况下,显示出来的图片就应该缩小一半。代码如下:

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.cat_dog);
ImageView ivl = (ImageView)findViewById(R.id.imgl);
ivl.setImageBitmap(bitmap);
int density = bitmap.getDensity();
Log.d("TAG", "density:" + density + " width:" + bitmap.getWidth() + " height:" + bitmap.getHeight());
int scaledDensity = density * 2;
bitmap.setDensity(scaledDensity);
Log.d("TAG", "density:" + bitmap.getDensity() + " width:" + bitmap.getWidth() + " height:" + bitmap.getHeight());
ImageView iv2 = (ImageView) findViewByid(R.id.img2);
iv2.setImageBitmap(bitmap);
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <ImageView
        android:id="@+id/imgl"
        android:scaleType="center"
        android:layout_width="wrap_content"
        android:layoutheight="wrap_content"/>
    <ImageView
        android:id="@+id/img2"
        android:scaleType="center"
        android:layout_width="wrap_content"
        android:layoutheight="wrap_content"/>
</LinearLayout>
D/TAG: density:480 width:563 height:717
D/TAG: density:960 width:563 height:717

从效果图中可以看出,在将Bitmap的Density增大两倍以后,显示出来的图像就明显缩小了一半。但是从日志中可以看到,在增大Density可以后,Bitmap在内存中的尺寸是没有变化的,所以这种设置BitmapDensity的方式只会影响显示缩放,而不会改变Bitmap本身在内存中的大小。而在设置BitmapFactory中的Density选项后,图片在被加载到内存中时,就已经被放大/缩小了。这是Bitmap的Density设置与BitmapFactory的Density设置的区别。

另外,在利用setDensity设置Bitmap的对应文件夹屏幕分辨率之后,如果想要使用ImageView显示,则ImageView的scaleType属性必须设置为center,而且layout_width和layout_height属性必须设置为wrap_content,才能完整显示经过setDensity设置后,在屏幕中被缩放后的图像。因为如果设置成其他属性,就会根据屏幕大小进行缩放。只有当scaleType="center"时,才会原样显示图片原有大小。

如果将setDensity缩放后的Bitmap通过setBackgroundDrawable(drawable)设置为背景,则同样是看不到源图像大小的,因为在作为背景显示时,背景会自动缩放到控件大小。

6.setPixel()、getPixel()

这两个函数用于针对Bitmap中某个位置的像素进行设置和获取。

public void setPixel(int x, int y, int color)
● x,y:像素级坐标
● color:要设置的颜色值
public int getPixel(int x, int y)
● 获取指定位置(x,y)处像素的颜色值

举一个例子,图片中绿色值较多,只将绿色通道增大30。 

Bitmap srcBmp = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
ImageView ivl = (ImageView) findViewById(R.id.imgl);
ivl.setImageBitmap(srcBmp);
Bitmap desBmp = srcBmp.copy(Bitmap.Config.ARGB_8888, true);// isMutable=true
for(int h=0; h<srcBmp.getHeight(); h++) {
	for(int w=0; w<srcBmp.getWidth(); w++) {
		int originColor = srcBmp.getPixel(w, h);
		int red = Color.red(originColor);
		int alpha = Color.alpha(originColor);
		int green = Color.green(originColor);
		int blue = Color.blue(originColor);
		if (green<200) {
			green += 30;
		}
		desBmp.setPixel(w, h, Color.argb(alpha, red, green, blue));
	}
}
ImageView iv2 = (ImageView) findViewById(R.id.img2);
iv2.setImageBitmap(desBmp);

(1) 通过srcBmp.copy(Bitmap.Config.ARGB_8888, true)函数生成一个像素可更改的Bitmap。前面我们讲过,通过Drawable获取的图像是像素不可更改的。
(2) 遍历图像中的每个像素。先通过srcBmp.getPixel(w, h)函数拿到当前位置像素的原始颜色值,然后通过Color的系列方法得到A、R、G、B各通道的值,最后只针对性地当绿色通道小于200时添加30。这是因为颜色值最大是255,如果超过255,则会用255取余,作为最终的通道颜色值。比如,如果得到的值是280,那么实际的颜色值是280%255=25。我们的目的是增强绿色的显示,当然不能让绿色值变小,所以必须添加一次判断,以使增加过的绿色通道值不超过255。
(3) 通过setPixel()函数将更改过的颜色值重新设置进去。

7.compress

1)概述

public boolean compress(CompressFormat format, int quality, OutputStream stream)
● CompressFormat format:Bitmap的压缩格式。支持CompressFormat.JPEG、CompressFormat.PNG、CompressFormat.WEBP_LOSSLESS(API 30)、CompressFormat.WEBP_LOSSY(API30)四种格式。API30之前只有三种格式,最后一种是CompressFormat.WEBP(API14)
● quality:压缩后图像的画质,取值0~100。对于PNG等无损格式的图片,会忽略此项设置。
● OutputStream stream:这是输出值,Bitmap在被压缩后,会以OutputStream的形式在这里输出。
● 返回值boolean:压缩成功,返回true;失败返回false。

2)压缩格式

(1) CompressFormat.JPEG:采用JPEG压缩算法,是一种有损压缩格式,即在压缩过程中会改变图像的原本质量。compress()函数中的quality参数值越小,画质越差,对图片的原有质量损伤越大,但是得到的图片文件比较小。而且,JPEG不支持Alpha透明度,当遇到透明度像素时,会以黑色背景填充。
(2) CompressFormat.PNG:采用PNG压缩算法,是一种支持透明度的无损压缩格式。
(3) CompressFormat.WEBP:WEBP是-种同时提供了有损压缩与无损压缩的图片文件格式,派生自视频编码格式VP8,Google于2010年发布;从Android4.0(API14)开始支持WEBP,从Android4.2.l+(API18)开始支持无损WEBP和带Alpha通道的WEBP。也就是说,在14运API~l7时,WEBP是一种有损压缩格式,而且不支持透明度。在API18以后,WEBP是一种无损压缩格式,而且支持透明度。在有损压缩时,在质量相同的情况下,WEBP格式图像的体积要比JPEG格式图像的体积小40%,美中不足的是,WEBP格式图像的编码时间比JPEG格式图像的编码时间长8倍。在无损压缩时,无损的WEBP图片比PNG图片小26%,但WEBP格式的压缩时间是PNG格式的压缩时间的5倍。所以,从整体来讲,WEBP格式是通过牺牲压缩时间来减小产出文件大小的。

3)compress压缩图像(使用JPEG格式进行压缩)

ImageView iv_l = (ImageView) findViewById(R.id.imgl);
ImageView iv_2 = (ImageView) findViewById(R.id.img2);
Bitmap bmp = BitmapFactory.decodeResource(this.getResources(), R.drawable.cat);
iv_l.setImageBitmap(bmp);
// 压缩图像后,显示
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bmp.compress(Bitmap.CompressFormat.JPEG, 1, bos);
byte[] bytes = bos.toByteArray();
Bitmap bmpl = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
iv_2.setImageBitmap(bmpl);

从效果图中明显可以看出,压缩后的JPEG图像质量很差,己经具有明显的颜色块了。源图像从上到下是具有Alpha渐变的;而在生成JPEG图像时,完全以黑色背景显示整幅图像,而且完全没有Alpha效果。前面我们已经讲过,JPEG压缩算法是有损压缩,不支持Alpha通道,当遇到透明度像素时,会以黑色背景填充。

CompressFormat.PNG、CompressFormat.WEBP

    

从图像中也可以明显看出,PNG图像是无损压缩的,它压缩后的图像跟源图像一模一样;而JPEG和WEBP图像是有损压缩的,而且都不支持透明通道,但是在quality一致的情况下,WEBP图像明显要比JPEG图像质量高。当然,只有在14<=API<=17时,WEBP才是有损压缩格式。在API18以后,WEBP和PNG一样,都是无损压缩格式。

4)示例:保存压缩后的图像

private void saveBmp(Bitmap bitmap) {
	File fileDir = Environment.getExternalStorageDirectory();
	String path = fileDir.getAbsolutePath() + "/lavor.webp";
	File file = new File(path);
	if (file.exists()) {
		file.delete();
	}
	try {
		FileOutputStream outputStream = new FileOutputStream(file);
		bitmap.compress(Bitmap.CompressFormat.WEBP, 10, outputStream);
		outputStream.flush();
		outputStream.close();
	} catch (FileNotFoundException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace ();
	}
}

常见问题:

1.对Bitmap的画笔设置ANTI_ALIAS_FLAG属性,为什么无效

经常会有开发者提出,明明对Bitmap的画笔设置了ANTIALIASFLAG属性,为什么画出来的Bitmap边缘还是粗糙的?
下面讲一下我们经常使用的两种绘图策略。
(l)直接在Canvas上绘制。
(2)先在Bitmap上绘制,再将Bitmap绘制到Canvas上。
我们来看一下,在这两种情况下给Bitmap的画笔设置ANTI_ALIAS_FLAG属性,各有什么效果。

Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
// 或者
Paint p = new Paint();
p.setAntiAlias(true);
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawCircle(mLeftX + 100, mTopY + 100, 100, p);
}

正如看到的,设置ANTI_ALIAS_FLAG属性可以产生平滑的边缘。它之所以能起作用,是因为默认当onDraw()函数被调用时,系统先将Canvas清空,然后重绘所有内容。注意:先清空Canvas,然后重绘所有内容。

2)先在Bitmap上绘制,再将Bitmap绘制到Canvas上

Paint p = new Paint();
Bitmap bitmap = null;
Canvas bitmapCanvas = null;
private void init() {
    bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
    bitmapCanvas = new Canvas(bitmap);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    bitmapCanvas.drawCircle(mLeftX + 100, mTopY + 100, 100, p);
    canvas.drawBitmap(bitmap, mLeftX, mTopY, p);
}

从效果图中可以看到,没有设置ANTI_ALIAS_FLAG属性的图像不够平滑,而设置了该属性的图像效果好一点,但还是能发现它的边缘是粗糙的。为什么?

我们很容易忽视上面的代码片段出现的问题。虽然每次onDraw()函数被调用时都会更新在Bitmap上绘制的图形,但从理论上说,你只是在上一张图片上重绘的。所以,解决这个问题的途径是看看ANTI_ALIAS_FLAG属性到底是怎么工作的。

3)ANTI_ALIAS_FLAG属性是怎么工作的

简单来说,ANTI_ALIAS_FLAG属性通过混合前景色与背景色来产生平滑的边缘。在我们的例子中,背景色是透明的,而前景色是红色的,ANTI_ALIAS_FLAG属性通过将边缘处的像素由纯色逐步转换为透明来让边缘看起来是平滑的。

而当我们在Bitmap上重绘时,像素的颜色会越来越纯粹,从而导致边缘越来越粗糙。在下面这张图片中,我们来看一下不断重绘50%透明度的红色会出现什么状况。正如你看到的,只需重绘三次,颜色就十分接近纯色了。这就是为什么设置了ANTI_ALIAS_FLAG属性后,图像的边缘仍然十分粗糙。

该如何解决这个问题?这里有两种选择:
● 避免重绘。
● 在重绘前清空Bitmap。

避免重绘的方法很简单,只需保证让Bitmap只被绘制一次即可,比如将Bitmap绘制操作放在初始化(onCreate()函数)的时候,而不要放在可能被多次调用的onDraw()、onMeasure()、onLayout()等函数中。

修改上文的代码,添加一行代码,让它在每次重绘前先请空Bitmap。当然,如果你觉得纯色更加符合你的需求,则也可以不用每次都清空Bitmap。

@Override
protected void onDraw(Canvas canvas ) {
    super.onDraw(canvas);
    // 清空Bitmap
    bitmapCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
    bitmapCanvas.drawCircle(mLeftX + 100, mTopY + 100, 100, p);
    canvas.drawBitmap(bitmap, mLeftX, mTopY, p);
}

2.Bitmap与Canvas、View、Drawable的关系

1)Bitmap与Canvas

Canvas canvas = new Canvas(bitmap);
// 或者
Canvas canvas = new Canvas();
canvas.setBitmap(bitmap;

从这两种方法中可以看出,Canvas中的画布实质上就是Bitmap,调用Canvas的各个绘图函数,实质上都是绘制在这个Canvas里保存的Bitmap对象上的。

2)Bitmap与View

在自定义控件时,如果该控件派生自View,则都会重写onDraw(Canvascanvas)函数,而当调用onDraw()函数里的参数canvas绘图之后,就会直接表现在View上。
难道View也是通过Canvas来显示的?其中保存的也是一个Bitmap?
答案是:我们自定义控件所显示的View也是通过Canvas中的Bitmap来显示的。

下面从View的源码中来寻找答案。
首先找到onDraw(Canvas canvas函数中的canvas参数是从哪里来的。

public void draw(Canvas canvas) {
//  Step 1, draw the background, if needed
//  skip step 2 & 5 if possible (common case)
//  Step 3 , draw the content
    if (!dirtyOpaque) {
        onDraw(canvas);
    }
//  Step 4, draw the children
//  Step 5, draw the fade effect and restore layers
//  Step 6, draw decorations (scrollbars)
...
}

在draw(Canvascanvas)函数中可以看到绘制一个控件的6个步骤,其中第三步,即绘制控件内容时,调用的是onDraw(canvas)函数。
然后继续找draw(canvas)函数中的canvas参数是从哪里来的。

Bitmap createSnapshot(Bitmap.Config quality, int backgroundColor, boolean skipChildren) {
    ...
    Bitmap bitmap = Bitmap.createBitmap(width > 0 ? width : 1, height > 0 ? height : 1, quality);
    if (bitmap == null) {
        throw new OutOfMemoryError();
    }
    Canvas canvas;
    if (attachinfo != null) {
        canvas = attachinfo.mCanvas;
        if (canvas == null) {
            canvas= new Canvas();
        }
        canvas.setBitmap(bitmap);
        attachinfo.mCanvas = null;
    } else {
        // This case should hopefully never or seldom happen
        canvas = new Canvas(bitmap);
    }
    ...
    if ((mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) {
        dispatchDraw(canvas);
    } else {
        draw(canvas);
    }
    ...
}

从这里可以看出,Canvas 是新建的,而且Canvas中的Bitmap也是新建的。所以,View所显示的内容同样是通过绘制一个内置的Bitmap来呈现的。

3)Bitmap与Drawable

我们在自定义Drawable时,想要将Drawable对象显示在View上,必须使用类似下面的方法。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Drawable drawable = new ShapeDrawable(new RectShape());
    drawable.setBounds(new Rect(S0, 50, 200, 100));
    drawable.draw(canvas);
}

在构造好 Drawable 以后,通过drawable.draw(canvas函数将它绘制到View上。下面我们就来看一下 drawable.draw(canvas)函数到底做了什么。

在Drawable 类中,draw(canvas)是一个虚函数。

public abstract class Drawable {
    public abstract void draw(Canvas canvas);
    ...
}

所以,具体的实现都是放在Drawable的各个子类中的。我们以ShapeDrawable为例,来看一下它的draw(canvas函数都执行了哪些操作。

@Override
public void draw(Canvas canvas) {
    Rect r = getBounds();
    Paint paint = mShapeState.mPaint;
    int prevAlpha = paint.getAlpha();
    paint.setAlpha(modulateAlpha(prevAlpha, mShapeState.mAlpha));
    if (mShapeState.mShape != null) {
        // need the save both for the translate, and for the (unknown) Shape
        int count= canvas.save();
        canvas.translate(r.left, r.top);
        onDraw(mShapeState.mShape, canvas, paint);
        canvas.restoreToCount(count);
    } else {
        canvas.drawRect(r, paint);
    }
    // restore
    paint.setAlpha(prevAlpha);
}
// 其中
protected void onDraw(Shape shape, Canvas canvas, Paint paint) {
    shape.draw(canvas, paint);
}

在这里看到了我们自定义Drawable中重写的onDraw()函数的踪迹,可以看到,onDraw()函数的Paint对象是由Drawable自己保存的。

Paint paint = mShapeState.mPaint;

在mShapeState中保存了当前Drawable的各种基本信息。

final static class ShapeState extends Drawable.ConstantState {
    int mChangingConfigurations;
    Paint mPaint;// 当前画笔
    Shape mShape;// 当前形状
    Rect mPadding;// padding信息
    int mIntrinsicWidth;// Drawable宽度
    int mIntrinsicHeight;// Drawable高度
    int mAlpha = 255;// Drawable透明度
    ...
}

而onDraw()函数中的Canvas对象却是从View中传过来的。
所以,总的来讲:
• Drawable各子类中的Paint是自带的,可以通过getPaint()函数得到。
• Drawable的Canvas画布使用的是View的,它的内部是没有保存Canvas变量的。

3.生成水印

private Bitmap createWaterBitmap(Bitmap src, Bitmap watermark) {
    if (src == null) {
        return null;
    }
    int w = src.getWidth();
    int h = src.getHeight();
    int ww = watermark.getWidth();
    int wh = watermark.getHeight();
    // 创建空白图像
    // 创建一个新的和src长度、宽度一样的Bitmap
    Bitmap newbmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    Canvas cv = new Canvas(newbmp);
    // 画原图
    cv.drawBitmap(src, 0, 0, null);// 从(0, 0)坐标开始画入src
    // 在src的右下角画入水印
    cv.drawBitmap(watermark, w - ww + 5, h - wh + 5, null);
    return newbmp;
}
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
Bitmap watermark = BitmapFactory.decodeResource(getResources(), R.drawable.watermark);
Bitmap result = createWaterBitmap(bitmap, watermark);
ImageView imageView = (ImageView) findViewById(R.id.img);
imageView.setImageBitmap(result);

      

三、SurfaceView

概述:

我们所有的控件基本上都是派生自View或者ViewGrou 的,为什么又多引入一个SurfaceView呢?
Android 屏幕刷新的时间间隔是16ms,如果View能够在16ms内完成所需执行的绘图操作,那么在视觉上,界面就是流畅的;否则就会出现卡顿。很多时候,在自定义View的日志中,经常会看到如下警告:

Skipped 60 frames ! The application may be doing too much work on its main thread

之所以会出现这些警告,大部分是因为我们在绘制过程中不单单执行了绘图操作,也夹杂了很多逻辑处理,导致在指定的16ms内并没有完成绘制,出现界面卡顿和警告。

然而,在很多情况下,这些逻辑处理又是必需的。为了解决这个问题,Android引入了SurfaceView。SurfaceView在两个方面改进了View 的绘图操作:
● 使用双缓冲技术。

● 自带画布,支持在子线程中更新画布内容。

所谓双缓冲技术,简单来讲,就是多加一块缓冲画布,当需要执行绘图操作时,先在缓冲画布上绘制,绘制好后直接将缓冲画布上的内容更新到主画布上。这样,在屏幕更新时,只需把缓冲画布上的内容照样画过来就可以了,就不会存在逻辑处理时间的问题,也就解决了超时绘制的问题。

由于View、ViewGroup、Animator的代码执行全部是在主线程中完成的,所以,当绘图操作的处理逻辑太过复杂时,除了引起卡顿,也可能会造成ANR(Application Not Responding)。而如果我们想在线程中更新界面,就需要使用Handler或者AsyncTask等,这无疑会加大代码的复杂度。SurfaceView就是为了解决这个问题而诞生的。SurfaceView中自带Canvas(就是我们所说的缓冲Canvas),支持在线程中更新Canvas中的内容。

虽然SurfaceView在处理耗时操作时很有用,但正是因为在新的线程中更新画面,所以不会阻塞主线程。但这也带来了另一个问题,就是事件同步。比如,你触摸了屏幕,SurfaceView就会调用线程来处理,当线程过多时,一般就需要一个线程队列来保存触摸事件,这会稍稍复杂一点,因为涉及线程同步。

所以,总的来讲,View和SurfaceView都有各自的应用场景。
● 当界面需要被动更新时,用View较好。比如,与手势交互的场景,因为画面的更新是依赖onTouch来完成的,所以可以直接使用invalidate()函数。在这种情况下,这一次Touch和下一次Touch间隔的时间比较长,不会产生影响。
● 当界面需要主动更新时,用SurfaceView较好。比如一个人在一直跑动,这就需要一个单独的线程不停地重绘人的状态,避免阻塞主线程。显然View不合适,需要SurfaceView来控制。
● 当界面绘制需要频繁刷新,或者刷新时数据处理量比较大时,就应该用SurfaceView来实现,比如视频播放及Camera(摄像头)。

SurfaceView的基本用法:

1.实现View功能

public class SurfaceView
extends View

java.lang.Object
   ↳android.view.View
    ↳android.view.SurfaceView
Known direct subclasses

GLSurfaceViewVideoView

SurfaceView派生自View,所以SurfaceView能使用View中的所有方法,即用View实现的自定义控件都可以使用SurfaceView来实现 。

public class SurfaceView extends View {
    ...
}

需要注意的是,View中的所有方法都是在主线程中执行的。所以,如果重写的方法来自View,那么也是在主线程中执行的。

1)基本实现

下面我们就用SurfaceView来实现 曾经实现的捕捉用户手势轨迹的自定义控件。为了增强代码可读性,不使用贝济埃曲线,直接使用Path 来连接手势轨迹 。

public class CustomView extends SurfaceView {
    private Paint mPaint;
    private Path mPath;
    public CustomView(Context context) {
        super(context);
        init();
    }
    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    public CustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init(){
        // setWillNotDraw(false);
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);
        mPaint.setColor(Color.RED);

        mPath = new Path();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int)event.getX();
        int y = (int)event.getY();
        if (event.getAction() == MotionEvent.ACTION_DOWN){
            mPath.moveTo(x,y);
            Log.d("TAG", "ACTION_DOWN");
            return true;
        } else if (event.getAction() == MotionEvent.ACTION_MOVE){
            mPath.lineTo(x,y);
        }
        postInvalidate();
        Log.d("TAG", "invalidate");
        return super.onTouchEvent(event);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(mPath,mPaint);
        Log.d("TAG", "ondraw");
    }
}

D/TAG: ACTION_DOWN
D/TAG: invalidate
D/TAG: invalidate
D/TAG: invalidate
D/TAG: ...

效果却是不显示手势轨迹,而一直显示黑屏。
通过日志可以看到,手指触摸屏幕后,然后移动手指,“ondraw”一直没有打印出来,即onDraw()函数没被调用。

那么,问题又来了:明明己经调用了postlnvalidate()函数,怎么在SurfaceView中不调用onDraw()函数来重绘呢?

在这段代码的init()函数中注释掉了一个方法,即setWillNotDraw(false);当你把这个方法打开后,就会惊奇地发现,居然可以看到手势轨迹了!

2020-06-18 16:23:42.594 D/TAG: ondraw
2020-06-18 16:23:44.565 D/TAG: ACTION_DOWN
2020-06-18 16:23:44.671 D/TAG: invalidate
2020-06-18 16:23:44.690 D/TAG: invalidate
2020-06-18 16:23:44.691 D/TAG: ondraw
2020-06-18 16:23:44.708 D/TAG: invalidate
2020-06-18 16:23:44.710 D/TAG: ondraw
2020-06-18 16:23:44.726 D/TAG: invalidate
2020-06-18 16:23:44.727 D/TAG: ondraw
2020-06-18 16:23:44.744 D/TAG: invalidate
2020-06-18 16:23:44.745 D/TAG: ondraw
2020-06-18 16:23:44.756 D/TAG: ondraw
2020-06-18 16:23:44.775 D/TAG: invalidate
2020-06-18 16:23:44.793 D/TAG: invalidate
2020-06-18 16:23:44.794 D/TAG: ondraw
2020-06-18 16:23:44.812 D/TAG: invalidate
2020-06-18 16:23:44.814 D/TAG: ondraw
2020-06-18 16:23:44.830 D/TAG: invalidate
2020-06-18 16:23:44.831 D/TAG: ondraw
2020-06-18 16:23:44.847 D/TAG: invalidate
2020-06-18 16:23:44.848 D/TAG: ondraw
2020-06-18 16:23:44.865 D/TAG: invalidate
2020-06-18 16:23:44.866 D/TAG: ondraw
2020-06-18 16:23:44.884 D/TAG: invalidate
2020-06-18 16:23:44.885 D/TAG: ondraw
2020-06-18 16:23:44.903 D/TAG: ondraw
2020-06-18 16:23:44.975 D/TAG: invalidate
2020-06-18 16:23:44.992 D/TAG: ondraw
2020-06-18 16:23:45.442 D/TAG: invalidate
2020-06-18 16:23:45.452 D/TAG: ondraw

2)setWillNotDraw(boolean willNotDraw)

这个函数存在于View类中,它主要用在View派生子类的初始化中,如果参数willNotDraw取true,则表示当前控件没有绘制内容,当屏幕重绘的时候,这个控件不需要绘制,所以在重绘的时候也就不会调用这个类的onDraw()函数。相反,如果参数willNotDraw取false,则表示当前控件在每次重绘时,都需要绘制该控件。可见,setWillNotDraw其实是一种优化策略,它让控件显式地告诉系统,在重绘屏幕时,哪个控件需要重绘,哪个控件不需要重绘,这样就可以大大提高重绘效率。

一般而言,像LinearLayout、RelativeLayout等布局控件,它们的主要功能是布局其中的控件,它们本身是没有东西需要绘制的,所以它们在构造的时候都会显式地设置setWillNotDraw(true)。

3)总结

(1)原本能够通过派生自View控件,依然可以通过SurfaceView来实现,因为SurfaceView派生自View。

(2)当SurfaceView需要使用View的onDraw()函数来重绘控件时,需要在初始化的时候调用setWillNotDraw(false),否则onDraw()函数不会被调用。

(3)View中所有方法都是在主线程(UI线程)中执行的,所以并不建议使用SurfaceView重写View的onDraw()函数来实现自定义控件,而要使用SurfaceView特有的双缓冲机制绘图。

所以,总的来讲,在需要用到SurfaceView特性的时候,建议使用SurfaceView;否则,仍使用View来自定义控件。

2.使用缓冲Canvas绘图

SurfaceView是自带画布的,具有双缓冲技术。这是SurfaceView建议使用的绘图方式。那么问题来了:我们怎么拿到这块自带画布来绘图呢?

SurfaceHolder surfaceHolder = getHolder();
Canvas canvas = surfaceHolder.lockCanvas();
//TODO 绘图操作
surfaceHolder.unlockCanvasAndPost(canvas);

我们可以先通过surfaceHolder.lockCanvas()函数得到SurfaceView中自带的缓冲画布,并将这个画布加锁,防止被其他线程更改:当绘图操作完成以后,通过surfaceHolder.unlockCanvasAndPost(canvas)函数将缓冲画布释放,并将所画内容更新到主线程的画布上,显示在屏幕上。

为什么得到画布的时候要加锁?

我们讲过,SurfaceView中的缓冲画布是可以在线程中更新的,这是它的一大特点。而如果我们有多个线程同时更新画布,那么这个画布岂不是被画得乱七八糟的,所以我们需要加锁。

而加锁会造成另一个问题,当画布被其他线程锁定的时候或者缓存Canvas没有被创建的时候,surfaceHolder.lockCanvas()函数会返回null,这样一来,如果存在多个线程同时操作缓冲画布的情况,则不仅需要对画布做判空处理,也需要在画布为空的时候添加重试策略。这里为了增强代码可读性就不再添加这些判断和策略了,大家在现实使用中一定要注意!

我们更改一下上面的跟踪用户手势轨迹的代码,使用缓冲画布来绘图。

public class SurfaceViewGesturePath extends SurfaceView {
    private Paint mPaint;
    private Path mPath;
    // 省略构造函数和 init()函数
    ...
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) eveηt.gety();
        if (event.getAction() == MotionEvent.ACTION DOWN) {
            mPath.moveTo(x, y);
            return true;
        } else if (event.getAction() == MotionEvent.ACTION MOVE} {
            mPath.lineTo(x, y);
        }
        drawCanvas();
        return super.onTouchEvent(event );
    }
    private void drawCanvas() {
        SurfaceHolder surfaceHolder = getHolder();
        Canvas canvas = surfaceHolder.lockCanvas();// 得到SurfaceView中自带的缓冲画布,并将这个画布加锁
        canvas.drawPath(mPath, mPaint);
        surfaceHolder.unlockCanvasAndPost(canvas);
    }
}

可见,在 onTouchEvent()函数中并没有再调用invalidate()函数,而是直接在需要重绘时,调用 surfaceHolder.lockCanvas()函数来绘图。这里并没有什么坑。

onTouchEvent()函数确实是在主线程中执行的,而我们的给图操作虽然是在缓冲画布上执行的,但依然是在主线程中绘制的,占用的是主线程的资源。所以这种写法跟直接重写View的onDraw()函数是没有什么区别的 。
我们说过,可以在子线程中更新画布。如果我们在子线程中更新画布,那又是什么情况呢?

private void drawCanvas() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            SurfaceHolder surfaceHolder = getHolder();
            Canvas canvas= surfaceHolder.lockCanvas();
            canvas.drawPath(mPath , mPaint);
            surfaceHolder.unlockCanvasAndPost(canvas);
        }
    }).start();
}

这样,绘图操作就是在子线程中执行的,就不会再占用主线程的资源,这才是SurfaceView的正确使用方法。
3.监听Surface生命周期

1)概述

上面我们简单介绍了如何使用SurfaceView缓冲画布,其实与SurfaceView相关的有三个概念:Surface、SurfaceView、SurfaceHolder。

其实,这三个概念是典型的MVC模式(Model-View-Controller)。Model就是数据模型,或者更简单地说就是数据,也就是这里的Surface,Surface中保存着缓冲画布和与绘图内容相关的各种信息;View即视图,代表用户交互界面,也就是这里的SurfaceView,负责将Surface中存储的数据展示在View上;SurfaceHolder很明显可以理解为MVC中的Controller(控制器),Surface是不允许直接用来操作的,必须通过SurfaceHolder来操作Surface中的数据。

既然我们知道SurfaceView的缓存Canvas是保存在Surface中的,那么,必然需要Surface存在的时候,才能够操作缓存Canvas,否则很容易导致获取到的Canvas是空的。庆幸的是,Android为我们提供了监昕Surface生命周期的函数。

SurfaceHolder surfaceHolder = getHolder();
surfaceHolder.addCallback(new SurfaceHolder.Callback() {
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
    }
    @Override
    public void surfaceChanged (SurfaceHolder holder, int format, int width, int height) {
    }
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    }
});

• surfaceCreated:当Surface对象被创建后,该函数就会被立即调用。
• surfaceChanged:当Surface发生任何结构性的变化时(格式或者大小),该函数就会被立即调用。
• surfaceDestroyed:当Surface对象将要销毁时,该函数就会被立即调用。

一般来说,如果我们需要在类初始化时就立即绘图,那么一般是放在surfaceCreated()函数中开启线程来操作的,以防Surface还没有被创建,返回的缓冲画布是空的;在调用surfaceDestroyed()函数时看线程是否执行完,如果还没执行完就强制取消。

2)示例:动态背景效果

根据效果图,我们先列举一下所需实现的功能。

(1)如何保证图片大小既可以充满屏幕,又可以左右移动?
为了保证图片可以左右移动,我们需要先将图片进行缩放,将高度缩放为与屏幕一致,而宽度则是屏幕的1.5倍,以使其可以左右移动。

(2)如何在屏幕上只画出图像的一部分?
public void drawBitmap(Bitmap bitmap , float left , float top , Paint paint)

(3)如何实现Bitmap左右移动?
我们默认从Bitmap的左上角点(0,0)开始绘制,然后根据每次的步进距离向右移动,当移到底时,再返回向左移动。

public class AnimationSurfaceView extends SurfaceView {
    private SurfaceHolder surfaceHolder;
    private boolean flag = false;// 线程标识
    private Bitmap bitmap_bg;// 背景图

    private float mSurfaceWindth, mSurfaceHeight;// 屏幕宽高
    private int mBitposX;//开始绘制的图片的X坐标
    private Canvas mCanvas;
    private Thread thread;

    // 背景移动状态
    private enum State {
        LEFT, RINGHT
    }

    // 默认为向左
    private State state = State.LEFT;

    private final int BITMAP_STEP = 10;// 背景画布移动步伐.

    public AnimationSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        surfaceHolder = getHolder();
        surfaceHolder.addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                flag = true;
                startAnimation();
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                flag = false;
            }
        });
    }

    private void startAnimation() {
        mSurfaceWindth = getWidth();
        mSurfaceHeight = getHeight();
        int mWindth = (int) (mSurfaceWindth * 3 / 2);
        /***
         * 将图片宽度放大到屏幕的1.5倍,高度充满屏幕
         */
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.scenery);
        bitmap_bg = Bitmap.createScaledBitmap(bitmap, mWindth, (int) mSurfaceHeight, true);

        //开始绘图
        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (flag) {
                    mCanvas = surfaceHolder.lockCanvas();
                    DrawView();
                    surfaceHolder.unlockCanvasAndPost(mCanvas);
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        thread.start();
    }

    /***
     * 进行绘制.
     */
    protected void DrawView() {
        //绘制背景
        mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);// 清屏幕.
        mCanvas.drawBitmap(bitmap_bg, mBitposX, 0, null);// 绘制当前屏幕背景

        /** 图片滚动效果 **/
        switch (state) {
            case LEFT:
                mBitposX -= BITMAP_STEP;// 画布左移
                break;
            case RINGHT:
                mBitposX += BITMAP_STEP;// 画布右移
                break;
            default:
                break;
        }
        // 根据当前图片是不是到底了 来决定是继续向右移动还是返回向左移动
        // -W/2 — 0 —— W — W+W/2
        if (mBitposX <= -mSurfaceWindth / 2) {
            state = State.RINGHT;
        }
        if (mBitposX >= 0) {
            state = State.LEFT;
        }
    }
}

因为我们需要在这个控件刚展示的时候自己开始运动,并不会与用户交互,而且需要不断绘图,这些要求非常符合SurfaceView的特性,所以通过派生自SurfaceView来自定义控件。

为了减轻主线程的计算负担,我们单独开启一个线程来执行绘图操作:在绘图完成后,我们延缓50ms再进行下次绘图操作,这样从效果上来看就是一步步移动的。

SurfaceView双缓冲技术:

前面讲到,Surface类是使用一种被称为双缓冲的技术来渲染程序UI的。这种双缓冲技术需要两个图形缓冲区,其中一个被称为前端缓冲区,另一个被称为后端缓冲区。前端缓冲区对应当前屏幕正在显示的内容,而后端缓冲区是接下来要渲染的图形缓冲区。我们通过surfaceHolder.lockCanvas()函数获得的缓冲区是后端缓冲区。当绘图完成以后,调用surfaceHolder.unlockCanvasAndPost(mCanvas)函数将后端缓冲区与前端缓冲区交换,后端缓冲区变成前端缓冲区,将内容显示在屏幕上;而原来的前端缓冲区则变成后端缓冲区,等待下一次sufaceHolder.lockCanvas()函数调用返回给用户使用,如此往复。

正是由于两块画布交替用来绘图,在绘图完成以后相互交换位置,而且在绘图完成以后直接更新到屏幕上,所以才使得绘图效率大大提高。而这样做却造成了一个问题:两块画布上的内容肯定会存在不一致的情况,尤其是在多线程的情况下。比如,我们利用一个线程操作A、B两块画布,目前A画布是屏幕画布,所以,当线程要绘图时,获得的缓冲画布是B。在更新以后,B画布更新到屏幕上,A画布与B画布交换位置。而这时,如果线程再次申请画布,则将获取到A画布。如果A画布与B画布上的内容不一样,那么,在A画布上继续作画肯定会与预想的不一样。

下面举一个例子,每获取一次画布写一个数字,循环10次。代码如下:

public class DoubleBufferingView extends SurfaceView {
    private Paint mPaint;

    public DoubleBufferingView(Context context) {
        super(context);
        init();
    }
    public DoubleBufferingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    public DoubleBufferingView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setTextSize(30);
        getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder ) {
                drawText(holder);
            }
            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            }
            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
            }
        });
    }

    private void drawText(SurfaceHolder holder) {
        for (int i = 0; i < 10; i++) {
            Canvas canvas = holder.lockCanvas();
            if (canvas != null) {
                canvas.drawText(i + "", i * 30, 50, mPaint);
            }
            holder.unlockCanvasAndPost(canvas);
        }
    }
}

在绘图时,每写一次数字都获取一次画布、更新一次画布。在写数字时,利用drawText()函数指定开始写数字的坐标,将各个数字分散开来。

按照我们的逻辑,如果有两块缓冲画布,那么结果应该是1 3 5 7 9。因为最后一个更新的数字必然是9,而往前推,每次间隔使用画布 , 跟9在同一块画布上的必然是 1 3 5 7,其他数字都在另一块画布上。但结果为什么是0 3 6 9呢?这是因为这里有三块缓冲画布。
如果我们在绘图时使用单独的线程,而且每次绘图完成以后,让线程休眠一段时间,就可以明显地看到每次所绘制的数字了。

private void drawText(final SurfaceHolder holder) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    Canvas canvas = holder.lockCanvas();
                    if (canvas != null) {
                        canvas.drawText(i + "", i * 30, 50, mPaint);
                    }
                    holder.unlockCanvasAndPost(canvas);
                    try {
                        Thread.sleep(800);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

 (这次最终绘制出的是:1 3 5 7 9)

0
□1
0□2
□1□3
0□2□4
□1□3□5
0□2□4□6
□1□3□5□7
0□2□4□6□8
□1□3□5□7□9

从以上数字的变化中,可以看到这里用到了2块缓冲画布,所以偶数都在1张缓冲画布上,奇数都在另1张缓冲画布上。

有关Surface中缓冲画布的数量,Google给出的解释如下:

The BufferQueue for a display Surface is typically configured for triple-buffering; but buffers are allocated ondemand. 
So if the producer generates buffers slowly enough -- maybe it's animating at 30fps on a 60fps display -- 
there might only be two allocated buffers in the queue. This helps minimize memory consumption. 
You can see a summary of the buffers associated with every layer in the [dumpsys SurfaceFlinger] output.

也就是说,Surface中缓冲画布的数量是根据需求动态分配的。如果用户获取画布的频率较慢,那么将会分配两块缓冲画布;否则,将分配 3的倍数块缓冲画布,具体分配多少块,视实际情况而定 。
总的来讲,Surface肯定会被分配大于等于两个缓冲区域, 具体分配多少个缓冲区域是不可知的。

2.双缓冲技术局部更新原理

SurfaceView是支持局部更新的,我们可以通过 Canvas lockCanvas(Rect dirty)函数指定获取画布的区域和大小。画布以外的地方会将现在屏幕上的内容复制过来,以保持与屏幕一致;而画布以内的区域则保持原画布内容。前面我们一直使用lockCanvas()函数来获取画布,这两个函数的区别如下。

■ lockCanvas():用于获取整屏画布,屏幕内容不会被更新到画布上,画布保持原画布内容。
■ lockCanvas(Rect dirty):用于获取指定区域的画布,画布以外的区域会保持与屏幕内容一致,画布以内的区域依然保持原画布内容。

首先,我们回顾一下lockCanvas()函数的作用。
我们每次通过lockCanvas()函数获取到的都是整屏画布,而这块画布不会继承当前屏幕内容,只保持所有在自己上面所画的内容。这就是“保持原画布内容”的含义,意思就是,所有在这块画布上上次利用holder.unlockCanvasAndPost(canvas);更新到屏幕上的内容,再次拿到这块画布时,初始态还是这些内容。
在重温了lockCanvas()函数之后,我们再来看看JockCanvas(Rect dirty)函数的含义。

public class RectView extends View {
    private Paint mPaint;
    public RectView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    // 其他构造函数省略
    ...
    private void init() {
        mPaint = new Paint();
        mPaint.setTextSize(30);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 画大方
        mPaint.setColor(Color.RED);
        canvas.drawRect(new Rect(l0, 10, 600, 600), mPaint);
        // 画中方
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(new Rect(30, 30, 570, 570), mPaint);
        // 画小方
        mPaint.setColor(Color.BLUE);
        canvas.drawRect(new Rect(60, 60, 540, 540), mPaint);
        // 画圆形
        mPaint.setColor(Color.argb(0x3F, 0xFF, 0xFF, 0xFF));
        canvas.drawCircle(300, 300, 100, mPaint);
        // 写数字
        mPaint.setColor(Color.GREEN);
        canvas.drawText("6", 300, 300, mPaint);
    }
}

从效果图中可以看到一层层的叠加效果。如果我们将这些层次分明的图形利用SurfaceView来绘制,那么效果是怎样的呢?代码如下:

public class CustomView extends SurfaceView {
    private Paint mPaint;

    public CustomView(Context context) {
        super(context);
        init();
    }

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setColor(Color.argb(0x1F, 0xFF, 0xFF, 0xFF));
        mPaint.setTextSize(30);

        getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                drawText(holder);
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
            }
        });
    }

    private void drawText(final SurfaceHolder holder) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //先进行清屏操作
                while (true) {
                    Rect dirtyRect = new Rect(0, 0, 1, 1);
                    Canvas canvas = holder.lockCanvas(dirtyRect);
                    Rect canvasRect = canvas.getClipBounds();
                    if (getWidth() == canvasRect.width() && getHeight() == canvasRect.height()) {
                        canvas.drawColor(Color.BLACK);
                        holder.unlockCanvasAndPost(canvas);
                    } else {
                        holder.unlockCanvasAndPost(canvas);
                        break;
                    }
                }
                //画图
                for (int i = 0; i < 5; i++) {
                    //画大方
                    if (i == 0) {
                        Canvas canvas = holder.lockCanvas(new Rect(10, 10, 600, 600));
                        dumpCanvasRect(canvas);
                        canvas.drawColor(Color.RED);
                        holder.unlockCanvasAndPost(canvas);
                    }
                    //画中方
                    if (i == 1) {
                        Canvas canvas = holder.lockCanvas(new Rect(30, 30, 570, 570));
                        dumpCanvasRect(canvas);
                        canvas.drawColor(Color.GREEN);
                        holder.unlockCanvasAndPost(canvas);
                    }
                    //画小方
                    if (i == 2) {
                        Canvas canvas = holder.lockCanvas(new Rect(60, 60, 540, 540));
                        dumpCanvasRect(canvas);
                        canvas.drawColor(Color.BLUE);
                        holder.unlockCanvasAndPost(canvas);
                    }
                    //画圆形
                    if (i == 3) {
                        Canvas canvas = holder.lockCanvas(new Rect(200, 200, 400, 400));
                        dumpCanvasRect(canvas);
                        mPaint.setColor(Color.argb(0x3F, 0xFF, 0xFF, 0xFF));
                        canvas.drawCircle(300, 300, 100, mPaint);
                        holder.unlockCanvasAndPost(canvas);
                    }
                    //写字
                    if (i == 4) {
                        Canvas canvas = holder.lockCanvas(new Rect(250, 250, 350, 350));
                        dumpCanvasRect(canvas);
                        mPaint.setColor(Color.RED);
                        canvas.drawText(i + "", 300, 300, mPaint);
                        holder.unlockCanvasAndPost(canvas);
                    }
                    try {
                        Thread.sleep(800);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    private void dumpCanvasRect(Canvas canvas) {
        if (canvas != null) {
            Rect rect = canvas.getClipBounds();
            Log.d("TAG", "left:" + rect.left + "  top:" + rect.top + "  right:" + rect.right + "  bottom:" + rect.bottom);
        }
    }
}

从效果图中可以看出,外围的红、绿、蓝框效果是相同的,但是到画圆和写数字部分的效果就完全不一样了。

从最后画圆形和写字可以看出,手机默认分配了三块缓冲画布。并且还可以知道圆形与红色大方形在同一画布,数字与绿色中方形在同一画布。

1)第一次绘图:画大方

if (i == 0) {
    Canvas canvas = holder.lockCanvas(new Rect(10, 10, 600, 600));
    canvas.drawColor(Color.RED);
    holder.unlockCanvasAndPost(canvas);
}

先拿到一块缓冲画布,大小是 Rect(10, 10, 600, 600),然后将画布填充为红色。在更新到屏幕上以后,效果如下:

从效果图中也可以看出,指定的画布区域被填充为红色。那么问题来了:画布以外的区域为什么是黑色的呢?

在使用unlockCanvasAndPost(canvas)函数更新到屏幕上以后,把屏幕上的画布给换下来。我们说过,在使用lockCanvas(newRect(10,10,600,600))函数指定的区域内使用的是我们的绘图结果,之外的部分则使用被换下来的屏幕上的内容。由于清屏时全屏绘制黑色,所以,除指定的rect区域以外,都会复制屏幕的黑色。

此时的画布分配情况如下:

2)第二次:画中方

if (i == 1) {
    Canvas canvas = holder.lockCanvas(new Rect(30, 30, 570, 570));
    dumpCanvasRect(canvas);
    canvas.drawColor(Color.GREEN);
    holder.unlockCanvasAndPost(canvas);
}

同样,问题来了: 当使用unlockCanvasAndPost(canvas)函数更新到屏幕上以后,画布B中除指定区域以外的内容应该怎么填充呢?

按照我们在前面所讲的,应该把当前屏幕A的对应区域的内容复制过来。因为这里拿到的画布大小要比上次填充的红色方框小一圈,所以,按理来说,绿色方框的周围会有红色方框的一部分。

很明显,我们的理论是正确的:通过 lockCanvas(rect)函数拿到的画布,画布以内的区域是我们的绘图内容,画布以外的区域是从当前屏幕上复制过来的。

此时,在使用holder.unlockCanvasAndPost(canvas)函数更新之后的画布分配情况如下图所示。

3)第三次绘制:画小方

同样,这次通过holder.lockCanvas()函数再从缓冲区中拿到一块画布,因为缓冲画布使用的是LRU策略,也就是说是轮番使用的,所以这次拿到的是缓冲画布C,从中截取Rect(60,60,540,540)区域返回。当然,这块画布要比上面的绿色画布小。

同样,将这块画布填充为蓝色,然后提交到屏幕上。在通过holder.unlockCanvasAndPost(canvas)函数提交到屏幕上以后,指定画布以外的区域当然也是从屏幕上直接复制过来的。此时的效果图如下图所示。

此时的画布分配情况如下 图所示。

当前缓冲画布C作为屏幕显示,而缓冲画布 A 和 B 则在缓冲区中等待使用。
到这里,我们先对以上内容进行总结:
●缓冲画布是根据LRU策略被存取使用的。
●使用holder.lockCanvas(rect)函数获取到的画布区域,在通过unlockCanvasAndPost(canvas)函数提交到屏幕上时,指定区域内的内容是我们自己的绘图结果,指定区域外的内容是从屏幕上复制过来的,与当前屏幕一致。

4)第四次绘制:画图形

根据LRU策略,这次拿到的缓冲画布应该就是A了。

那么问题来了:我们这次画的是半透明的白色圆,而画布以外的区域是从屏幕上复制过来的,那屏幕内的画布用的是哪块画布呢?
答案是:屏幕内的画布用的是我们拿到的画布本身!我们知道,这里拿到的是画布A,所以画圆就是在画布A上叠加来画的。画出来的圆的效果如下图所示。

很明显,区域内用的是我们的绘图结果,区域外用的是当前屏幕上的内容。只是区域内的画布在作画时,依然使用缓冲画布A本身的画布内容来作画。

此时的画布分配情况如下图所示。

5)第五次绘图:写数字

此时的画布分配情况如下图所示。

同样,区域外的部分用当前屏幕上的内容,区域内的部分是在拿到的缓冲画布B上作画。
很明显,缓冲画布B的这块区域是绿色的,所以在这块区域内写数字的结果如下图所示。

最后整体效果如下图。

此时的画布分配情况如下图所示 。

6)总结

(1)缓冲画布的存取遵循 LRU 策略 。
(2)画布以内的区域仍在原缓冲画布上叠加作画,画布以外的区域是从屏幕上直接复制过来的。
(3)为了防止画布以内的缓冲画布本身的图像与所画内容产生冲突,在对画布以内的区域作画时,建议先清空画布。清空画布的方法如下:

Paint paint = new Paint();
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawPaint(paint);

3.局部更新为何要先清屏

在上面的局部更新的例子中,我们在开始绘图前使用一个 while 循环先请屏。如果把这个while循环去掉,那么效果将是怎样的呢?来看下图 。

来看看在没有清屏的时候,通过 lockCanvas(rect)函数拿到的画布区域是不是还是指定的区域。
在不清屏的情况下,即注释掉如下代码。然后把每次得到的rect区域打印出来。

while (true) {
    Rect dirtyRect = new Rect(0, 0, 1, 1);
    Canvas canvas = holder.lockCanvas(dirtyRect);
    Rect canvasRect = canvas.getClipBounds();
    if (getWidth() == canvasRect.width() && getHeight() == canvasRect.height()) {
        canvas.drawColor(Color.BLACK);
        holder.unlockCanvasAndPost(canvas);
    } else {
        holder.unlockCanvasAndPost(canvas);
        break;
    }
}
D/TAG: left:0  top:0  right:1080  bottom:1731
D/TAG: left:30  top:30  right:570  bottom:570
D/TAG: left:60  top:60  right:540  bottom:540
D/TAG: left:200  top:200  right:400  bottom:400
D/TAG: left:250  top:250  right:350  bottom:350

通过日志发现,第一次获取到的画布区域并不是我们所指定的区域,而是SurfaceView所占的全屏。其他区域正常。

如果加上清屏代码,打印是完全正常的。

很明显,在加上清屏代码以后,每次拿到的画布区域都是指定的区域。这是为什么呢?

因为这里有三块缓冲画布,有一块画布初始化地被显示在屏幕上,已经被默认填充为黑色,而另外两块画布都还没有被画过。虽然我们指定了获取画布的区域范围,但是系统认为,整块画布都是脏区域,都应该被画上,所以会返回屏幕大小的画布。只有我们将每块画布都画过以后,才会按照我们指定的区域来返回画布大小。

利用这个原理,我们可以指定一个极小的区域。

Rect dirtyRect = new Rect(0, 0, 1, 1);
Canvas canvas= holder.lockCanvas(dirtyRect);

如果这个屏幕还没有被画过,那么它应该返回与当前控件一样大小的区域,这时我们就可以给它画上默认的黑色,也可以利用Xfermode的清屏代码。

while (true) {
    Rect dirtyRect = new Rect(0, 0, 1, 1);
    Canvas canvas = holder.lockCanvas(dirtyRect);
    Rect canvasRect = canvas.getClipBounds();
    if (getWidth() == canvasRect.width() && getHeight() == canvasRect.height()) {
        canvas.drawColor(Color.BLACK);
        holder.unlockCanvasAndPost(canvas);
    } else {
        holder.unlockCanvasAndPost(canvas);
        break;
    }
}

很明显,当返回的区域大小不与当前控件大小一致时,就表示我们已经把所有的画布都画了一遍,这时,我们就可以正式作画了。

通过调试可以发现,程序首先进入到while里面的if语句,此时getWidth()==canvasRest.width()=1080;getHeight()==canvasRect.height()=1731。
继续while循环,此时,getWidth()=1080,getHeight()=1731;canvasRect.width()=1,canvasRect.height()=1。返回的区域大小不与当前控件大小一致,执行else语句块,然后break while循环。之后接着执行接下来的for循环语句。

注:虽然canvas = holder.lockCanvas(new Rect(0,0,1,1));但canvas.getClipBounds()仍是整个屏幕的尺寸。因为holder.lockCanvas(rect)是想要锁住指定区域。而刚创建的时候画布时,lockCanvas(rect)之前没有对画布进行过操作,就锁不住。这与上面示例注释掉while清除画布后程序运行的第一条日志是相匹配的。也印证了这一点。

如果是新建一个指定大小的画布,那性质就不一样了。maybe因为画片确实已经创建成功了。

Bitmap bmp = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bmp);
Rect r = c.getClipBounds();
Log.d("TAG", r.width() + "," + r.height());
———————————————————————————————————————————
D/TAG: 100,200

4.双缓冲技术解决方案

方案一:保存所有要绘制的内容,全屏重绘。

为了防止每次画布上的内容不一致,我们的第一种解决方案就是,每次将我们绘制的内容都保存起来,下次拿到画布时,把这些绘制的内容全部重新画一遍。这种方案主要用在比较简单的给图上。比如,在捕捉用户手势轨迹的例子中,可以用一个全局的Path变量来保存手指的路径,然后每次把整个路径重新画出来。

同样,对于数字的例子而言,如果用这种方案来解决,那么,既可以一次性将所有数字画完,又可以保存每次画完的所有数字,下次将这些数字重画即可。

如果使用一次性画完的方式,则代码如下:

private void drawText(SurfaceHolder holder) (
    Canvas canvas = holder.lockCanvas();
    for (int i=0;i < 10; i++) {
        if (canvas != null) {
            canvas.drawText(i + "",i * 30, 50, mPaint);
            holder.unlockCanvasAndPost(canvas);
        }
    }
}

如果我们将所有数字分10次画到画布上,但每次都将画布内容保存起来,在下次绘制的时候将所有内容全部画上去,则代码如下:

private List<Integer> mInts = new ArrayList<Integer>();
private void drawText(SurfaceHolder holder) {
    for (int i=0; i < 10;i++) {
        Canvas canvas = holder.lockCanvas();
        mints.add(i);
        if (canvas != null) {
            for (int num :mInts) {
                canvas.drawText(num + "", num * 30, 50, mPaint);
            }
        }
        holder.unlockCanvasAndPost(canvas);
    }
}

同一个线程分10次绘图,每次将绘制的内容保存起来的方法看起来很别扭,因为完全可以一次性画上去,为什么还要分10 次?在实际工作 中,大家可能有多个线程同时绘图,只有每个线程各自保存自己当前己经绘制的内容,各线程之间才不会相互被影响 。

方案二:在内容不交叉时,可以采用增量绘制

经过前面的分析可知,通过lockCanvas(rect)函数得到的局部缓冲区域内的绘图依然是在所拿到的缓冲画布源图像基础上绘制的,所以,为了避免源图像内容对我们所绘内容的干扰,可以采取两种方法。

第一种方法就是对拿到的区域画布清屏,在清屏后,把我们所要画的内容画出来。当然,为了保证与以前所画内容一致,也需要把以前的绘制内容保存起来重画一遍。这种方法其实与解决方案一一致。

第二种方法相对简单,就是当我们所绘内容不交叉时,可以采用增量绘制。比如上面的写数字问题,每个数字的写字区域都与其他数字不交叉,在这种情况下,我们可以采用增量绘制。

下面仍以写数字为例,来看一下增量绘制的使用方法。

private void drawText(final SurfaceHolder holder) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 先进行清屏操作
            while(true) {
                Rect dirtyRect = new Rect(0, 0, 1, 1);
                Canvas canvas= holder.lockCanvas(dirtyRect);
                Rect canvasRect = canvas.getClipBounds();
                if (getWidth() == canvasRect.width() && getHeight() == canvasRect.height()) {
                    canvas.drawColor(Color.BLACK);
                    holder.unlockCanvasAndPost(canvas);
                } else {         
                    holder.unlockCanvasAndPost(canvas);
                    break;
                }
            }
            // 画图
            for (int i = 0; i < 10; i++) {
                int itemWidth = 50;
                int itemHeight = 50;
                Rect rect = new Rect(i * itemWidth, 0, (i+1) * itemWidth - 10, itemHeight);
                Canvas canvas= holder.lockCanvas(rect);
                if (canvas != null) {
                    canvas.drawColor(Color.GREEN);
                    canvas.drawText(i+"", i * itemWidth + 10, itemHeight/2, mPaint);
                }
                holder.unlockCanvasAndPost(canvas);
                try {
                    Thread.sleep(800);
                } catch(Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();
}

根据局部刷新的原理,当然需要先清屏,再绘图。在绘图时,每间隔一段区间写一个数字,同时把得到的区间画布填充为绿色。


总结:
(1)缓冲画布的存取遵循LRU策略。
(2)通过lockCanvas()或者lockCanvas(null)函数可以得到整个控件大小的缓冲画布,通过lockCanvas(rect)函数可以得到指定大小的缓冲画布。
(3)在使用lockCanvas(rect)函数获取缓冲画布前,需要使用while循环清屏。
(4)所获得画布以内的区域仍在原缓冲画布上叠加作画,画布以外的区域是从屏幕上直接复制过来的。
(5)由于画布以内的区域是在原缓冲画布的基础上叠加作画的,所以,为了防止产生冲突,建议使用Xfermode先请空所获得的画布:或者在内容不交叉时,采用增量绘制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

itzyjr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值