Android雷达图变种的一个实现

48 篇文章 0 订阅
40 篇文章 0 订阅

遇到需求,要做一个雷达图类似的图。大概就是一个正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);
        }
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值