美文网首页
Drawable源码分析与自定义Drawable Demo

Drawable源码分析与自定义Drawable Demo

作者: 黄海佳 | 来源:发表于2017-03-13 18:03 被阅读597次

    上一篇文章写了 Android Drawable 基础常识 , 今天写写对于 android.graphics.drawable 的源码分析以及在开发中可以用上的例子。

    一、自定义Drawable VS 自定义View

    在实际项目中,我们在会有一些自定义控件,常常是写自定义View来实现,其实使用自定义的Drawable也可以实现大部分的效果,那么现在谈谈自定义Drawable VS 自定义View

    自定义Drawable VS 自定义View的好处:轻量级,更容易实现,提高UI性能优化

    Paste_Image.png

    先简单看代码

    public class CustomView extends Drawable{
    
        @Override
        public void draw(Canvas canvas) {
    
        }
    
        @Override
        public void setAlpha(int i) {
    
        }
    
        @Override
        public void setColorFilter(ColorFilter colorFilter) {
    
        }
    
        @Override
        public int getOpacity() {
            return 0;
        }
    }
    

    发现继承Drawable 绘图只有 draw 方法,如果是自定义View 则有onDraw onMeasure onLayout,这样子我们去看 Drawable 的源码,这里抽取几个常用的方法来讲:

    (一)创建Drawable实例的几种不同方法(流、XML、文件地址),源码如下:
         /**
         * Create a drawable from an inputstream
         */
        public static Drawable createFromStream(InputStream is, String srcName) {
            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, srcName != null ? srcName : "Unknown drawable");
            try {
                return createFromResourceStream(null, null, is, srcName);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
            }
        }
    
         /**
         * Create a drawable from an inputstream, using the given resources and
         * value to determine density information.
         */
        public static Drawable createFromResourceStream(Resources res, TypedValue value,
                InputStream is, String srcName) {
            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, srcName != null ? srcName : "Unknown drawable");
            try {
                return createFromResourceStream(res, value, is, srcName, null);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
            }
        }
    
        /**
         * Create a drawable from an inputstream, using the given resources and
         * value to determine density information.
         */
        public static Drawable createFromResourceStream(Resources res, TypedValue value,
                InputStream is, String srcName, BitmapFactory.Options opts) {
            if (is == null) {
                return null;
            }
    
            /*  ugh. The decodeStream contract is that we have already allocated
                the pad rect, but if the bitmap does not had a ninepatch chunk,
                then the pad will be ignored. If we could change this to lazily
                alloc/assign the rect, we could avoid the GC churn of making new
                Rects only to drop them on the floor.
            */
            Rect pad = new Rect();
    
            // Special stuff for compatibility mode: if the target density is not
            // the same as the display density, but the resource -is- the same as
            // the display density, then don't scale it down to the target density.
            // This allows us to load the system's density-correct resources into
            // an application in compatibility mode, without scaling those down
            // to the compatibility density only to have them scaled back up when
            // drawn to the screen.
            if (opts == null) opts = new BitmapFactory.Options();
            opts.inScreenDensity = res != null
                    ? res.getDisplayMetrics().noncompatDensityDpi : DisplayMetrics.DENSITY_DEVICE;
            Bitmap  bm = BitmapFactory.decodeResourceStream(res, value, is, pad, opts);
            if (bm != null) {
                byte[] np = bm.getNinePatchChunk();
                if (np == null || !NinePatch.isNinePatchChunk(np)) {
                    np = null;
                    pad = null;
                }
    
                final Rect opticalInsets = new Rect();
                bm.getOpticalInsets(opticalInsets);
                return drawableFromBitmap(res, bm, np, pad, opticalInsets, srcName);
            }
            return null;
        }
    
        /**
         * Create a drawable from an XML document. For more information on how to
         * create resources in XML, see
         * <a href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.
         */
        public static Drawable createFromXml(Resources r, XmlPullParser parser)
                throws XmlPullParserException, IOException {
            return createFromXml(r, parser, null);
        }
    
        /**
         * Create a drawable from an XML document using an optional {@link Theme}.
         * For more information on how to create resources in XML, see
         * <a href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.
         */
        public static Drawable createFromXml(Resources r, XmlPullParser parser, Theme theme)
                throws XmlPullParserException, IOException {
            AttributeSet attrs = Xml.asAttributeSet(parser);
    
            int type;
            while ((type=parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                // Empty loop
            }
    
            if (type != XmlPullParser.START_TAG) {
                throw new XmlPullParserException("No start tag found");
            }
    
            Drawable drawable = createFromXmlInner(r, parser, attrs, theme);
    
            if (drawable == null) {
                throw new RuntimeException("Unknown initial tag: " + parser.getName());
            }
    
            return drawable;
        }
    
        /**
         * Create from inside an XML document.  Called on a parser positioned at
         * a tag in an XML document, tries to create a Drawable from that tag.
         * Returns null if the tag is not a valid drawable.
         */
        public static Drawable createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs)
                throws XmlPullParserException, IOException {
            return createFromXmlInner(r, parser, attrs, null);
        }
    
        /**
         * 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();
            switch (name) {
                case "selector":
                    drawable = new StateListDrawable();
                    break;
                case "animated-selector":
                    drawable = new AnimatedStateListDrawable();
                    break;
                case "level-list":
                    drawable = new LevelListDrawable();
                    break;
                case "layer-list":
                    drawable = new LayerDrawable();
                    break;
                case "transition":
                    drawable = new TransitionDrawable();
                    break;
                case "ripple":
                    drawable = new RippleDrawable();
                    break;
                case "color":
                    drawable = new ColorDrawable();
                    break;
                case "shape":
                    drawable = new GradientDrawable();
                    break;
                case "vector":
                    drawable = new VectorDrawable();
                    break;
                case "animated-vector":
                    drawable = new AnimatedVectorDrawable();
                    break;
                case "scale":
                    drawable = new ScaleDrawable();
                    break;
                case "clip":
                    drawable = new ClipDrawable();
                    break;
                case "rotate":
                    drawable = new RotateDrawable();
                    break;
                case "animated-rotate":
                    drawable = new AnimatedRotateDrawable();
                    break;
                case "animation-list":
                    drawable = new AnimationDrawable();
                    break;
                case "inset":
                    drawable = new InsetDrawable();
                    break;
                case "bitmap":
                    drawable = new BitmapDrawable(r);
                    if (r != null) {
                        ((BitmapDrawable) drawable).setTargetDensity(r.getDisplayMetrics());
                    }
                    break;
                case "nine-patch":
                    drawable = new NinePatchDrawable();
                    if (r != null) {
                        ((NinePatchDrawable) drawable).setTargetDensity(r.getDisplayMetrics());
                    }
                    break;
                default:
                    throw new XmlPullParserException(parser.getPositionDescription() +
                            ": invalid drawable tag " + name);
    
            }
            drawable.inflate(r, parser, attrs, theme);
            return drawable;
        }
    
    
        /**
         * Create a drawable from file path name.
         */
        public static Drawable createFromPath(String pathName) {
            if (pathName == null) {
                return null;
            }
    
            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, pathName);
            try {
                Bitmap bm = BitmapFactory.decodeFile(pathName);
                if (bm != null) {
                    return drawableFromBitmap(null, bm, null, null, null, pathName);
                }
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
            }
    
            return null;
        }
    
    
    (二)从XML文件中加载Drawable实例 的方法
         /**
         * Inflate this Drawable from an XML resource optionally styled by a theme.
         * Drawable实例接受主题设置的风格
         * @param r Resources used to resolve attribute values
         * @param parser XML parser from which to inflate this Drawable
         * @param attrs Base set of attribute values
         * @param theme Theme to apply, may be null
         * @throws XmlPullParserException
         * @throws IOException
         */
        public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
                throws XmlPullParserException, IOException {
            final TypedArray a;
            if (theme != null) {
                a = theme.obtainStyledAttributes(
                        attrs, com.android.internal.R.styleable.Drawable, 0, 0);
            } else {
                a = r.obtainAttributes(attrs, com.android.internal.R.styleable.Drawable);
            }
    
            inflateWithAttributes(r, parser, a, com.android.internal.R.styleable.Drawable_visible);
            a.recycle();
        }
    
        /**
         * Inflate a Drawable from an XML resource.
         *
         * @throws XmlPullParserException
         * @throws IOException
         */
        void inflateWithAttributes(Resources r, XmlPullParser parser, TypedArray attrs, int visibleAttr)
                throws XmlPullParserException, IOException {
            mVisible = attrs.getBoolean(visibleAttr, mVisible);
        }
    
    (三)ConstantState
         /**
         *ConstantState这个抽象类被用于存储 多个Drawable实例间 共享的 常量状态值及数据。
        如从同一个图片资源创建的多个BitmapDrawable实例,它们将共享
        同一个存储在它们的ConstantState中的Bitmap。
         * This abstract class is used by {@link Drawable}s to store shared constant state and data
         * between Drawables. {@link BitmapDrawable}s created from the same resource will for instance
         * share a unique bitmap stored in their ConstantState.
         *
         * <p>
         * {@link #newDrawable(Resources)} can be used as a factory to create new Drawable instances
         * from this ConstantState.
         * </p>
         *Drawable#getConstantState可以获取一个Drawable关联的ConstantState。
        调用Drawable#mutate(),则将为新创建的Drawable实例单独关联一个ConstantState。
         * Use {@link Drawable#getConstantState()} to retrieve the ConstantState of a Drawable. Calling
         * {@link Drawable#mutate()} on a Drawable should typically create a new ConstantState for that
         * Drawable.
         */
        public static abstract class ConstantState {
             /**
            运用ConstantState创建一个新的Drawable实例
             */
            public abstract Drawable newDrawable();
    
             /**
             运用ConstantState创建一个新的Drawable实例
             */
            public Drawable newDrawable(Resources res) {
                return newDrawable();
            }
    
            /**
             * Create a new Drawable instance from its constant state. This must be
             * implemented for drawables that can have a theme applied.
             */
            public Drawable newDrawable(Resources res, Theme theme) {
                return newDrawable(null);
            }
    
            /**
            返回会影响Drawable实例的一个bit掩码变化设置
             */
            public abstract int getChangingConfigurations();
    
            /**
             *返回所有的像素数
             */
            public int addAtlasableBitmaps(Collection<Bitmap> atlasList) {
                return 0;
            }
    
            /** @hide */
            protected final boolean isAtlasable(Bitmap bitmap) {
                return bitmap != null && bitmap.getConfig() == Bitmap.Config.ARGB_8888;
            }
    
            /**
             * 返回当前共享状态是否可以设置主题
             */
            public boolean canApplyTheme() {
                return false;
            }
        }
    
    (四)onBoundsChange

    图片不断的绘制呈现过程中变化的回调方法。

      /**
         * Override this in your subclass to change appearance if you vary based on
         * the bounds.
         */
        protected void onBoundsChange(Rect bounds) {
        }
    
    (五)updateTintFilter

    确保色彩过滤器和当前色彩与色彩模式一致

      
        @Nullable PorterDuffColorFilter updateTintFilter(@Nullable PorterDuffColorFilter tintFilter,
                @Nullable ColorStateList tint, @Nullable PorterDuff.Mode tintMode) {
            if (tint == null || tintMode == null) {
                return null;
            }
            final int color = tint.getColorForState(getState(), Color.TRANSPARENT);
            if (tintFilter == null) {
                return new PorterDuffColorFilter(color, tintMode);
            }
            tintFilter.setColor(color);
            tintFilter.setMode(tintMode);
            return tintFilter;
        }
    
    (六)setLevel

    设置自定义Drawable绘制的渐变的程度 , 图片渐变的范围 0-10000

     /**
         * Specify the level for the drawable.  This allows a drawable to vary its
         * imagery based on a continuous controller, for example to show progress
         * or volume level.
         *
         * <p>If the new level you are supplying causes the appearance of the
         * Drawable to change, then it is responsible for calling
         * {@link #invalidateSelf} in order to have itself redrawn, <em>and</em>
         * true will be returned from this function.
         *
         * @param level The new level, from 0 (minimum) to 10000 (maximum).
         *
         * @return Returns true if this change in level has caused the appearance
         * of the Drawable to change (hence requiring an invalidate), otherwise
         * returns false.
         */
        public final boolean setLevel(int level) {
            if (mLevel != level) {
                mLevel = level;
                return onLevelChange(level);
            }
            return false;
        }
    
    二、 Drawable绘制流程

    Drawable实例到底是如何被绘制到屏幕上面?

    1 通过Resource获取Drawable实例

    (.9图返回1个NinePatchDrawable实例,普通图片返回1个BitmapDrawable实例。)

    2 将获取的Drawable实例当做背景设置给View

    最常用写法:targetView.setBackgroundDrawable(Drawable bg)

    public class View implements Drawable.Callback, KeyEvent.Callback,
            AccessibilityEventSource {
        ****
        public void setBackgroundDrawable(Drawable background) {
            ****
            if (background == mBackground) {
                //如果当前背景和background相同,直接return
                return;
            }
            boolean requestLayout = false;
            mBackgroundResource = 0;
            if (mBackground != null) {
                if (isAttachedToWindow()) {
                    //如果当前View实例已经被绘制到屏幕上,则首先取消
                    //该View实例原始背景Drawable的动画
                    mBackground.setVisible(false, false);
                }
                //移除该View实例原始背景Drawable的动画监听接口
                mBackground.setCallback(null);
                //取消该View实例原始背景Drawable的所有事件
                unscheduleDrawable(mBackground);
            }
            if (background != null) {
                ****
                //设置background的布局方向和View实例一致,
                //Drawable.setLayoutDirection见上一篇文章
                background.setLayoutDirection(getLayoutDirection());
                if (background.getPadding(padding)) {
                    //如果Drawable实例background有padding
                    resetResolvedPaddingInternal();
                    switch (background.getLayoutDirection()) {
                        case LAYOUT_DIRECTION_RTL:
                            //布局方向从右至左
                            mUserPaddingLeftInitial = padding.right;
                            mUserPaddingRightInitial = padding.left;
                            internalSetPadding(padding.right, padding.top, padding.left, padding.bottom);
                            break;
                        case LAYOUT_DIRECTION_LTR:
                        default:
                            //布局方向从左至右
                            mUserPaddingLeftInitial = padding.left;
                            mUserPaddingRightInitial = padding.right;
                            //internalSetPadding会将四个参数值和View实例的padding进行比对,若不同则会重新布局+重建View的外部轮廓
                            internalSetPadding(padding.left, padding.top, padding.right, padding.bottom);
                    }
                    mLeftPaddingDefined = false;
                    mRightPaddingDefined = false;
                }
                if (mBackground == null
                        || mBackground.getMinimumHeight() != background.getMinimumHeight()
                        || mBackground.getMinimumWidth() != background.getMinimumWidth()) {
                    requestLayout = true;
                }
                //设置当前View实例的背景为传入的Drawable实例 background
                mBackground = background;
                if (background.isStateful()) {
                    //如果background会根据状态值变更外观,则设置其状态为
                    //当前View实例的state
                    background.setState(getDrawableState());
                }
                if (isAttachedToWindow()) {
                    //如果当前View实例已经被绘制到屏幕上
                    //且实例和实例的父控件及递归获得的根布局都处于可见状态,
                    //则设置background开启动画效果
                    background.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
                }
                applyBackgroundTint();
                //设置background动画接口监听为View实例本身(View实现了 Drawable.Callback):
                //public class View implements Drawable.Callback
                background.setCallback(this);
                if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
                    mPrivateFlags &= ~PFLAG_SKIP_DRAW;
                    //需要重新布局
                    requestLayout = true;
                }
            } else {
                mBackground = null;
                if ((mViewFlags & WILL_NOT_DRAW) != 0
                        && (mForegroundInfo == null || mForegroundInfo.mDrawable == null)) {
                    mPrivateFlags |= PFLAG_SKIP_DRAW;
                }
                requestLayout = true;
            }
            computeOpaqueFlags();
            if (requestLayout) {
                //重新布局
                requestLayout();
            }
            mBackgroundSizeChanged = true;
            //重绘View实例
            invalidate(true);
            //重建View实例的外部轮廓
            invalidateOutline();
        }
    }
    

    由此可见,
    1:setBackgroundDrawable方法,调用了Drawable的一系列方法,设置了Drawable实例一系列属性值,最终引发了View实例的重新布局(requestLayout()),重绘(invalidate(true))及重建View实例的外部轮廓(invalidateOutline())。
    2:在View实例重绘过程的第一步,将得到的Drawable实例(View实例的背景)绘制到屏幕上,实质是调用了Drawable.draw(@NonNull Canvas canvas)。
    3:Drawable.draw本身是个抽象方法,绘制具体逻辑由其子类实现。
    我们以之前获得的BitmapDrawable为例进行分析:。
    最终调用了Canvas.drawBitmap方法,将Drawable实例中的bitmap绘制到View实例关联的画布上。

    三、Demo
    原理
    通过level变量因子实现颜色变化

    0---左边全灰
    10000---全灰
    5000---蓝色

    5000---0混合颜色状态
    5000-10000混合颜色状态

    重点抠出灰色区域(宽度?--》level)

    看代码

    **
     * @创建 HaiJia
     * @时间 2017/3/11 11:42
     * @描述 自定义渐变View
     */
    
    public class ReveaView extends Drawable{
    
        private Drawable unSelectedDrawble;
        private Drawable seletedDrawable;
        private Rect outRect = new Rect();
    
    
        public ReveaView(Drawable unSelectedDrawble,Drawable seletedDrawable){
            super();
            this.unSelectedDrawble = unSelectedDrawble;
            this.seletedDrawable = seletedDrawable;
        }
    
        //onDraw onMeasure onLayout
    
    
        @Override
        public void draw(Canvas canvas) {
            int level = getLevel();
            if(level ==0 || level ==10000){//绘制全灰的图片
                unSelectedDrawble.draw(canvas);
            }else if(level == 5000){//绘制全蓝图片
                seletedDrawable.draw(canvas);
            }else{//混合颜色图片
                //渐变 先绘制左边的区域(从灰色图片抠出左边+从彩色区域抠出右边
                Rect bounds = getBounds();
                {//绘制左边
                    float ratio = level/5000f - 1f;
                    int width = bounds.width();
                    int height = bounds.height();
                    width = (int) (width*Math.abs(ratio));
                    int gravity = ratio<0?Gravity.LEFT:Gravity.RIGHT;
                    //进行抠的动作
                    Gravity.apply(gravity,width,height,
                            bounds,//在没有扣之前矩形区域
                            outRect);//目标矩形区域
                    canvas.save();//保存canvas
                    canvas.clipRect(outRect);
                    unSelectedDrawble.draw(canvas);
                    canvas.restore();//进行还原
    
                }
    
    
                {
                    //右边
                    float ratio = level/5000f - 1f;
                    int width = bounds.width();
                    int height = bounds.height();
                    width = (int) (width-width*Math.abs(ratio));
                    int gravity = ratio<0?Gravity.LEFT:Gravity.RIGHT;
                    //进行抠的动作
                    Gravity.apply(gravity,width,height,
                            bounds,//在没有扣之前矩形区域
                            outRect);//目标矩形区域
                    canvas.save();//保存canvas
                    canvas.clipRect(outRect);
                    seletedDrawable.draw(canvas);
                    canvas.restore();//进行还原
    
                }
    
            }
        }
    
        @Override
        public void setAlpha(int i) {
            //设置p
        }
    
        @Override
        public void setColorFilter(ColorFilter colorFilter) {
    
        }
    
        @Override
        public int getOpacity() {
            return 0;
        }
    
        //呈现图片 回调
        @Override
        protected void onBoundsChange(Rect bounds) {
            super.onBoundsChange(bounds);
            //图片动的时候,选择区域也在进行变化
            //确定好drawable边距,也就是bounds
            //
            unSelectedDrawble.setBounds(bounds);
            seletedDrawable.setBounds(bounds);
        }
    
        @Override
        protected boolean onLevelChange(int level) {
            //Level 改变的时候不断刷新它本身,所以设为返回true
            return true;
        }
    
        //图片的宽高
        @Override
        public int getIntrinsicWidth() {
            return unSelectedDrawble.getIntrinsicWidth();
        }
    
        @Override
        public int getIntrinsicHeight() {
            return unSelectedDrawble.getIntrinsicHeight();
        }
    }
    
    

    相关文章

      网友评论

          本文标题:Drawable源码分析与自定义Drawable Demo

          本文链接:https://www.haomeiwen.com/subject/qmzggttx.html