Android开发者对Drawable自然是无比熟悉,Drawable代表一类可以绘制的东西,它有许多继承类,常见的就是BitmapDrawable,此外ShapeDrawable,LayerListDrawable也用得比较多,这篇文章讲另外一个比较常见的类ClipDrawable。ClipDrawable非常简单,写博客是因为觉得它非常好用,有时候合理的运用能让代码编写过程中非常愉悦
ClipDrawable是Drawable中的一种,和我们常见的BitmapDrawable是同类。主要功能是能针对自身进行裁剪复制显示。
文章开头有说过Drawable,Drawable只是一个抽象类,它有许多子类,常见的有bitmapdrawable、LayerDrawable、LevelListDrawable和ClipDrawable等等。
大家可以看下它的源码。(代码有删简)
public abstract class Drawable {
/**
* Draw in its bounds (set via setBounds) respecting optional effects such
* as alpha (set via setAlpha) and color filter (set via setColorFilter).
*
* @param canvas The canvas to draw into
*/
public abstract void draw(Canvas canvas);
/**
* Create a drawable from inside an XML document using an optional
* {@link Theme}. Called on a parser positioned at a tag in an XML
* document, tries to create a Drawable from that tag. Returns {@code null}
* if the tag is not a valid drawable.
*/
public static Drawable createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs,
Theme theme) throws XmlPullParserException, IOException {
final Drawable drawable;
final String name = parser.getName();
if (name.equals("selector")) {
drawable = new StateListDrawable();
} else if (name.equals("animated-selector")) {
drawable = new AnimatedStateListDrawable();
} else if (name.equals("level-list")) {
drawable = new LevelListDrawable();
} else if (name.equals("layer-list")) {
drawable = new LayerDrawable();
} else if (name.equals("transition")) {
drawable = new TransitionDrawable();
} else if (name.equals("ripple")) {
drawable = new RippleDrawable();
} else if (name.equals("color")) {
drawable = new ColorDrawable();
} else if (name.equals("shape")) {
drawable = new GradientDrawable();
} else if (name.equals("vector")) {
drawable = new VectorDrawable();
} else if (name.equals("animated-vector")) {
drawable = new AnimatedVectorDrawable();
} else if (name.equals("scale")) {
drawable = new ScaleDrawable();
} else if (name.equals("clip")) {
drawable = new ClipDrawable();
} else if (name.equals("rotate")) {
drawable = new RotateDrawable();
} else if (name.equals("animated-rotate")) {
drawable = new AnimatedRotateDrawable();
} else if (name.equals("animation-list")) {
drawable = new AnimationDrawable();
} else if (name.equals("inset")) {
drawable = new InsetDrawable();
} else if (name.equals("bitmap")) {
//noinspection deprecation
drawable = new BitmapDrawable(r);
if (r != null) {
((BitmapDrawable) drawable).setTargetDensity(r.getDisplayMetrics());
}
} else if (name.equals("nine-patch")) {
drawable = new NinePatchDrawable();
if (r != null) {
((NinePatchDrawable) drawable).setTargetDensity(r.getDisplayMetrics());
}
} else {
throw new XmlPullParserException(parser.getPositionDescription() +
": invalid drawable tag " + name);
}
drawable.inflate(r, parser, attrs, theme);
return drawable;
}
}
在上面的
createFromXmlInner()
方法中可以看到通过解析xml中的各种标签会创建相应的子类。今天我们的主题就是它的子类之一–ClipDrawable。
ClipDrawable用法非常简单,ClipDrawable可以在xml文件配置,比如我在工程的res/drawable
下创建一个文件 test_drawable.xml
<clip xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/test"/>
clip>
然后在activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.frank.clipdrawabledemo.MainActivity">
<TextView
android:id="@+id/tv_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/iv_show"
android:layout_below="@id/tv_info"
android:layout_width="match_parent"
android:layout_height="500dp"
android:background="@drawable/test_drawable"
/>
<SeekBar
android:id="@+id/seekbar"
android:layout_below="@id/iv_show"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
RelativeLayout>
把ClipDrawable资源设置作为ImageView的background
然后在代码中
mSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
int max = seekBar.getMax();
double scale = (double)progress/(double)max;
ClipDrawable drawable = (ClipDrawable) mImageShow.getBackground();
drawable.setLevel((int) (10000*scale));
mTvShow.setText(progress+"");
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
我们通过一个SeekBar来控制ClipDrawable的level,调用的是它的setLevel()方法。最终可以让它显示不同的比例。 效果就是前面的图像效果。
在ClipsDrawable中level取值从0~10000.其中0表示ClipDrawable完全不能显示,10000表示完全显示,而这之间的值对应着不同的比例。
ClipDrawable还可以在xml中布置它的方向,和它画面增长的位置。
android:clipOrientation="vertical|horizontal"
android:gravity="left|right|top|bottom|center"
clipOrientation
是指图像复制的方向是水平还是垂直。
ClipDrawable中默认的方向是horizontal,说明显示内容的增长是自横向的。当设置为vertical时,表明显示内容的增长是竖向的。
当然clipOrientation是和Gravity配合使用的。ClipDrawable中默认的clipOrientation是horizontal,而默认的gravity是left。
gravity可以看作是起点的位置,比如默认的left的话,表明水平方向,图像复制是从左边增长到右边。
接下来我们看具体的场景
我们也可以让内容从右边向左边增长,只需要如下配置
<clip xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/test"
android:clipOrientation="horizontal"
android:gravity="right">
clip>
水平方向
<clip xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/test"
android:clipOrientation="horizontal"
android:gravity="center">
clip>
还有这样,垂直方向
<clip xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/test"
android:clipOrientation="vertical"
android:gravity="center">
clip>
<clip xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/test"
android:clipOrientation="vertical"
android:gravity="top">
clip>
<clip xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/test"
android:clipOrientation="vertical"
android:gravity="top">
clip>
clipOrientation为horizontal时,gravity为top或者bottom时,会被处理为gravity为center.
clipOritentation为vertical时,gravity为left或者right时,也会处理为gravity为center.
掌握上面的方法,我们就可以愉快地玩耍了,可以应付很多场景了,比如系统的ProgressBar其实就运用了ClipDrawable作进度条的图像,进行水平方向的裁剪复制。但是,作为开发者而言,还可以稍微深入了解一下它的源码。
其实我看见ClipDrawable就在想它是怎么实现图像的裁剪的。那好,看源代码吧。
public class ClipDrawable extends Drawable implements Drawable.Callback {
private ClipState mClipState;
private final Rect mTmpRect = new Rect();
public static final int HORIZONTAL = 1;
public static final int VERTICAL = 2;
ClipDrawable() {
this(null, null);
}
/**
* @param orientation Bitwise-or of {@link #HORIZONTAL} and/or {@link #VERTICAL}
*/
public ClipDrawable(Drawable drawable, int gravity, int orientation) {
this(null, null);
//ClipDrawable包裹其它的Drawable
mClipState.mDrawable = drawable;
mClipState.mGravity = gravity;
mClipState.mOrientation = orientation;
if (drawable != null) {
drawable.setCallback(this);
}
}
@Override
public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
throws XmlPullParserException, IOException {
super.inflate(r, parser, attrs, theme);
int type;
TypedArray a = obtainAttributes(
r, theme, attrs, com.android.internal.R.styleable.ClipDrawable);
int orientation = a.getInt(
com.android.internal.R.styleable.ClipDrawable_clipOrientation,
HORIZONTAL);
int g = a.getInt(com.android.internal.R.styleable.ClipDrawable_gravity, Gravity.LEFT);
Drawable dr = a.getDrawable(com.android.internal.R.styleable.ClipDrawable_drawable);
a.recycle();
final int outerDepth = parser.getDepth();
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type != XmlPullParser.START_TAG) {
continue;
}
dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
}
if (dr == null) {
throw new IllegalArgumentException("No drawable specified for " );
}
mClipState.mDrawable = dr;
mClipState.mOrientation = orientation;
mClipState.mGravity = g;
dr.setCallback(this);
}
......
@Override
protected boolean onLevelChange(int level) {
mClipState.mDrawable.setLevel(level);
invalidateSelf();
return true;
}
@Override
protected void onBoundsChange(Rect bounds) {
mClipState.mDrawable.setBounds(bounds);
}
@Override
public void draw(Canvas canvas) {
if (mClipState.mDrawable.getLevel() == 0) {
return;
}
final Rect r = mTmpRect;
final Rect bounds = getBounds();
int level = getLevel();
int w = bounds.width();
final int iw = 0; //mClipState.mDrawable.getIntrinsicWidth();
if ((mClipState.mOrientation & HORIZONTAL) != 0) {
w -= (w - iw) * (10000 - level) / 10000;
}
int h = bounds.height();
final int ih = 0; //mClipState.mDrawable.getIntrinsicHeight();
if ((mClipState.mOrientation & VERTICAL) != 0) {
h -= (h - ih) * (10000 - level) / 10000;
}
final int layoutDirection = getLayoutDirection();
Gravity.apply(mClipState.mGravity, w, h, bounds, r, layoutDirection);
if (w > 0 && h > 0) {
canvas.save();
canvas.clipRect(r);
mClipState.mDrawable.draw(canvas);
canvas.restore();
}
}
final static class ClipState extends ConstantState {
Drawable mDrawable;
int mChangingConfigurations;
int mOrientation;
int mGravity;
ClipState(ClipState orig, ClipDrawable owner, Resources res) {
if (orig != null) {
if (res != null) {
mDrawable = orig.mDrawable.getConstantState().newDrawable(res);
} else {
mDrawable = orig.mDrawable.getConstantState().newDrawable();
}
mDrawable.setCallback(owner);
mDrawable.setLayoutDirection(orig.mDrawable.getLayoutDirection());
mDrawable.setBounds(orig.mDrawable.getBounds());
mDrawable.setLevel(orig.mDrawable.getLevel());
mOrientation = orig.mOrientation;
mGravity = orig.mGravity;
mCheckedConstantState = mCanConstantState = true;
}
}
@Override
public Drawable newDrawable() {
return new ClipDrawable(this, null);
}
@Override
public Drawable newDrawable(Resources res) {
return new ClipDrawable(this, res);
}
.....
}
......
}
源码没有多少,我删简了一些。保留了主体信息。从源码中我们可以得到下面的信息:
TypedArray a = obtainAttributes(
r, theme, attrs, com.android.internal.R.styleable.ClipDrawable);
int orientation = a.getInt(
com.android.internal.R.styleable.ClipDrawable_clipOrientation,
HORIZONTAL);
int g = a.getInt(com.android.internal.R.styleable.ClipDrawable_gravity, Gravity.LEFT);
2. 在infalate()方法上面这段代码显示,如果在xml中没有指定clipOrientation默认为HORIZONTAL。如果在xml中没有指定gravity,默认是Gravity.LEFT。
@Override
protected boolean onLevelChange(int level) {
mClipState.mDrawable.setLevel(level);
invalidateSelf();
return true;
}
@Override
public void draw(Canvas canvas) {
if (mClipState.mDrawable.getLevel() == 0) {
return;
}
final Rect r = mTmpRect;
final Rect bounds = getBounds();
int level = getLevel();
int w = bounds.width();
final int iw = 0; //mClipState.mDrawable.getIntrinsicWidth();
if ((mClipState.mOrientation & HORIZONTAL) != 0) {
w -= (w - iw) * (10000 - level) / 10000;
}
int h = bounds.height();
final int ih = 0; //mClipState.mDrawable.getIntrinsicHeight();
if ((mClipState.mOrientation & VERTICAL) != 0) {
h -= (h - ih) * (10000 - level) / 10000;
}
final int layoutDirection = getLayoutDirection();
Gravity.apply(mClipState.mGravity, w, h, bounds, r, layoutDirection);
if (w > 0 && h > 0) {
canvas.save();
canvas.clipRect(r);
mClipState.mDrawable.draw(canvas);
canvas.restore();
}
}
3. 我们在使用过程是通过ClipDrawable的setLevel()方法,这个方法会触发它的onLevelChange()方法。
在onLevleChange()中,会设置ClipState中的drawable的level。然后刷新自己,这样触发它的onDraw()方法,对自身进行绘制。
在onDraw()中通过获取level,然后计算结合它的方向,计算它显示的矩形范围,然后通过canvas.clipRect()方法。最终完成图片的裁剪显示.
所以归根到底,ClipDrawable的核心就是
setLevel()
和Canvas.clipRect()
方法。
setLevel()
设置显示比例,
然后在onDraw()方法中调用计算出来的矩形进行画面的裁剪,正是通过
canvas.clipRect().
ClipDrawable是一个非常实用的类,合理的运用能让我们节省不少的图片资源,让代码显得整洁与优雅。试想一下,如果没有ClipDrawable去实现一个ProgressBar,我们可能要用多张不同宽度的图片去动态同步进度条的值,而有了CllipDrawable后,我们只要一张图片就可以搞定各种宽度的进度值。当然,我们可以根据实际的开发情况去定制自己的View。