遇到需求,要做一个雷达图类似的图。大概就是一个正n边型,每个顶点有一个View,多边形外接圆圆心有一个View。
很早的一段代码,公司没用上,可以分享出来了。
思路呢,就是模仿ListView,顶点使用Adapter.getView返回的View。同时尽量有复用,减少View的创建。
- 统一接口
public interface PolygonInterface {
interface OnItemClickListener {
void onItemClick(View parent, int position, long id);
}
void setOnItemClickListener(OnItemClickListener listener);
void setAdapter(Adapter adapter);
int getArcRadius();
}
- 仿ListView的PolygonView
public class PolygonView extends ViewGroup implements PolygonInterface {
public static final int UNSET = -1;
private static final int sUnavailablePosition = -1;
public static final int FLAG_NONE = 0;
public static final int FLAG_LINE = 1;
public static final int FLAG_ARC = 2;
public static final int FLAG_RADIUS = 4;
private static final int sInit = 0;
private static final int sChanged = 1;
private static final int sRelayout = 2;
private static final int sDraw = 3;
//adapter
private Adapter mAdapter;
private int mCount = -1;
private DataSetObserver mDataSetObserver = new DataSetObserver() {
@Override
public void onChanged() {
super.onChanged();
onNewData(false);
requestLayout();
}
};
private ViewPool mPool = new ViewPool();
//events
private PolygonInterface.OnItemClickListener mListener;
private OnClickListener mClickDispatcher = new OnClickListener() {
@Override
public void onClick(View v) {
if (null == mListener || null == mAdapter) {
return;
}
int position = getViewPosition(v);
if (sUnavailablePosition == position) {
return;
}
long id = mAdapter.getItemId(position);
mListener.onItemClick(PolygonView.this, position, id);
}
};
//draw structure
private ArrayList<Point> mPositions;
private int mNodeRadius;
private double mRadius;
private int mLineType;
private Paint mPaint;
private Point mMiddle;
private RectF mOval;
//draw control
private int mState = sInit;
private int mLastWidth;
private int mLastHeight;
private int mLastCount;
public void setOnItemClickListener(PolygonInterface.OnItemClickListener listener) {
mListener = listener;
}
public void setAdapter(Adapter adapter) {
if (null == adapter) {
return;
}
if (null != mAdapter) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
mAdapter = adapter;
mAdapter.registerDataSetObserver(mDataSetObserver);
onNewData(true);
requestLayout();
}
private void onNewData(boolean viewTypeChanged) {
mState = sChanged;
mLastCount = mCount;
if (mCount != mAdapter.getCount()) {
mCount = mAdapter.getCount();
mPool.init(mCount, viewTypeChanged);
mPositions = new ArrayList<Point>(mCount);
mRadius = UNSET;
mMiddle = new Point();
} else if (viewTypeChanged) {
mPool.init(mCount, viewTypeChanged);
}
}
public PolygonView(Context context) {
super(context);
init(context, null, 0);
}
public PolygonView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0);
}
public PolygonView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs, defStyle);
}
public void setLineType(int type) {
mLineType = type;
}
public void setLineColor(int color) {
mPaint.setColor(color);
}
public void setLineWidth(int width) {
mPaint.setStrokeWidth(width);
}
public void setMaxNodeRadius(int radius) {
mNodeRadius = radius;
}
public double getRadius() {
return mRadius;
}
public int getNodeRadius() {
return mNodeRadius;
}
public Point getMiddle() {
return new Point(mMiddle);
}
public int getArcRadius() {
return (int) (mRadius - mNodeRadius / 3);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mCount != mAdapter.getCount()) {
throw new IllegalStateException("count isn't the same");
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mLastCount != mCount || sInit == mState || mLastWidth != widthMeasureSpec || mLastHeight != heightMeasureSpec || UNSET == mRadius) {
final int height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
final int width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
mRadius = Math.min(getRadiusByWidth(width), getRadiusByHeight(height));
if (UNSET == mNodeRadius) {
mNodeRadius = (int) (mRadius / 5);
}
}
for (int i = 0; i < mCount; ++i) {
View child = mPool.obtainView(i);
if (null == child) {
continue;
}
LayoutParams layoutParams = child.getLayoutParams();
child.measure(getMeasureSpecForChild(layoutParams.width), getMeasureSpecForChild(layoutParams.height));
if (child.getMeasuredHeight() > mNodeRadius * 2 || child.getMeasuredWidth() > mNodeRadius * 2) {
throw new IllegalStateException("child over sized");
}
mPool.scrapNoneActiveView(i, child);
}
mLastHeight = heightMeasureSpec;
mLastWidth = widthMeasureSpec;
}
private int getMeasureSpecForChild(int layoutSize) {
if (layoutSize > 2 * mNodeRadius || layoutSize < 0) {
return MeasureSpec.makeMeasureSpec(2 * mNodeRadius, MeasureSpec.AT_MOST);
} else {
return MeasureSpec.makeMeasureSpec(layoutSize, MeasureSpec.EXACTLY);
}
}
/*
* height = radius * (1 - cos(max(angle < 180)))
* */
private double getRadiusByHeight(int height) {
if (FLAG_NONE != (mLineType & FLAG_ARC)) {
if (UNSET == mNodeRadius) {
return height * 5 / 12;
} else {
return height / 2 - mNodeRadius;
}
} else {
final double alpha = Math.PI * 2 / mCount;
final int halfCount = mCount / 2;
if (UNSET == mNodeRadius) {
return height / (5 / 7 - Math.cos(halfCount * alpha));
} else {
return (height - 2 * mNodeRadius) / (1 - Math.cos(halfCount * alpha));
}
}
}
/*
* width = radius * max(sin(max(angle < 90)), sin(min(angle > 90))))
* */
private double getRadiusByWidth(int width) {
if (FLAG_NONE != (mLineType & FLAG_ARC)) {
if (UNSET == mNodeRadius) {
return width * 5 / 12;
} else {
return width / 2 - mNodeRadius;
}
} else {
final double alpha = Math.PI * 2 / mCount;
final int quaterCount = mCount / 4;
if (UNSET == mNodeRadius) {
return width / 2 / (0.2 + Math.max(Math.sin(quaterCount * alpha), Math.sin((quaterCount + 1) * alpha)));
} else {
return (width / 2 - mNodeRadius) / Math.max(Math.sin(quaterCount * alpha), Math.sin((quaterCount + 1) * alpha));
}
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mCount != mAdapter.getCount()) {
throw new IllegalStateException("count isn't the same");
}
if (mLastCount != mCount || sInit == mState || 0 == mPositions.size()) {
initPosition(l, t, r, b);
mState = sRelayout;
}
int measureSpec = MeasureSpec.makeMeasureSpec(2 * mNodeRadius, MeasureSpec.AT_MOST);
for (int i = 0; i < mCount; ++i) {
View child = mPool.obtainView(i);
if (null == child) {
continue;
}
int childHeight = child.getMeasuredHeight();
int childWidth = child.getMeasuredWidth();
if (0 == childHeight || 0 == childWidth) {
child.measure(measureSpec, measureSpec);
}
childHeight = child.getMeasuredHeight();
childWidth = child.getMeasuredWidth();
if (0 == childHeight || 0 == childWidth) {
mPool.scrapView(i, child);
continue;
}
//for center
childHeight /= 2;
childWidth /= 2;
LayoutParams layoutParams = child.getLayoutParams();
if (null == layoutParams) {
layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
addViewInLayout(child, i, layoutParams);
final Point point = mPositions.get(i);
child.layout(point.x - childWidth, point.y - childHeight, point.x + childWidth, point.y + childHeight);
mPool.activateView(i, child);
}
}
private void initPosition(int l, int t, int r, int b) {
final double alpha = Math.PI * 2 / mCount;
final int halfCount = (int) (Math.PI / alpha);
final int contentHeight = (int) ((1 - Math.cos(halfCount * alpha)) * mRadius + 2 * mNodeRadius);
int offset = b - t - getPaddingTop() - getPaddingBottom() - contentHeight;
offset = offset > 0 ? offset / 2 : 0;
mMiddle.x = getPaddingLeft() + (r - l) / 2;
mMiddle.y = (int) (getPaddingTop() + mNodeRadius + mRadius + offset);
if (FLAG_NONE != (mLineType & FLAG_ARC)) {
double arcRadius = getArcRadius();
mOval.left = (float) (mMiddle.x - arcRadius);
mOval.top = (float) (mMiddle.y - arcRadius);
mOval.right = (float) (mMiddle.x + arcRadius);
mOval.bottom = (float) (mMiddle.y + arcRadius);
}
for (int i = 0; i < mCount; ++i) {
mPositions.add(new Point((int) (mMiddle.x + mRadius * Math.sin(alpha * i)), (int) (mMiddle.y - mRadius * Math.cos(alpha * i))));
}
}
@Override
protected void onDraw(Canvas canvas) {
if (mCount != mAdapter.getCount()) {
throw new IllegalStateException("count isn't the same");
}
final int length = mPositions.size();
if (FLAG_NONE != (mLineType & FLAG_ARC)) {
canvas.drawArc(mOval, 0F, 360F, false, mPaint);
}
if (length > 2 && FLAG_NONE != mLineType) {
boolean drawLine = FLAG_NONE != (mLineType & FLAG_LINE);
boolean drawRadius = FLAG_NONE != (mLineType & FLAG_RADIUS);
if (drawLine || drawRadius) {
Point last = mPositions.get(0);
for (int i = 1; i < length; ++i) {
Point point = mPositions.get(i);
if (drawLine) {
canvas.drawLine(last.x, last.y, point.x, point.y, mPaint);
}
if (drawRadius) {
canvas.drawLine(mMiddle.x, mMiddle.y, point.x, point.y, mPaint);
}
last = point;
}
Point point = mPositions.get(0);
if (drawLine) {
canvas.drawLine(last.x, last.y, point.x, point.y, mPaint);
}
if (drawRadius) {
canvas.drawLine(mMiddle.x, mMiddle.y, point.x, point.y, mPaint);
}
}
}
super.onDraw(canvas);
mState = sDraw;
}
private void init(Context context, AttributeSet attrs, int defStyle) {
final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PolygonView);
mNodeRadius = typedArray.getDimensionPixelSize(R.styleable.PolygonView_maxNodeRadius, UNSET);
mLineType = typedArray.getInt(R.styleable.PolygonView_lineType, FLAG_NONE);
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
int color = typedArray.getColor(R.styleable.PolygonView_lineColor, Color.BLACK);
mPaint.setColor(color);
int width = typedArray.getDimensionPixelSize(R.styleable.PolygonView_lineWidth, 1);
mPaint.setStrokeWidth(width);
typedArray.recycle();
setWillNotDraw(false);
mOval = new RectF();
}
private int getViewPosition(View v) {
for (int i = 0; i < mCount; ++i) {
if (v == getChildAt(i)) {
return i;
}
}
return sUnavailablePosition;
}
private class ViewPool {
private View[] mScrappedViews;
private View[] mActiveViews;
public void init(int size, boolean viewTypeChanged) {
//remove last attached views
if (null != mActiveViews) {
removeAllViews();
final int length = mActiveViews.length;
for (int i = 0; i < length; ++i) {
if (null != mActiveViews[i]) {
mActiveViews[i].setOnClickListener(null);
}
}
}
//reuse available views
View[] scrappedTemp = mScrappedViews;
View[] activeTemp = mActiveViews;
mScrappedViews = new View[size];
mActiveViews = new View[size];
if (null != scrappedTemp && null != activeTemp && !viewTypeChanged) {
int diff = mScrappedViews.length - scrappedTemp.length;
System.arraycopy(scrappedTemp, 0, mScrappedViews, 0, Math.min(scrappedTemp.length, mScrappedViews.length));
if (diff > 0) {
System.arraycopy(activeTemp, 0, mScrappedViews, diff, Math.min(diff, activeTemp.length));
}
}
}
public void scrapView(int index, View view) {
removeView(view);
if (index < 0 || index >= mScrappedViews.length) {
throw new IllegalStateException("index out of bounds");
}
if (mActiveViews[index] == view) {
mActiveViews[index] = null;
}
mScrappedViews[index] = view;
view.setOnClickListener(null);
}
public void scrapNoneActiveView(int index, View view) {
if (index < 0 || index >= mScrappedViews.length) {
throw new IllegalStateException("index out of bounds");
}
if (mActiveViews[index] == view) {
return;
}
mScrappedViews[index] = view;
view.setOnClickListener(null);
}
public void activateView(int index, View view) {
if (index < 0 || index >= mScrappedViews.length) {
throw new IllegalStateException("index out of bounds " + index);
}
if (mScrappedViews[index] == view) {
mScrappedViews[index] = null;
}
mActiveViews[index] = view;
view.setOnClickListener(mClickDispatcher);
}
public View obtainView(int index) {
if (index < 0 || index >= mScrappedViews.length) {
throw new IllegalStateException("index out of bounds " + index);
}
View view;
if (null != mActiveViews[index]) {
view = mActiveViews[index];
if (null != mAdapter) {
view = mAdapter.getView(index, view, PolygonView.this);
if (view != mActiveViews[index]) {
mActiveViews[index] = null;
}
}
return view;
}
if (null != mScrappedViews[index]) {
view = mScrappedViews[index];
mScrappedViews[index] = null;
if (null != mAdapter) {
view = mAdapter.getView(index, view, PolygonView.this);
}
return view;
}
return null == mAdapter ? null : mAdapter.getView(index, null, PolygonView.this);
}
public View obtainActiveView(int index) {
if (index < 0 || index >= mScrappedViews.length) {
throw new IllegalStateException("index out of bounds " + index);
}
View view = null;
if (null != mActiveViews[index]) {
view = mActiveViews[index];
if (null != mAdapter) {
view = mAdapter.getView(index, view, PolygonView.this);
if (view != mActiveViews[index]) {
mActiveViews[index] = null;
view = null;
}
}
}
return view;
}
public View obtainScrappedView(int index) {
if (index < 0 || index >= mScrappedViews.length) {
throw new IllegalStateException("index out of bounds " + index);
}
View view;
if (null != mScrappedViews[index]) {
view = mScrappedViews[index];
mScrappedViews[index] = null;
if (null != mAdapter) {
view = mAdapter.getView(index, view, PolygonView.this);
}
return view;
}
return null == mAdapter ? null : mAdapter.getView(index, null, PolygonView.this);
}
}
public static class IllegalStateException extends RuntimeException {
public IllegalStateException(String detailMessage) {
super(detailMessage);
}
}
}
- 加入圆心的View
public class PolygonWithMiddleLabelView extends FrameLayout implements PolygonInterface {
private PolygonView mPolygon;
private View mMiddleView;
public PolygonWithMiddleLabelView(Context context) {
super(context);
init(context, null, 0);
}
public PolygonWithMiddleLabelView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0);
}
public PolygonWithMiddleLabelView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
private void init(Context context, AttributeSet attrs, int defStyle) {
LayoutInflater.from(context).inflate(R.layout.view_polygon_with_middle_label, this, true);
mPolygon = (PolygonView) findViewById(R.id.polygon);
final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PolygonView);
int nodeRadius = typedArray.getDimensionPixelSize(R.styleable.PolygonView_maxNodeRadius, PolygonView.UNSET);
mPolygon.setMaxNodeRadius(nodeRadius);
int lineType = typedArray.getInt(R.styleable.PolygonView_lineType, PolygonView.FLAG_NONE);
mPolygon.setLineType(lineType);
int color = typedArray.getColor(R.styleable.PolygonView_lineColor, Color.BLACK);
mPolygon.setLineColor(color);
int width = typedArray.getDimensionPixelSize(R.styleable.PolygonView_lineWidth, 1);
mPolygon.setLineWidth(width);
typedArray.recycle();
//force calling onDraw
setWillNotDraw(false);
}
@Override
public void setOnItemClickListener(OnItemClickListener listener) {
if (null == mPolygon) {
return;
}
mPolygon.setOnItemClickListener(listener);
}
@Override
public void setAdapter(Adapter adapter) {
if (null == mPolygon) {
return;
}
mPolygon.setAdapter(adapter);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (null != mMiddleView) {
ViewGroup.LayoutParams layoutParams = mMiddleView.getLayoutParams();
mMiddleView.measure(getMeasureSpecForChild(layoutParams.width), getMeasureSpecForChild(layoutParams.height));
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (null != mMiddleView) {
removeViewInLayout(mMiddleView);
ViewGroup.LayoutParams layoutParams = mMiddleView.getLayoutParams();
if (null == layoutParams) {
layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
addViewInLayout(mMiddleView, 0, layoutParams);
int width = mMiddleView.getMeasuredWidth() / 2;
int height = mMiddleView.getMeasuredHeight() / 2;
final Point point = mPolygon.getMiddle();
mMiddleView.layout(point.x - width, point.y - height, point.x + width, point.y + height);
//z order
mMiddleView.bringToFront();
}
}
public void setMiddleView(View view) {
if (null != mMiddleView) {
removeView(mMiddleView);
}
mMiddleView = view;
}
public int getArcRadius() {
return mPolygon.getArcRadius();
}
public PolygonView getPolygon() {
return mPolygon;
}
private int getMeasureSpecForChild(int layoutSize) {
int max = (int) mPolygon.getRadius();
if (layoutSize > max || layoutSize < 0) {
return MeasureSpec.makeMeasureSpec(max, MeasureSpec.AT_MOST);
} else {
return MeasureSpec.makeMeasureSpec(layoutSize, MeasureSpec.EXACTLY);
}
}
}