前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ClipDrawable让开发变得更简单

ClipDrawable让开发变得更简单

作者头像
Frank909
发布2019-01-14 18:02:52
9290
发布2019-01-14 18:02:52
举报
文章被收录于专栏:Frank909Frank909

ClipDrawable让开发更简单

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

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

先看看效果图。

这里写图片描述
这里写图片描述

文章开头有说过Drawable,Drawable只是一个抽象类,它有许多子类,常见的有bitmapdrawable、LayerDrawable、LevelListDrawable和ClipDrawable等等。 大家可以看下它的源码。(代码有删简)

代码语言:javascript
复制
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

代码语言:javascript
复制
<?xml version="1.0" encoding="utf-8"?>
<clip xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/test"/>
</clip>

然后在activity_main.xml

代码语言:javascript
复制
<?xml version="1.0" encoding="utf-8"?>
<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

然后在代码中

代码语言:javascript
复制
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中布置它的方向,和它画面增长的位置。

方向

代码语言:javascript
复制
android:clipOrientation="vertical|horizontal"
android:gravity="left|right|top|bottom|center"

clipOrientation是指图像复制的方向是水平还是垂直。 ClipDrawable中默认的方向是horizontal,说明显示内容的增长是自横向的。当设置为vertical时,表明显示内容的增长是竖向的。 当然clipOrientation是和Gravity配合使用的。ClipDrawable中默认的clipOrientation是horizontal,而默认的gravity是left。 gravity可以看作是起点的位置,比如默认的left的话,表明水平方向,图像复制是从左边增长到右边。

接下来我们看具体的场景

右—->左

我们也可以让内容从右边向左边增长,只需要如下配置

代码语言:javascript
复制
<clip xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/test"
    android:clipOrientation="horizontal"
    android:gravity="right">
</clip>

效果如下:

这里写图片描述
这里写图片描述

从中间向两边:

水平方向

代码语言:javascript
复制
<clip xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/test"
    android:clipOrientation="horizontal"
    android:gravity="center">
</clip>
这里写图片描述
这里写图片描述

还有这样,垂直方向

代码语言:javascript
复制
<clip xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/test"
    android:clipOrientation="vertical"
    android:gravity="center">
</clip>
这里写图片描述
这里写图片描述

从上到下:

代码语言:javascript
复制
<clip xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/test"
    android:clipOrientation="vertical"
    android:gravity="top">
</clip>
这里写图片描述
这里写图片描述

从下到上:

代码语言:javascript
复制
<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.

ClipDrawable的秘密和原理

掌握上面的方法,我们就可以愉快地玩耍了,可以应付很多场景了,比如系统的ProgressBar其实就运用了ClipDrawable作进度条的图像,进行水平方向的裁剪复制。但是,作为开发者而言,还可以稍微深入了解一下它的源码。

其实我看见ClipDrawable就在想它是怎么实现图像的裁剪的。那好,看源代码吧。

代码语言:javascript
复制
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 <clip>");
        }

        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信息。
代码语言:javascript
复制
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。

代码语言:javascript
复制
@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。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2016年12月09日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ClipDrawable让开发更简单
  • 用法
    • 方向
      • 右—->左
      • 从中间向两边:
      • 从上到下:
      • 从下到上:
      • 注意点
  • ClipDrawable的秘密和原理
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档