ClipDrawable让开发变得更简单

ClipDrawable让开发更简单

Android开发者对Drawable自然是无比熟悉,Drawable代表一类可以绘制的东西,它有许多继承类,常见的就是BitmapDrawable,此外ShapeDrawable,LayerListDrawable也用得比较多,这篇文章讲另外一个比较常见的类ClipDrawable。ClipDrawable非常简单,写博客是因为觉得它非常好用,有时候合理的运用能让代码编写过程中非常愉悦

ClipDrawable是Drawable中的一种,和我们常见的BitmapDrawable是同类。主要功能是能针对自身进行裁剪复制显示。

先看看效果图。
ClipDrawable让开发变得更简单_第1张图片

文章开头有说过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>

效果如下:
ClipDrawable让开发变得更简单_第2张图片

从中间向两边:

水平方向

<clip xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/test"
    android:clipOrientation="horizontal"
    android:gravity="center">
clip>

ClipDrawable让开发变得更简单_第3张图片

还有这样,垂直方向

<clip xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/test"
    android:clipOrientation="vertical"
    android:gravity="center">
clip>

ClipDrawable让开发变得更简单_第4张图片

从上到下:

<clip xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/test"
    android:clipOrientation="vertical"
    android:gravity="top">
clip>

ClipDrawable让开发变得更简单_第5张图片

从下到上:

<clip xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/test"
    android:clipOrientation="vertical"
    android:gravity="top">
clip>

ClipDrawable让开发变得更简单_第6张图片

注意点

clipOrientation为horizontal时,gravity为top或者bottom时,会被处理为gravity为center.
clipOritentation为vertical时,gravity为left或者right时,也会处理为gravity为center.

ClipDrawable的秘密和原理

掌握上面的方法,我们就可以愉快地玩耍了,可以应付很多场景了,比如系统的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);
        }

        .....
    }

    ......
}

源码没有多少,我删简了一些。保留了主体信息。从源码中我们可以得到下面的信息:

  1. ClipDrawable中内部有一个状态类ClipState,它主要功能是保存原始的Drawable及orientation和gravity信息。
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。

你可能感兴趣的:(Android笔记)