public class CircleLayout extends ViewGroup {
public static final int LAYOUT_NORMAL = 1;
public static final int LAYOUT_PIE = 2;
private int mLayoutMode = LAYOUT_NORMAL;
private Drawable mInnerCircle;
private float mAngleOffset;
private float mAngleRange;
private float mDividerWidth;
private int mInnerRadius;
private Paint mDividerPaint;
private Paint mCirclePaint;
private RectF mBounds = new RectF();
private Bitmap mDst;
private Bitmap mSrc;
private Canvas mSrcCanvas;
private Canvas mDstCanvas;
private Xfermode mXfer;
private Paint mXferPaint;
private View mMotionTarget;
private Bitmap mDrawingCache;
private Canvas mCachedCanvas;
private Set<View> mDirtyViews = new HashSet<View>();
private boolean mCached = false;
public CircleLayout(Context context) {
this(context, null);
}
@SuppressLint("NewApi")
public CircleLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mDividerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.CircleLayout, 0, 0);
try {
int dividerColor = a.getColor(
R.styleable.CircleLayout_sliceDivider,
getResources().getColor(android.R.color.darker_gray));
mInnerCircle = a.getDrawable(R.styleable.CircleLayout_innerCircle);
if (mInnerCircle instanceof ColorDrawable) {
int innerColor = a.getColor(
R.styleable.CircleLayout_innerCircle,
getResources().getColor(android.R.color.white));
mCirclePaint.setColor(innerColor);
}
mDividerPaint.setColor(dividerColor);
mAngleOffset = a
.getFloat(R.styleable.CircleLayout_angleOffset, 90f);
mAngleRange = a.getFloat(R.styleable.CircleLayout_angleRange, 360f);
mDividerWidth = a.getDimensionPixelSize(
R.styleable.CircleLayout_dividerWidth, 1);
mInnerRadius = a.getDimensionPixelSize(
R.styleable.CircleLayout_innerRadius, 80);
mLayoutMode = a.getColor(R.styleable.CircleLayout_layoutMode,
LAYOUT_NORMAL);
} finally {
a.recycle();
}
mDividerPaint.setStrokeWidth(mDividerWidth);
mXfer = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
mXferPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// Turn off hardware acceleration if possible
if (Build.VERSION.SDK_INT >= 11) {
setLayerType(LAYER_TYPE_SOFTWARE, null);
}
}
public void setLayoutMode(int mode) {
mLayoutMode = mode;
requestLayout();
invalidate();
}
public int getLayoutMode() {
return mLayoutMode;
}
public int getRadius() {
final int width = getWidth();
final int height = getHeight();
final float minDimen = width > height ? height : width;
float radius = (minDimen - mInnerRadius) / 2f;
return (int) radius;
}
public void getCenter(PointF p) {
p.set(getWidth() / 2f, getHeight() / 2);
}
public void setAngleOffset(float offset) {
mAngleOffset = offset;
requestLayout();
invalidate();
}
public float getAngleOffset() {
return mAngleOffset;
}
public void setInnerRadius(int radius) {
mInnerRadius = radius;
requestLayout();
invalidate();
}
public int getInnerRadius() {
return mInnerRadius;
}
public void setInnerCircle(Drawable d) {
mInnerCircle = d;
requestLayout();
invalidate();
}
public void setInnerCircle(int res) {
mInnerCircle = getContext().getResources().getDrawable(res);
requestLayout();
invalidate();
}
public void setInnerCircleColor(int color) {
mInnerCircle = new ColorDrawable(color);
requestLayout();
invalidate();
}
public Drawable getInnerCircle() {
return mInnerCircle;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int count = getChildCount();
int maxHeight = 0;
int maxWidth = 0;
// Find rightmost and bottommost child
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
}
}
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
int width = resolveSize(maxWidth, widthMeasureSpec);
int height = resolveSize(maxHeight, heightMeasureSpec);
setMeasuredDimension(width, height);
if (mSrc != null
&& (mSrc.getWidth() != width || mSrc.getHeight() != height)) {
mDst.recycle();
mSrc.recycle();
mDrawingCache.recycle();
mDst = null;
mSrc = null;
mDrawingCache = null;
}
if (mSrc == null) {
mSrc = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mDst = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mDrawingCache = Bitmap.createBitmap(width, height,
Bitmap.Config.ARGB_8888);
mSrcCanvas = new Canvas(mSrc);
mDstCanvas = new Canvas(mDst);
mCachedCanvas = new Canvas(mDrawingCache);
}
}
private LayoutParams layoutParams(View child) {
return (LayoutParams) child.getLayoutParams();
}
@Override
@SuppressWarnings("deprecation")
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int childs = getChildCount();
float totalWeight = 0f;
for (int i = 0; i < childs; i++) {
final View child = getChildAt(i);
LayoutParams lp = layoutParams(child);
totalWeight += lp.weight;
}
final int width = getWidth();
final int height = getHeight();
final float minDimen = width > height ? height : width;
final float radius = (minDimen - mInnerRadius) / 2f;
mBounds.set(width / 2 - minDimen / 2, height / 2 - minDimen / 2, width
/ 2 + minDimen / 2, height / 2 + minDimen / 2);
float startAngle = mAngleOffset;
for (int i = 0; i < childs; i++) {
final View child = getChildAt(i);
final LayoutParams lp = layoutParams(child);
final float angle = mAngleRange / totalWeight * lp.weight;
final float centerAngle = startAngle + angle / 2f;
final int x;
final int y;
if (childs > 1) {
x = (int) (radius * Math.cos(Math.toRadians(centerAngle)))
+ width / 2;
y = (int) (radius * Math.sin(Math.toRadians(centerAngle)))
+ height / 2;
} else {
x = width / 2;
y = height / 2;
}
final int halfChildWidth = child.getMeasuredWidth() / 2;
final int halfChildHeight = child.getMeasuredHeight() / 2;
final int left = lp.width != LayoutParams.FILL_PARENT ? x
- halfChildWidth : 0;
final int top = lp.height != LayoutParams.FILL_PARENT ? y
- halfChildHeight : 0;
final int right = lp.width != LayoutParams.FILL_PARENT ? x
+ halfChildWidth : width;
final int bottom = lp.height != LayoutParams.FILL_PARENT ? y
+ halfChildHeight : height;
child.layout(left, top, right, bottom);
if (left != child.getLeft() || top != child.getTop()
|| right != child.getRight() || bottom != child.getBottom()
|| lp.startAngle != startAngle
|| lp.endAngle != startAngle + angle) {
mCached = false;
}
lp.startAngle = startAngle;
startAngle += angle;
lp.endAngle = startAngle;
}
invalidate();
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
LayoutParams lp = new LayoutParams(p.width, p.height);
if (p instanceof LinearLayout.LayoutParams) {
lp.weight = ((LinearLayout.LayoutParams) p).weight;
}
return lp;
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mLayoutMode == LAYOUT_NORMAL) {
return super.dispatchTouchEvent(ev);
}
final int action = ev.getAction();
final float x = ev.getX() - getWidth() / 2f;
final float y = ev.getY() - getHeight() / 2f;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
MotionEvent cancelEvent = MotionEvent.obtain(ev);
cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
cancelEvent.offsetLocation(-mMotionTarget.getLeft(),
-mMotionTarget.getTop());
mMotionTarget.dispatchTouchEvent(cancelEvent);
cancelEvent.recycle();
mMotionTarget = null;
}
final float radius = (float) Math.sqrt(x * x + y * y);
if (radius < mInnerRadius || radius > getWidth() / 2f
|| radius > getHeight() / 2f) {
return false;
}
float angle = (float) Math.toDegrees(Math.atan2(y, x));
if (angle < 0)
angle += mAngleRange;
final int childs = getChildCount();
for (int i = 0; i < childs; i++) {
final View child = getChildAt(i);
final LayoutParams lp = layoutParams(child);
float startAngle = lp.startAngle % mAngleRange;
float endAngle = lp.endAngle % mAngleRange;
float touchAngle = angle;
if (startAngle > endAngle) {
if (touchAngle < startAngle && touchAngle < endAngle) {
touchAngle += mAngleRange;
}
endAngle += mAngleRange;
}
if (startAngle <= touchAngle && endAngle >= touchAngle) {
ev.offsetLocation(-child.getLeft(), -child.getTop());
boolean dispatched = child.dispatchTouchEvent(ev);
if (dispatched) {
mMotionTarget = child;
return true;
} else {
ev.setLocation(0f, 0f);
return onTouchEvent(ev);
}
}
}
} else if (mMotionTarget != null) {
ev.offsetLocation(-mMotionTarget.getLeft(), -mMotionTarget.getTop());
mMotionTarget.dispatchTouchEvent(ev);
if (action == MotionEvent.ACTION_UP
|| action == MotionEvent.ACTION_CANCEL) {
mMotionTarget = null;
}
}
return onTouchEvent(ev);
}
private void drawChild(Canvas canvas, View child, LayoutParams lp) {
mSrcCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
mDstCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
mSrcCanvas.save();
int childLeft = child.getLeft();
int childTop = child.getTop();
int childRight = child.getRight();
int childBottom = child.getBottom();
mSrcCanvas.clipRect(childLeft, childTop, childRight, childBottom,
Op.REPLACE);
mSrcCanvas.translate(childLeft, childTop);
child.draw(mSrcCanvas);
mSrcCanvas.restore();
mXferPaint.setXfermode(null);
mXferPaint.setColor(Color.BLACK);
float sweepAngle = (lp.endAngle - lp.startAngle) % 361;
mDstCanvas
.drawArc(mBounds, lp.startAngle, sweepAngle, true, mXferPaint);
mXferPaint.setXfermode(mXfer);
mDstCanvas.drawBitmap(mSrc, 0f, 0f, mXferPaint);
canvas.drawBitmap(mDst, 0f, 0f, null);
}
private void redrawDirty(Canvas canvas) {
for (View child : mDirtyViews) {
drawChild(canvas, child, layoutParams(child));
}
if (mMotionTarget != null) {
drawChild(canvas, mMotionTarget, layoutParams(mMotionTarget));
}
}
private void drawDividers(Canvas canvas, float halfWidth, float halfHeight,
float radius) {
final int childs = getChildCount();
if (childs < 2) {
return;
}
for (int i = 0; i < childs; i++) {
final View child = getChildAt(i);
LayoutParams lp = layoutParams(child);
canvas.drawLine(halfWidth, halfHeight,
radius * (float) Math.cos(Math.toRadians(lp.startAngle))
+ halfWidth,
radius * (float) Math.sin(Math.toRadians(lp.startAngle))
+ halfHeight, mDividerPaint);
if (i == childs - 1) {
canvas.drawLine(halfWidth, halfHeight,
radius * (float) Math.cos(Math.toRadians(lp.endAngle))
+ halfWidth,
radius * (float) Math.sin(Math.toRadians(lp.endAngle))
+ halfHeight, mDividerPaint);
}
}
}
private void drawInnerCircle(Canvas canvas, float halfWidth,
float halfHeight) {
if (mInnerCircle != null) {
if (!(mInnerCircle instanceof ColorDrawable)) {
mInnerCircle
.setBounds((int) halfWidth - mInnerRadius,
(int) halfHeight - mInnerRadius,
(int) halfWidth + mInnerRadius,
(int) halfHeight + mInnerRadius);
mInnerCircle.draw(canvas);
} else {
canvas.drawCircle(halfWidth, halfHeight, mInnerRadius,
mCirclePaint);
}
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (mLayoutMode == LAYOUT_NORMAL) {
super.dispatchDraw(canvas);
return;
}
if (mSrc == null || mDst == null || mSrc.isRecycled()
|| mDst.isRecycled()) {
return;
}
final int childs = getChildCount();
final float halfWidth = getWidth() / 2f;
final float halfHeight = getHeight() / 2f;
final float radius = halfWidth > halfHeight ? halfHeight : halfWidth;
if (mCached && mDrawingCache != null && !mDrawingCache.isRecycled()
&& mDirtyViews.size() < childs / 2) {
canvas.drawBitmap(mDrawingCache, 0f, 0f, null);
redrawDirty(canvas);
drawDividers(canvas, halfWidth, halfHeight, radius);
drawInnerCircle(canvas, halfWidth, halfHeight);
return;
} else {
mCached = false;
}
Canvas sCanvas = null;
if (mCachedCanvas != null) {
sCanvas = canvas;
canvas = mCachedCanvas;
}
Drawable bkg = getBackground();
if (bkg != null) {
bkg.draw(canvas);
}
for (int i = 0; i < childs; i++) {
final View child = getChildAt(i);
LayoutParams lp = layoutParams(child);
drawChild(canvas, child, lp);
}
drawDividers(canvas, halfWidth, halfHeight, radius);
drawInnerCircle(canvas, halfWidth, halfHeight);
if (mCachedCanvas != null) {
sCanvas.drawBitmap(mDrawingCache, 0f, 0f, null);
mDirtyViews.clear();
mCached = true;
}
}
public static class LayoutParams extends ViewGroup.LayoutParams {
private float startAngle;
private float endAngle;
public float weight = 1f;
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleLayout">
<attr name="innerRadius" format="dimension" />
<attr name="sliceDivider" format="reference|color" />
<attr name="innerCircle" format="reference|color" />
<attr name="angleOffset" format="float" />
<attr name="angleRange" format="float" />
<attr name="layoutMode">
<enum name="normal" value="1" />
<enum name="pie" value="2" />
</attr>
<attr name="dividerWidth" format="dimension" />
</declare-styleable>
</resources>