/* * Copyright dmitry.zaicew@gmail.com Dmitry Zaitsev * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.bosi.chineseclass.han.view; import java.util.HashSet; import java.util.Set; import com.bosi.chineseclass.R; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.graphics.Xfermode; import android.graphics.Region.Op; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; 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, android.R.color.darker_gray); mInnerCircle = a.getDrawable(R.styleable.CircleLayout_innerCircle); if(mInnerCircle instanceof ColorDrawable) { int innerColor = a.getColor(R.styleable.CircleLayout_innerCircle, 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); } } }