Android里,我们经常会用shape去定义View的形状。如下是在xml里定义一个简单shape的代码:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<solid android:color="#00FF00" />
<corners
android:bottomLeftRadius="10dp"
android:bottomRightRadius="10dp"
android:topLeftRadius="10dp"
android:topRightRadius="10dp" />
</shape>
使用时,将它设置在view 的背景上,有的同学这样问,如下使用shape,为什么不起作用?
第一例, 不起作用:
<ImageView
android:id="@+id/img_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/round_corner_rectangle"
android:scaleType="fitXY"
android:src="@drawable/img"/>
第二例,不起作用,看不到圆角效果
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/round_corner_rectangle">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</FrameLayout>
第三例,TextView 有圆角,正常
<TextView
android:background="@drawable/round_corner_rectangle"
android:layout_height="wrap_content"
android:layout_width="wrap_content" />
首先,shape是什么?
以圆角矩形<shape>为例,其中 shape 标签在解析后对应于 GradientDrawable类(注意不是ShapeDrawable),即在xml里定义<shape>,运行期间会生成对应的GradientDrawable对象,同时传入xml里定义的圆角属性值。
查看GradientDrawable 源码,将看到在xml里<shape>设定的各个角圆角弧度,被传入并保存在数组mRadiusArray:
private void updateDrawableCorners(TypedArray a) {
......
setCornerRadii(new float[] {
topLeftRadius, topLeftRadius,
topRightRadius, topRightRadius,
bottomRightRadius, bottomRightRadius,
bottomLeftRadius, bottomLeftRadius
});
}
所以,设定shape标签即设生成drawable 对象。
- Drawable 可以理解为:二维平面上,能画出来的图形图像,如:BitmapDrawable, ShapeDrawable, PictureDrawable, LayerDrawable, 等等派生类。Drawable 都有自己的draw() 方法,来操纵 canvas
- canvas 画布是透明的,可以在上面涂抹任意形状,并填充上颜色、渐变等,即 Drawable
继续查看GradientDrawable源码,其绘制过程是基本图形绘制,涉及:Canvas、Path、 Paint。其中path 定义封闭形状,并设定好圆角,paint 画笔设置颜色等,最终在canvas 画布上画出图形,步骤如下:
- path定义封闭形状代码如下:
private void buildPathIfDirty() {
final GradientState st = mGradientState;
if (mPathIsDirty) {
ensureValidRect();
mPath.reset();
mPath.addRoundRect(mRect, st.mRadiusArray, Path.Direction.CW);
mPathIsDirty = false;
}
}
- 画线及填充,558行:
switch (st.mShape) {
case RECTANGLE:
if (st.mRadiusArray != null) {
buildPathIfDirty();
// 画线及填充
canvas.drawPath(mPath, mFillPaint);
if (haveStroke) {
// 描边
canvas.drawPath(mPath, mStrokePaint);
}
}
以上分析了定义一个 圆角矩形时,GradientDrawable 将在 canvas上自我绘制的过程。
View设置各种drawable为背景,怎么起作用的?
以第三例为例,设置TextView的background,先了解以下基础:
- TextView 继承自View基类
- 设置各种背景都将转化为drawable对象
- View 里有一个公用画布 canvas
查看View源码,View 里背景和内容的绘制步骤:
- 首先绘制底部 background
- 绘制具体的内容,通过onDraw 通知继承View类子类绘制具体内容。
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) 的源码及注释 16153 行:
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 接着,绘制内容,dispatchDraw 通知该 View 上的子结点进行自我绘制。
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
绘制Background 的过程,简化一下即为drawable 直接调用自身 draw 方法,在同一画布上进行绘制。
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
…
background.draw(canvas);
}
以上分析解释了View绘制背景和内容的区别,同时,也顺便可以解释Imageview 的 background 和 src 的不同之处:
- 本质上无区别,都是各种不同类型的drawable,本质上都通过自身的draw方法在canvas上绘制。
- background 是背景,首先会在View基类的draw里被绘制。src 是内容,随后在子类ImageView的 ondraw 里被绘制。
这里验证一下,如果将 android:background=“@null” 会发生什么
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
android:text="此处背景透明"
android:layout_alignParentBottom="true"/>
会发现,将会取和设置 transparent 也是一样透明的效果。
将会看到,是和设置背景为 transparent 一样透明的效果。
回头来看文中开头处提到的shape不起作用的例子:
第二例
其中TextView为外部 FrameLayout 的子结点,外部FrameLayout设置的的标签与TextView无关,TextView的绘制范围仅宽高受FrameLayout的影响,标签只代表了一个图像,不影响子节点。
解决方法:
FrameLayout 设置padding, 或者TextView设置 margin,padding要大于等于sqrt(r),其中r为所设圆角半径值,并且两者背景颜色一致。为何为sqrt(r),请自行画图计算。
第一例
ImageView 设置圆角为何不起作用。参见 ImageView 里源码,src 对应 mDrawable,绘制时,将覆盖底层 background,即设置了圆角的drawable。
private void updateDrawable(Drawable d) {
……
mDrawable = d;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
…
mDrawable.draw(canvas);
}
解决办法
那该怎么给ImageView 画圆角呢?办法是通过paint 的SRC_IN模式:
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
SRC_IN 模式设置后,将两个绘制的效果叠加后取交集后展现,比如:第一个绘制的是个圆形,第二个绘制的是个Bitmap,于是交集为圆形,就实现了圆形图片效果。
而且,android Tint 也是靠 SRC_IN 来自动变成我们想要的背景颜色,来达到Material Design的效果。
网友评论