package com.sctdroid.app.uikit; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.CornerPathEffect; import android.graphics.Paint; import android.graphics.PaintFlagsDrawFilter; import android.graphics.Path; import android.graphics.drawable.Drawable; import android.text.TextPaint; import android.text.style.TextAppearanceSpan; import android.util.AttributeSet; import android.util.SparseArray; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.animation.DecelerateInterpolator; import java.util.Collections; import java.util.HashSet; import java.util.Set; /** * Created by lixindong on 9/23/16. */ public class CurveView extends View implements DataObserver { public CurveView(Context context) { this(context, null); } public CurveView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CurveView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initAttrs(context, attrs); init(); } public static final String TAG = CurveView.class.getSimpleName(); protected int mUnitWidth; protected int mFillColor; protected int mContentColor; protected int mStrokeWidth; protected int mContentPaddingTop; protected int mContentPaddingBottom; protected int mDotTextSize; protected int mDotTextColor; protected int mAxisTextSize; protected int mAxisTextColor; protected int mAxisLineToCurveAreaGapHeight; protected int mAxisTextToLineGapHeight; private int mCorner; protected int mContentPaddingStart; protected int mContentPaddingEnd; protected int mGravity = 0; private boolean mShowAll = false; /** <flag name="top" value="0x01" /> <flag name="bottom" value="0x02" /> <flag name="start" value="0x04" /> <flag name="end" value="0x08" /> <flag name="center_vertical" value="0x10" /> <flag name="center_horizontal" value="0x20" /> <flag name="center" value="0x30" /> */ public static class Gravity { public final static int TOP = 0x01; public final static int BOTTOM = 0x02; public final static int START = 0x04; public final static int END = 0x08; public final static int CENTER_VERTICAL = 0x10; public final static int CENTER_HORIZONTAL = 0x20; public final static int CENTER = 0x30; } private boolean mShowXLine = false; private boolean mShowXText = false; private boolean mShowY = false; protected Paint mContentPaint; protected Paint mBackgroundPaint; protected TextPaint mXAxisPaint; protected TextPaint mDotTextPaint; private Drawable mBackground; protected int mOffsetX = 0; protected Path mContentPath; protected CornerPathEffect mCornerPathEffect; PaintFlagsDrawFilter mPaintFlagsDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG|Paint.FILTER_BITMAP_FLAG); private void init() { mCornerPathEffect = new CornerPathEffect(mCorner); mContentPaint = new Paint(); mContentPaint.setStyle(Paint.Style.STROKE); mContentPaint.setColor(mContentColor); mContentPaint.setStrokeWidth(mStrokeWidth); mContentPaint.setPathEffect(mCornerPathEffect); mBackgroundPaint = new Paint(); // mBackgroundPaint.setColor(mFillColor); mXAxisPaint = new TextPaint(); mXAxisPaint.setColor(mAxisTextColor); mXAxisPaint.setTextSize(mAxisTextSize); mDotTextPaint = new TextPaint(); mDotTextPaint.setColor(mDotTextColor); mDotTextPaint.setTextSize(mDotTextSize); mContentPath = new Path(); // mMaxVelocity = ViewConfiguration.get(getContext()).getScaledMaximumFlingVelocity(); mMaxVelocity = 3000; } private void initAttrs(Context context, AttributeSet attrs) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.Curve, 0, 0); try { mUnitWidth = a.getDimensionPixelSize(R.styleable.Curve_unitWidth, 120); mFillColor = a.getColor(R.styleable.Curve_backgroundColor, Color.TRANSPARENT); mContentColor = a.getColor(R.styleable.Curve_contentColor, Color.BLACK); mStrokeWidth = a.getDimensionPixelSize(R.styleable.Curve_strokeWidth, 10); mContentPaddingTop = a.getDimensionPixelSize(R.styleable.Curve_contentPaddingTop, 0); mContentPaddingBottom = a.getDimensionPixelSize(R.styleable.Curve_contentPaddingBottom, 0); mDotTextSize = a.getDimensionPixelSize(R.styleable.Curve_dotTextSize, 60); mDotTextColor = a.getColor(R.styleable.Curve_dotTextColor, Color.BLACK); mAxisTextSize = a.getDimensionPixelSize(R.styleable.Curve_axisTextSize, 40); mAxisTextColor = a.getColor(R.styleable.Curve_axisTextColor, Color.BLACK); mCorner = a.getDimensionPixelSize(R.styleable.Curve_corner, 0); mContentPaddingStart = a.getDimensionPixelSize(R.styleable.Curve_contentPaddingStart, 0); mContentPaddingEnd = a.getDimensionPixelSize(R.styleable.Curve_contentPaddingEnd, 0); mShowXLine = a.getBoolean(R.styleable.Curve_showXLine, false); mShowXText = a.getBoolean(R.styleable.Curve_showXText, false); mShowY = a.getBoolean(R.styleable.Curve_showY, false); mGravity = a.getInteger(R.styleable.Curve_dotTextGravity, 0); mShowAll = a.getBoolean(R.styleable.Curve_showAll, false); mAxisTextToLineGapHeight = a.getDimensionPixelSize(R.styleable.Curve_axisTextToLineGapHeight, 0); mAxisLineToCurveAreaGapHeight = a.getDimensionPixelSize(R.styleable.Curve_axisLineToCurveAreaGapHeight, 0); } finally { a.recycle(); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 设置抗锯齿 canvas.setDrawFilter(mPaintFlagsDrawFilter); canvas.drawColor(mFillColor); if (mDecorations == null || mDecorations.size() == 0) { return; } int curveAreaHeight = getHeight() - mContentPaddingTop - mContentPaddingBottom - mAxisLineToCurveAreaGapHeight - mAxisTextToLineGapHeight - mAxisTextSize - mStrokeWidth; int curveAreaTop = mContentPaddingTop; int curveAreaBottom = mContentPaddingTop + curveAreaHeight; int curveAreaStart = mContentPaddingStart; int curveAreaEnd = mContentPaddingEnd; int heightPerLevel = curveAreaHeight / (mMaxLevel - mMinLevel); float scaleX = 1f; int unitWidth = mUnitWidth; if (mShowAll) { unitWidth = (getWidth() - mContentPaddingStart - mContentPaddingEnd) / (mDecorations.size() - 1); } if (mContentPath.isEmpty() || mForceUpdate) { mForceUpdate = false; mContentPath.moveTo(0, curveAreaBottom - (mDecorations.get(0).mLevel - mMinLevel) * heightPerLevel); for (int i = 1; i < mDecorations.size(); i++) { mContentPath.lineTo(i * unitWidth * scaleX, curveAreaBottom - (mDecorations.get(i).mLevel - mMinLevel) * heightPerLevel); } } canvas.save(); canvas.translate(mOffsetX + mContentPaddingStart, 0); canvas.drawPath(mContentPath, mContentPaint); for (int i = 0; i < mDecorations.size(); i++) { ItemDecoration decoration = mDecorations.get(i); int dotX = (int) (unitWidth * scaleX * i); int dotY = curveAreaBottom - (mDecorations.get(i).mLevel - mMinLevel) * heightPerLevel; // draw x axis text if (mShowXText) { int offsetX = getTextOffsetX(mXAxisPaint, decoration.mXAxisText, Gravity.CENTER_HORIZONTAL); canvas.drawText(decoration.mXAxisText, dotX + offsetX, getHeight() - mContentPaddingBottom, mXAxisPaint); } // draw mark text on dot for (Mark mark : decoration.mMarks) { int offsetX = getTextOffsetX(mDotTextPaint, mark.content, mark.gravity) + mark.marginStart - mark.marginEnd; int offsetY = getTextOffsetY(mDotTextPaint, mark.gravity) + mark.marginTop - mark.marginBottom; canvas.drawText(mark.content, dotX + offsetX, dotY + offsetY, mDotTextPaint); } } canvas.restore(); int axisY = getHeight() - mContentPaddingBottom - mAxisTextToLineGapHeight - mAxisTextSize; if (mShowXLine) { canvas.drawLine(0, axisY, getWidth(), axisY, mContentPaint); } } private int getTextOffsetY(TextPaint paint, int gravity) { int height = (int) (paint.getFontMetrics().descent - paint.getFontMetrics().ascent); int offset = (int) (paint.getFontMetrics().descent + paint.getFontMetrics().ascent) / 2; if ((gravity & Gravity.CENTER_VERTICAL) != 0) { offset += height / 2; } else if ((gravity & Gravity.BOTTOM) != 0) { offset += height; } return offset; } private int getTextOffsetX(TextPaint paint, String s, int gravity) { int width = (int) paint.measureText(s); int offset = 0; if ((gravity & Gravity.CENTER_HORIZONTAL) != 0) { offset = - width / 2; } else if ((gravity & Gravity.START) != 0) { offset = - width; } return offset; } public void setOffsetX(int offsetX) { this.mOffsetX = offsetX; } int mLastX; VelocityTracker mVelocityTracker; int mMaxVelocity; private void acquireVelocityTracker(MotionEvent event) { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); } private void releaseVelocityTracker() { if (mVelocityTracker != null) { mVelocityTracker.clear(); mVelocityTracker.recycle(); mVelocityTracker = null; } } @Override public boolean onTouchEvent(MotionEvent event) { if (mShowAll) { return false; } acquireVelocityTracker(event); final VelocityTracker velocityTracker = mVelocityTracker; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mLastX = (int) event.getRawX(); break; case MotionEvent.ACTION_MOVE: int offset = (int) (mOffsetX + (event.getRawX() - mLastX)); offset = checkOffset(offset); setOffsetX(offset); mLastX = (int) event.getRawX(); invalidate(); break; case MotionEvent.ACTION_UP: velocityTracker.computeCurrentVelocity(1000, mMaxVelocity); final float velocityX = velocityTracker.getXVelocity(); final int initialOffset = mOffsetX; ValueAnimator animator = ValueAnimator.ofFloat(velocityX, 0); final int duration = 300; animator.setDuration(duration); animator.setInterpolator(new DecelerateInterpolator()); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float v = (Float) animation.getAnimatedValue(); int t = (int) animation.getCurrentPlayTime(); int d = (int) (velocityX * duration * 1.0f / 1000 / 2 - v * (duration - t) * 1.0f / 1000 / 2); setOffsetX(checkOffset(initialOffset + d)); invalidate(); } }); animator.start(); break; } return true; } /** * offset > 0, scroll to left * offset < 0, scroll to right * normally, offset should <= 0 * @param offset offset to scroll horizontally * @return fixed offset, not to exceed limit */ private int checkOffset(int offset) { // only scroll when paint width > view width int paintWdith = (mDecorations.size() - 1) * mUnitWidth + mContentPaddingStart + mContentPaddingEnd; if (paintWdith < getWidth()) { return mOffsetX; } if (offset > 0) { offset = 0; } if (offset < getWidth() - paintWdith) { offset = getWidth() - paintWdith; } return offset; } private Adapter mAdapter; public void setAdapter(Adapter adapter) { if (mAdapter != null) { mAdapter.unregisterDataSetObserver(this); } mAdapter = adapter; adapter.registerDataSetObserver(this); updateAdapterData(); } @Override public void onChanged() { updateAdapterData(); invalidate(); } private int mMinLevel = 0; private int mMaxLevel = 100; private SparseArray<ItemDecoration> mDecorations = new SparseArray<>(); private boolean mForceUpdate = false; private void updateAdapterData() { mForceUpdate = true; clearData(); if (mAdapter == null) { return; } mMinLevel = mAdapter.getMinLevel(); mMaxLevel = mAdapter.getMaxLevel(); for (int i = 0; i < mAdapter.getCount(); i++) { ItemDecoration decoration = new ItemDecoration(); int level = mAdapter.getLevel(i); Set<Mark> marks = mAdapter.onCreateMarks(i); String xAxisText = mAdapter.getXAxisText(i); decoration.mLevel = level; decoration.mMarks = marks; decoration.mXAxisText = xAxisText; mAdapter.decorate(decoration, i); mDecorations.append(i, decoration); } } private void clearData() { // dot data // dot text data mDecorations.clear(); mContentPath.reset(); // line data // other data } public abstract static class Adapter { private final DataObservable mDataSetObservable = new DataObservable(); public void registerDataSetObserver(DataObserver observer) { mDataSetObservable.registerObserver(observer); } public void unregisterDataSetObserver(DataObserver observer) { mDataSetObservable.unregisterObserver(observer); } /** * Notifies the attached observers that the underlying data has been changed * and any View reflecting the data set should refresh itself. */ public void notifyDataSetChanged() { mDataSetObservable.notifyChanged(); } /** * @return 点的数量 */ public abstract int getCount(); /** * 绘制自定义元素, 未完成 * @param canvas * @param x * @param y * @param position */ public void draw(Canvas canvas, int x, int y, int position) { // TODO } /** * 添加自定义数据, 未完成 * @param decoration * @param position */ public void decorate(ItemDecoration decoration, int position) { // TODO } /** * 设置点上的文字,每个mark是一个,可同时设置点的 8 个方向的文字 * 注意: Gravity 应使用 CurveView.Gravity 类 * * @param position * @return */ public Set<Mark> onCreateMarks(int position) { return Collections.emptySet(); } /** * @return y 轴下限, 默认是 0 */ public int getMinLevel() { return 0; } /** * @return y 轴上限,默认是 100 */ public int getMaxLevel() { return 100; } /** * level 是 y 轴高度,在 minLevel 和 maxLevel 之间 * @param position * @return 返回当前 position 的 level */ public abstract int getLevel(int position); /** * 获取第 i 个点 x 轴上的文字 * @param i * @return */ public String getXAxisText(int i) { return ""; } } public static class ItemDecoration { protected Set<Mark> mMarks = new HashSet<>(); protected int mLevel; protected String mXAxisText; } public static class Mark { public final String content; public final int gravity; public final int marginStart; public final int marginEnd; public final int marginTop; public final int marginBottom; public final TextAppearanceSpan textAppearanceSpan; public Mark(String content) { this(content, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); } public Mark(String content, int gravity) { this(content, gravity, 0, 0, 0, 0); } public Mark(String content, int gravity, int marginStart, int marginTop, int marginEnd, int marginBottom) { this(content, gravity, marginStart, marginTop, marginEnd, marginBottom, null); } public Mark(String content, int gravity, int marginStart, int marginTop, int marginEnd, int marginBottom, TextAppearanceSpan mTextAppearanceSpan) { this.content = content; this.gravity = gravity; this.marginStart = marginStart; this.marginEnd = marginEnd; this.marginTop = marginTop; this.marginBottom = marginBottom; this.textAppearanceSpan = mTextAppearanceSpan; } } }