前言:
通常我们实现某一特殊功能的View控件的时候,都会在当前View上动刀子,比如想要实现一个圆角矩形边框的图片控件,好,在ImageView上动动手,改一个RoundImageView; 想要实现一个下拉刷新的列表控件,好,在ListView上动动刀,改一个RefreshListView.
以上思路不能说有问题,但动违反了任何事务应该抽象化的一般思想.问题不能只看眼前,要有更长远的规划.
近来研究Android 新增的一些控件,可以明显感觉到这些实现所带来的重大编程思想.
比如: RefreshLayout,将下拉刷新的逻辑抽象到一个布局中去,那么任何添加到这个布局中的控件都具有下拉刷新的行为.
CardView 可以让一切加入其中的View控件实现圆角化.而不仅仅局限于一张图片.
还有Metro的很多控件,都是基于这样一个思路–动刀子不要只顾着眼下,治病要治根.
正文
本文提供的一个新型控件是对CardView的一个改进, 用过CardView的同学应该都知道,它在低版本的手机上表示出来的缺陷–通过给内容增加padding只是背景圆角化了,内容仍然四四方方.
有没有办法实现在任意系统下都能够让内容圆角化的控件呢? 答案是肯定的.
思路是什么呢? 如果大家深入研究一下Canvas这个绘图画布就应该知道, 画布设置多大,内容就会显示多大. 另外这个画布还可以设置剪裁区.
剪裁区是个什么东东呢?剪裁区就是内容能够显示出来的窗口,剪裁区的形状也决定了内容最终显示出来的形状.例如我将剪裁区设置为一个圆形,那么画出来的内容,一定是个圆形,圆形以外的地方,则会被遮挡而没有绘制.
这就是我们今天要讲的这个控件的实现.
基于剪裁区的多边形布局实现.
不仅仅是圆角哦~你可以为它添加任意形状,圆形,椭圆形,圆角矩形,星形,5边形….形状随你控制.
下边直接贴出原码供大家参考:
package cn.andrewlu.app.customview;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.FrameLayout;
/**
* Created by Andrewlu on 2016/4/6.
* CardView 在低版本上内容与边框之间会有空隙.并非真正的圆角布局.
* RoundView用来构造一个真正的圆角布局.可选形状有: 圆角矩形, 圆,椭圆.矩形.矩形实际上是弧度为0的圆角矩形.
*/
public class RoundView extends FrameLayout {
private float mRadius = 0;
private float mStrokeWidth = 0;
private int mStrokeColor = Color.TRANSPARENT;
private Path mBoundPath = null;
private Type mType = Type.Rect;// 默认为 圆角矩形,即未声明type属性时,默认当成圆角矩形.
public RoundView(Context context) {
this(context, null);
}
public RoundView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
/**
*通过自定义属性,设置圆角大小,形状,边框宽度,边框颜色等.参考CardView的自定义属性.
**/
public RoundView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setWillNotDraw(false);
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.RoundView);
mRadius = a.getDimension(R.styleable.RoundView_radius, mRadius);
mStrokeWidth = a.getDimension(R.styleable.RoundView_strokeWidth,
mStrokeWidth);
mStrokeColor = a.getColor(R.styleable.RoundView_strokeColor,
mStrokeColor);
int shape = a.getInt(R.styleable.RoundView_shape, mType.getType());
mType = Type.from(shape);
a.recycle();
}
public void setRadius(float radius) {
if (mRadius == radius)
return;
this.mRadius = radius;
postInvalidate();
}
public float getRadius() {
return mRadius;
}
public void setStrokeWidth(float strok) {
this.mStrokeWidth = strok;
postInvalidate();
}
public float getStrokeWidth() {
return mStrokeWidth;
}
public void setStrokeColor(int strokeColor) {
this.mStrokeColor = strokeColor;
}
public int getStrokeColor() {
return mStrokeColor;
}
//draw()函数是任何View在绘制自己前都会被调用的方法,通过在绘制自己前设置裁剪区,可以实现任意形状的布局.
public void draw(Canvas canvas) {
beforeDraw(canvas);
super.draw(canvas);
}
//dispatchDraw是任何布局在内容绘制结束时都会被调用的方法.可以做一些其他操作,如描边等.
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
afterDraw(canvas);
}
//在绘制前计算出可以剪裁的矩形区域,并根据矩形区域设置自己的形状Path.
private void beforeDraw(Canvas canvas) {
Rect rect = new Rect();
getLocalVisibleRect(rect);
mBoundPath = onCaculatePath(rect);
canvas.clipPath(mBoundPath);
Log.i("RoundView", "beforeDraw");
}
//绘制结束后,可以给形状进行描边操作.
private void afterDraw(Canvas canvas) {
Rect rect = new Rect();
getLocalVisibleRect(rect);
// 进行描边操作.
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setStyle(Paint.Style.STROKE);
p.setColor(mStrokeColor);
p.setStrokeWidth(mStrokeWidth);
Path path = onGetPathStroke(rect, mBoundPath);
if (path == null)
return;
canvas.drawPath(path, p);
}
protected Path onCaculatePath(Rect r) {
switch (mType) {
case Rect:
return caculateRoundRectPath(r);
case Circle:
return caculateCirclePath(r);
case Oval:
return caculateOvalPath(r);
}
return caculateRoundRectPath(r);
}
protected Path onGetPathStroke(Rect r, Path boundPath) {
switch (mType) {
case Circle:
return getCirclePathWithinStroke(r, boundPath);
default:
return getPathWithinStroke(r, boundPath);
}
}
// 将path 进行变换,以容纳描边线宽.
private Path getPathWithinStroke(Rect r, Path path) {
if (mStrokeWidth <= 0)
return path;
// 防止边过宽,完全遮挡内容.
int minWidth = r.width() > r.height() ? r.height() : r.width();
if (minWidth <= 0)
return null;
if (mStrokeWidth >= minWidth / 2)
mStrokeWidth = minWidth / 2.5f;
Path p = new Path();
Matrix matrix = new Matrix();
float scaleX = (r.width() - mStrokeWidth / 2) / r.width();
float scaleY = (r.height() - mStrokeWidth / 2) / r.height();
matrix.setScale(scaleX, scaleY, r.centerX(), r.centerY());
path.transform(matrix, p);
return p;
}
private Path getCirclePathWithinStroke(Rect r, Path path) {
if (mStrokeWidth <= 0)
return path;
// 防止边过宽,完全遮挡内容.
int minWidth = r.width() > r.height() ? r.height() : r.width();
if (minWidth <= 0)
return null;
if (mStrokeWidth >= minWidth / 2)
mStrokeWidth = minWidth / 2.5f;
Path p = new Path();
Matrix matrix = new Matrix();
float scale = (minWidth - mStrokeWidth / 2) / minWidth;
matrix.setScale(scale, scale, r.centerX(), r.centerY());
path.transform(matrix, p);
return p;
}
// 以下方法留做备用,可用于生产各种外形的边框.仅供参考.
private Path caculateRoundRectPath(Rect r) {
Path path = new Path();
float radius = getRadius();
float elevation = 0;
path.addRoundRect(new RectF(r.left + elevation, r.top + elevation,
r.right - elevation, r.bottom - elevation), radius, radius,
Path.Direction.CW);
return path;
}
private Path caculateCirclePath(Rect r) {
Path path = new Path();
int radius = r.width() > r.height() ? r.height() / 2 : r.width() / 2;
path.addCircle(r.left + radius, r.top + radius, radius,
Path.Direction.CW);
return path;
}
private Path caculateOvalPath(Rect r) {
Path path = new Path();
path.addOval(new RectF(r), Path.Direction.CW);
return path;
}
public enum Type {
Rect(0), Circle(1), Oval(2);
private int type;
Type(int type) {
this.type = type;
}
public int getType() {
return this.type;
}
public static Type from(int type) {
switch (type) {
case 0:
return Rect;
case 1:
return Circle;
case 2:
return Oval;
default:
return Rect;
}
}
}
}