美文网首页
XImageView-ShapeImageView处理Image

XImageView-ShapeImageView处理Image

作者: 码农朱同学 | 来源:发表于2019-01-19 19:00 被阅读0次

    ShapeImageView 处理ImageView形状,原形圆角等

    0. 源码地址

    https://github.com/zhxhcoder/XImageView

    1. 引用方法

    compile 'com.zhxh:ximageviewlib:1.2'
    

    2. 使用方法

    举个栗子:

                <com.zhxh.ximageviewlib.ShapeImageView
                    android:id="@+id/iv_avatar"
                    android:layout_width="40dp"
                    android:layout_height="40dp"
                    android:layout_centerVertical="true"
                    android:layout_marginStart="15dp"
                    android:layout_marginLeft="15dp"
                    android:src="@drawable/ic_test_750_360"
                    app:siv_border_color="@android:color/holo_purple"
                    app:siv_border_size="1px"
                    app:siv_round_radius="4dp"
                    app:siv_shape="rect"
                    tools:ignore="ContentDescription" />
    

    3. 源码实现

    3.1 属性定义与描述

        <declare-styleable name="ShapeImageView">
            <attr name="siv_shape" format="enum">
                <enum name="rect" value="1" />
                <enum name="circle" value="2" />
                <enum name="oval" value="3" />
            </attr>
            <attr name="siv_round_radius" format="dimension" />
            <attr name="siv_round_radius_leftTop" format="dimension" />
            <attr name="siv_round_radius_leftBottom" format="dimension" />
            <attr name="siv_round_radius_rightTop" format="dimension" />
            <attr name="siv_round_radius_rightBottom" format="dimension" />
            <attr name="siv_border_size" format="dimension" />
            <attr name="siv_border_color" format="color" />
        </declare-styleable>
    

    3.2 代码实现

    1,属性初始化
    从AttributeSet 中初始化相关属性

        private void init(AttributeSet attrs) {
            TypedArray a = getContext().obtainStyledAttributes(attrs,
                    R.styleable.ShapeImageView);
            mShape = a.getInt(R.styleable.ShapeImageView_siv_shape, mShape);
            mRoundRadius = a.getDimension(R.styleable.ShapeImageView_siv_round_radius, mRoundRadius);
            mBorderSize = a.getDimension(R.styleable.ShapeImageView_siv_border_size, mBorderSize);
            mBorderColor = a.getColor(R.styleable.ShapeImageView_siv_border_color, mBorderColor);
    
            mRoundRadiusLeftBottom = a.getDimension(R.styleable.ShapeImageView_siv_round_radius_leftBottom, mRoundRadius);
            mRoundRadiusLeftTop = a.getDimension(R.styleable.ShapeImageView_siv_round_radius_leftTop, mRoundRadius);
            mRoundRadiusRightBottom = a.getDimension(R.styleable.ShapeImageView_siv_round_radius_rightBottom, mRoundRadius);
            mRoundRadiusRightTop = a.getDimension(R.styleable.ShapeImageView_siv_round_radius_rightTop, mRoundRadius);
    
            a.recycle();
        }
    

    2,关键数据初始化

        private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
        private static final int COLORDRAWABLE_DIMENSION = 2;
        public static int SHAPE_REC = 1; // 矩形
        public static int SHAPE_CIRCLE = 2; // 圆形
        public static int SHAPE_OVAL = 3; // 椭圆
        private final Matrix mShaderMatrix = new Matrix();
        private float mBorderSize = 0; // 边框大小,默认为0,即无边框
        private int mBorderColor = Color.WHITE; // 边框颜色,默认为白色
        private int mShape = SHAPE_REC; // 形状,默认为直接矩形
        private float mRoundRadius = 0; // 矩形的圆角半径,默认为0,即直角矩形
    

    3,重新生成所需的Bitmap
    我们覆盖ImageView的setImageResource与setImageDrawable函数,对生成的drawable对象重新自定义

        @Override
        public void setImageResource(int resId) {
            super.setImageResource(resId);
            mBitmap = getBitmapFromDrawable(getDrawable());
            setupBitmapShader();
        }
    
        @Override
        public void setImageDrawable(Drawable drawable) {
            super.setImageDrawable(drawable);
            mBitmap = getBitmapFromDrawable(drawable);
            setupBitmapShader();
        }
    

    自定义所需的Bitmap对象

        private Bitmap getBitmapFromDrawable(Drawable drawable) {
            if (drawable == null) {
                return null;
            }
            if (drawable instanceof BitmapDrawable) {
                return ((BitmapDrawable) drawable).getBitmap();
            }
            try {
                Bitmap bitmap;
                if (drawable instanceof ColorDrawable) {
                    bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG);
                } else {
                    bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG);
                }
                Canvas canvas = new Canvas(bitmap);
                drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
                drawable.draw(canvas);
                return bitmap;
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        private void setupBitmapShader() {
            // super(context, attrs, defStyle)调用setImageDrawable时,成员变量还未被正确初始化
            if (mBitmapPaint == null) {
                return;
            }
            if (mBitmap == null) {
                invalidate();
                return;
            }
            mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
            mBitmapPaint.setShader(mBitmapShader);
    
            // 固定为CENTER_CROP,使图片在view中居中并裁剪
            mShaderMatrix.set(null);
            // 缩放到高或宽 与view的高或宽 匹配
            float scale = Math.max(getWidth() * 1f / mBitmap.getWidth(), getHeight() * 1f / mBitmap.getHeight());
            // 由于BitmapShader默认是从画布的左上角开始绘制,所以把其平移到画布中间,即居中
            float dx = (getWidth() - mBitmap.getWidth() * scale) / 2;
            float dy = (getHeight() - mBitmap.getHeight() * scale) / 2;
            mShaderMatrix.setScale(scale, scale);
            mShaderMatrix.postTranslate(dx, dy);
            mBitmapShader.setLocalMatrix(mShaderMatrix);
            invalidate();
        }
    

    从上面代码我们看到,会调用 invalidate();重新绘制。
    该方法的作用是什么呢?
    我们进入 invalidate()方法:

    public void invalidate() {
        invalidate(true);
    }
    void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }
    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
        if (mGhostView != null) {
            mGhostView.invalidate(true);
            return;
        }
    
        //这里判断该子View是否可见或者是否处于动画中
        if (skipInvalidate()) {
            return;
        }
    
        //根据View的标记位来判断该子View是否需要重绘,假如View没有任何变化,那么就不需要重绘
        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
                || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
                || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
                || (fullInvalidate && isOpaque() != mLastIsOpaque)) {
            if (fullInvalidate) {
                mLastIsOpaque = isOpaque();
                mPrivateFlags &= ~PFLAG_DRAWN;
            }
    
            //设置PFLAG_DIRTY标记位
            mPrivateFlags |= PFLAG_DIRTY;
    
            if (invalidateCache) {
                mPrivateFlags |= PFLAG_INVALIDATED;
                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
            }
    
            // Propagate the damage rectangle to the parent view.
            //把需要重绘的区域传递给父容器
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                //调用父容器的方法,向上传递事件
                p.invalidateChild(this, damage);
            }
            ...
        }
    }
    

    从上面代码代码我们看到,该方法的调用会引起View树的重绘,常用于内部调用(比如 setVisiblity())或者需要刷新界面的时候,需要在主线程(即UI线程)中调用该方法。可以看出,invalidate有多个重载方法,但最终都会调用invalidateInternal方法,在这个方法内部,进行了一系列的判断,判断View是否需要重绘,接着为该View设置标记位,然后把需要重绘的区域传递给父容器,即调用父容器的invalidateChild方法。
    接着我们看ViewGroup#invalidateChild:

    /**
     * Don't call or override this method. It is used for the implementation of
     * the view hierarchy.
     */
    public final void invalidateChild(View child, final Rect dirty) {
    
        //设置 parent 等于自身
        ViewParent parent = this;
    
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            // If the child is drawing an animation, we want to copy this flag onto
            // ourselves and the parent to make sure the invalidate request goes
            // through
            final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION)
                    == PFLAG_DRAW_ANIMATION;
    
            // Check whether the child that requests the invalidate is fully opaque
            // Views being animated or transformed are not considered opaque because we may
            // be invalidating their old position and need the parent to paint behind them.
            Matrix childMatrix = child.getMatrix();
            final boolean isOpaque = child.isOpaque() && !drawAnimation &&
                    child.getAnimation() == null && childMatrix.isIdentity();
            // Mark the child as dirty, using the appropriate flag
            // Make sure we do not set both flags at the same time
            int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;
    
            if (child.mLayerType != LAYER_TYPE_NONE) {
                mPrivateFlags |= PFLAG_INVALIDATED;
                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
            }
    
            //储存子View的mLeft和mTop值
            final int[] location = attachInfo.mInvalidateChildLocation;
            location[CHILD_LEFT_INDEX] = child.mLeft;
            location[CHILD_TOP_INDEX] = child.mTop;
    
            ...
    
            do {
                View view = null;
                if (parent instanceof View) {
                    view = (View) parent;
                }
    
                if (drawAnimation) {
                    if (view != null) {
                        view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                    } else if (parent instanceof ViewRootImpl) {
                        ((ViewRootImpl) parent).mIsAnimating = true;
                    }
                }
    
                // If the parent is dirty opaque or not dirty, mark it dirty with the opaque
                // flag coming from the child that initiated the invalidate
                if (view != null) {
                    if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
                            view.getSolidColor() == 0) {
                        opaqueFlag = PFLAG_DIRTY;
                    }
                    if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
                        //对当前View的标记位进行设置
                        view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
                    }
                }
    
                //调用ViewGrup的invalidateChildInParent,如果已经达到最顶层view,则调用ViewRootImpl
                //的invalidateChildInParent。
                parent = parent.invalidateChildInParent(location, dirty);
    
                if (view != null) {
                    // Account for transform on current parent
                    Matrix m = view.getMatrix();
                    if (!m.isIdentity()) {
                        RectF boundingRect = attachInfo.mTmpTransformRect;
                        boundingRect.set(dirty);
                        m.mapRect(boundingRect);
                        dirty.set((int) (boundingRect.left - 0.5f),
                                (int) (boundingRect.top - 0.5f),
                                (int) (boundingRect.right + 0.5f),
                                (int) (boundingRect.bottom + 0.5f));
                    }
                }
            } while (parent != null);
        }
    }
    
    

    可以看到,在该方法内部,先设置当前视图的标记位,接着有一个do…while…循环,该循环的作用主要是不断向上回溯父容器,求得父容器和子View需要重绘的区域的并集(dirty)。当父容器不是ViewRootImpl的时候,调用的是ViewGroup的invalidateChildInParent方法,我们来看看这个方法,ViewGroup#invalidateChildInParent:

    public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
        if ((mPrivateFlags & PFLAG_DRAWN) == PFLAG_DRAWN ||
                (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) {
            if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) !=
                        FLAG_OPTIMIZE_INVALIDATE) {
    
                //将dirty中的坐标转化为父容器中的坐标,考虑mScrollX和mScrollY的影响
                dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
                        location[CHILD_TOP_INDEX] - mScrollY);
    
                if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
                    //求并集,结果是把子视图的dirty区域转化为父容器的dirty区域
                    dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
                }
    
                final int left = mLeft;
                final int top = mTop;
    
                if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
                    if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
                        dirty.setEmpty();
                    }
                }
                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
    
                //记录当前视图的mLeft和mTop值,在下一次循环中会把当前值再向父容器的坐标转化
                location[CHILD_LEFT_INDEX] = left;
                location[CHILD_TOP_INDEX] = top;
    
                if (mLayerType != LAYER_TYPE_NONE) {
                    mPrivateFlags |= PFLAG_INVALIDATED;
                }
                //返回当前视图的父容器
                return mParent;
    
            }
            ...
        }
        return null;
    }
    

    可以看出,这个方法做的工作主要有:调用offset方法,把当前dirty区域的坐标转化为父容器中的坐标,接着调用union方法,把子dirty区域与父容器的区域求并集,换句话说,dirty区域变成父容器区域。最后返回当前视图的父容器,以便进行下一次循环。

    回到上面所说的do…while…循环,由于不断向上调用父容器的方法,到最后会调用到ViewRootImpl的invalidateChildInParent方法,我们来看看它的源码,ViewRootImpl#invalidateChildInParent:

    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();
        if (DEBUG_DRAW) Log.v(TAG, "Invalidate child: " + dirty);
    
        if (dirty == null) {
            invalidate();
            return null;
        } else if (dirty.isEmpty() && !mIsAnimating) {
            return null;
        }
    
        if (mCurScrollY != 0 || mTranslator != null) {
            mTempRect.set(dirty);
            dirty = mTempRect;
            if (mCurScrollY != 0) {
                dirty.offset(0, -mCurScrollY);
            }
            if (mTranslator != null) {
                mTranslator.translateRectInAppWindowToScreen(dirty);
            }
            if (mAttachInfo.mScalingRequired) {
                dirty.inset(-1, -1);
            }
        }
    
        final Rect localDirty = mDirty;
        if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
            mAttachInfo.mSetIgnoreDirtyState = true;
            mAttachInfo.mIgnoreDirtyState = true;
        }
    
        // Add the new dirty rect to the current one
        localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
        // Intersect with the bounds of the window to skip
        // updates that lie outside of the visible region
        final float appScale = mAttachInfo.mApplicationScale;
        final boolean intersected = localDirty.intersect(0, 0,
                (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
        if (!intersected) {
            localDirty.setEmpty();
        }
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            scheduleTraversals();
        }
        return null;
    }
    

    可以看出,该方法所做的工作与上面的差不多,都进行了offset和union对坐标的调整,然后把dirty区域的信息保存在mDirty中,最后调用了scheduleTraversals方法,触发View的工作流程,由于没有添加measure和layout的标记位,因此measure、layout流程不会执行,而是直接从draw流程开始。

    总结一下invalidate方法,当子View调用了invalidate方法后,会为该View添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递到ViewRootImpl中,最终触发performTraversals方法,进行开始View树重绘流程(只绘制需要重绘的视图)。

    回到ShapeImageView代码中,因为我们改变图片的形状不仅改变了大小,图片也相应变化,所以:

        /**
         * 对于普通的view,在执行到onDraw()时,背景图已绘制完成
         * <p>
         * 对于ViewGroup,当它没有背景时直接调用的是dispatchDraw()方法, 而绕过了draw()方法,
         * 当它有背景的时候就调用draw()方法,而draw()方法里包含了dispatchDraw()方法的调用,
         */
        @Override
        public void onDraw(Canvas canvas) {
    
            if (mBitmap != null) {
                if (mShape == SHAPE_CIRCLE) {
                    canvas.drawCircle(mViewRect.right / 2, mViewRect.bottom / 2,
                            Math.min(mViewRect.right, mViewRect.bottom) / 2, mBitmapPaint);
                } else if (mShape == SHAPE_OVAL) {
                    canvas.drawOval(mViewRect, mBitmapPaint);
                } else {
    //                canvas.drawRoundRect(mViewRect, mRoundRadius, mRoundRadius, mBitmapPaint);
                    mPath.reset();
                    mPath.addRoundRect(mViewRect, new float[]{
                            mRoundRadiusLeftTop, mRoundRadiusLeftTop,
                            mRoundRadiusRightTop, mRoundRadiusRightTop,
                            mRoundRadiusRightBottom, mRoundRadiusRightBottom,
                            mRoundRadiusLeftBottom, mRoundRadiusLeftBottom,
                    }, Path.Direction.CW);
                    canvas.drawPath(mPath, mBitmapPaint);
    
                }
            }
    
            if (mBorderSize > 0) { // 绘制边框
                if (mShape == SHAPE_CIRCLE) {
                    canvas.drawCircle(mViewRect.right / 2, mViewRect.bottom / 2,
                            Math.min(mViewRect.right, mViewRect.bottom) / 2 - mBorderSize / 2, mBorderPaint);
                } else if (mShape == SHAPE_OVAL) {
                    canvas.drawOval(mBorderRect, mBorderPaint);
                } else {
    //                canvas.drawRoundRect(mBorderRect, mRoundRadius, mRoundRadius, mBorderPaint);
                    mPath.reset();
                    mPath.addRoundRect(mBorderRect, new float[]{
                            mRoundRadiusLeftTop, mRoundRadiusLeftTop,
                            mRoundRadiusRightTop, mRoundRadiusRightTop,
                            mRoundRadiusRightBottom, mRoundRadiusRightBottom,
                            mRoundRadiusLeftBottom, mRoundRadiusLeftBottom,
                    }, Path.Direction.CW);
                    canvas.drawPath(mPath, mBorderPaint);
                }
            }
        }
    

    另外需要注意的是,onSizeChanged回调,
    因为View的大小不仅由View本身控制,而且受父控件的影响,所以我们在确定View大小的时候最好使用系统提供的onSizeChanged回调函数。它又四个参数,分别为 宽度,高度,上一次宽度,上一次高度。这个函数比较简单,我们只需关注 宽度(w), 高度(h) 即可,这两个参数就是View最终的大小。
    它一般是视图大小发生变化的时候回调了,那么具体看源码是在layout的过程中出发的
    在layout方法中会调用setFrame方法,在setFrame方法中又调用了sizeChange,在该方法里面回调了onSizeChanged,然后才去回调onLayout过程

        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            initRect();
            setupBitmapShader();
        }
    

    initRect()方法用来设置图片的绘制区域

        // 设置图片的绘制区域
        private void initRect() {
    
            mViewRect.top = 0;
            mViewRect.left = 0;
            mViewRect.right = getWidth(); // 宽度
            mViewRect.bottom = getHeight(); // 高度
    
            // 边框的矩形区域不能等于ImageView的矩形区域,否则边框的宽度只显示了一半
            mBorderRect.top = mBorderSize / 2;
            mBorderRect.left = mBorderSize / 2;
            mBorderRect.right = getWidth() - mBorderSize / 2;
            mBorderRect.bottom = getHeight() - mBorderSize / 2;
        }
    

    相关文章

      网友评论

          本文标题:XImageView-ShapeImageView处理Image

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