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;
}
网友评论