Android 鬼点子 - 分享自定义控件的思路

阅读 1796
收藏 0
2017-04-24
原文链接: www.jianshu.com

分享一下自定义控件的思路,单纯是个人的经验。
首先是美工妹子给的效果图。


效果图5.jpg

然后是这次做出来的真机截图。


效果3.jpg

这次主要分享我的思路,而不是具体的代码实现。

1.功能分析

显示0~100的数据,数据个数不确定,横向可以滑动,点击会出现气泡。这是典型的折线图的需求。

2.细节效果确认

背景色,横向坐标背景色,纵向坐标背景色,单位线,折线渐变背景,顶点有弧度,点击出现气泡,点击顶点出现圆圈,相应的单位线高亮,相应的横坐标高亮。

3.绘制思路

①确定控件大小

控件宽度铺满屏幕,高度可以自定义。但是这一切都是通过xml设置的,控件本身不需要做特殊处理。但是这里有个比较重要的点。就是字体的大小,字体的大小不能是不变的,要可以根据控件的尺寸进行缩放。甚至可以同将要绘制的字数进行缩放。

②确定关键部件的坐标

关键坐标可以死自定义view坐标基本不变的地方,比如这次的横纵坐标的位置。其他的部件的位置都是根据关键部件的位置来绘制的。控件大小确定了之后,关键部件的位置就可以大致确定,比如这次的这个折线图,横坐标的y轴位置是控件的高度分成12.5个格子后的第12个格子里面,同理纵坐标的x轴位置是控件宽度分成10.5个格子后最后一个格子里面。

③确定可变量

可变量的确定是很关键的,某些可变量可以通过外部设置,从而提高控件的适用性。另外某些变量是动画的因子,或者滑动的因子。这次的控件的可变量是横向的格子的数量,曲线的横坐标(用于滑动)。

④确定绘制顺序

后绘制的会把前面绘制的图层遮挡,所以要确定绘制顺序。还有事件触发的绘制的时机。

⑤了解绘制时会用到的API和数据的结构

就是要知道具体效果绘制时要要的API,比如如何画曲线啊,如何绘制渐变啊等等。数据的结构也是在这个时候就要确定了。

⑥绘制固定状态

我的个人习惯是先绘制固定状态,就是不要考虑动画或者触摸事件,只是绘制关键状态的控件的样子,当然这里就要使用上面说到的可变量绘制。这么做类似做动画时的关键帧吧。

⑦改变可变量

改变可变量的值,让各个固定状态连接起来,适时的呼起重绘,从而控件就动了起来。

⑧触摸事件处理

根据触摸事件修改可变量。当然前提是要十分了解Android触摸事件处理机制。

⑨美工过目

效果出来,给设计人员看看,看看是否漏掉某些细节。或者哪些细节需要修改。

⑩优化

优化界面效果,增加动画,优化内存和绘制效率,适配分辨率。

4.使用

实际项目中使用,持续优化。

最后,代码分享,用到这个效果的同学可以自己改改。

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.CornerPathEffect;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

import com.allrun.arsmartelevatorformanager.R;
import com.allrun.arsmartelevatorformanager.util.DPUnitUtil;

import java.util.List;

/**
 * Created by GrenndaMi on 2017/4/5.
 */

public class PPChart extends View {
    Context mContext;

    Paint mPaint;
    private int mXDown, mLastX;
    //最短滑动距离
    int a = 0;


    float startX = 0;
    float lastStartX = 0;//抬起手指后,当前控件最左边X的坐标
    float cellCountW = 9.5f;//一个屏幕的宽度会显示的格子数
    float cellCountH = 12.5f;//整个控件的高度会显示的格子数

    float cellH, cellW;

    float topPadding = 0.25f;

    PathEffect mEffect = new CornerPathEffect(20);//平滑过渡的角度

    int state = -100;
    int lineWidth;

    public void setData(List<dataObject> data) {
        this.data = data;
        state = -100;
        postInvalidate();
    }

    List<dataObject> data;

    public PPChart(Context context) {
        super(context);
        mContext = context;
    }

    public PPChart(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        a = DPUnitUtil.px2dip(context, ViewConfiguration.get(context).getScaledDoubleTapSlop());
        setClickable(true);
        lineWidth = DPUnitUtil.dip2px(mContext, 1);
    }


    public PPChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
    }

    private void initPaint() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        //线的颜色
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        initPaint();
        cellH = getHeight() / cellCountH;
        cellW = getWidth() / cellCountW;
        //画底部背景
        mPaint.setColor(0xff44b391);
        canvas.drawRect(0, (((int) cellCountH - 1) + topPadding) * cellH, getWidth(), cellCountH * cellH, mPaint);

        if (data == null || data.size() == 0) {
            return;
        }

        DrawAbscissaLines(canvas);
        DrawOrdinate(canvas);
        //------------到此背景结束---------------

        canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
        DrawDataBackground(canvas);
        canvas.restore();

        canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
        DrawDataLine(canvas);
        canvas.restore();

        canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
        DrawAbscissa(canvas);
        canvas.restore();

        showPop(canvas);

        if (state == -100) {
            gotoEnd();
        }

    }

    //画横坐标
    private void DrawOrdinate(Canvas canvas) {
        mPaint.reset();

        float i = 0.5f;
        for (dataObject tmp : data) {
            mPaint.setColor(0xffb4e1d3);
            mPaint.setTextSize(getWidth() / cellCountW / 3.2f);
            dataObject tmp2 = getDataByX(mLastX);

            //选中的那一项需要加深
            if (tmp2 != null && tmp2.getHappenTime().equals(tmp.getHappenTime()) && state == MotionEvent.ACTION_UP && Math.abs(mLastX - mXDown) < a) {
                mPaint.setColor(0xffffffff);
            } else {
                mPaint.setColor(0xffb4e1d3);
            }
            String str1 = tmp.getHappenTime().split("-")[0];
            canvas.drawText(str1,
                    startX + cellW * i - mPaint.measureText(str1) / 2,
                    (((int) cellCountH - 1) + topPadding + cellCountH) / 2 * cellH,
                    mPaint);
            mPaint.setTextSize(getWidth() / cellCountW / 3.5f);
            String str2 = tmp.getHappenTime().split("-")[1] + "." + tmp.getHappenTime().split("-")[2];
            canvas.drawText(str2,
                    startX + cellW * i - mPaint.measureText(str2) / 2,
                    (((int) cellCountH - 1) + topPadding + cellCountH) / 2 * cellH - 1.5f * (mPaint.ascent() + mPaint.descent()),
                    mPaint);

            //画背景竖线
            mPaint.setColor(0xff92dac4);
            canvas.drawLine(startX + cellW * i,
                    topPadding * cellH,
                    startX + cellW * i,
                    (topPadding + 10.5f) * cellH,
                    mPaint);

            i++;
        }

        mPaint.setColor(0xffb4e1d3);
        mPaint.setTextSize(getWidth() / cellCountW / 3f);
        canvas.drawText("end",
                startX + cellW * i - mPaint.measureText("end") / 2,
                (((int) cellCountH - 1) + topPadding + cellCountH) / 2 * cellH - (mPaint.ascent() + mPaint.descent()) / 2,
                mPaint);
    }

    //画纵坐标
    public void DrawAbscissaLines(Canvas canvas) {

        mPaint.setColor(0xff92dac4);

        //画背景横线
        canvas.drawLine(0,
                topPadding * cellH,
                cellW * 9.5f,
                topPadding * cellH,
                mPaint);
        canvas.drawLine(0,
                (topPadding + 1) * cellH,
                cellW * 9.5f,
                (topPadding + 1) * cellH,
                mPaint);
        canvas.drawLine(0,
                (topPadding + 2) * cellH,
                cellW * 9.5f,
                (topPadding + 2) * cellH,
                mPaint);
        canvas.drawLine(0,
                (topPadding + 3) * cellH,
                cellW * 9.5f,
                (topPadding + 3) * cellH,
                mPaint);
        canvas.drawLine(0,
                (topPadding + 4) * cellH,
                cellW * 9.5f,
                (topPadding + 4) * cellH,
                mPaint);
        canvas.drawLine(0,
                (topPadding + 5) * cellH,
                cellW * 9.5f,
                (topPadding + 5) * cellH,
                mPaint);
        canvas.drawLine(0,
                (topPadding + 6) * cellH,
                cellW * 9.5f,
                (topPadding + 6) * cellH,
                mPaint);
        canvas.drawLine(0,
                (topPadding + 7) * cellH,
                cellW * 9.5f,
                (topPadding + 7) * cellH,
                mPaint);
        canvas.drawLine(0,
                (topPadding + 8) * cellH,
                cellW * 9.5f,
                (topPadding + 8) * cellH,
                mPaint);
        canvas.drawLine(0,
                (topPadding + 9) * cellH,
                cellW * 9.5f,
                (topPadding + 9) * cellH,
                mPaint);
        canvas.drawLine(0,
                (topPadding + 10) * cellH,
                cellW * 9.5f,
                (topPadding + 10) * cellH,
                mPaint);
    }

    //画纵坐标
    public void DrawAbscissa(Canvas canvas) {
        mPaint.reset();
        mPaint.setColor(mContext.getResources().getColor(R.color.colorPrimary));
        //画纵坐标背景9.51 = 10 - 0.5(i 的起步)+ 0.01(把最后一条线露出来)
        canvas.drawRect(cellW * ((int) cellCountW - 0.5f + 0.01f), 0, cellW * ((int) cellCountW + 1), 11.2f * cellH, mPaint);

        mPaint.setColor(0xffb6e6d7);
        mPaint.setTextSize(getWidth() / cellCountW / 3);


        canvas.drawText("100%",
                cellW * (int) cellCountW - mPaint.measureText("100%") / 2,
                topPadding * cellH - (mPaint.ascent() + mPaint.descent()) / 2,
                mPaint);

        canvas.drawText("80%",
                cellW * (int) cellCountW - mPaint.measureText("80%") / 2,
                (topPadding + 2) * cellH - (mPaint.ascent() + mPaint.descent()) / 2,
                mPaint);

        canvas.drawText("60%",
                cellW * (int) cellCountW - mPaint.measureText("60%") / 2,
                (topPadding + 4) * cellH - (mPaint.ascent() + mPaint.descent()) / 2,
                mPaint);

        canvas.drawText("40%",
                cellW * (int) cellCountW - mPaint.measureText("40%") / 2,
                (topPadding + 6) * cellH - (mPaint.ascent() + mPaint.descent()) / 2,
                mPaint);

        canvas.drawText("20%",
                cellW * (int) cellCountW - mPaint.measureText("20%") / 2,
                (topPadding + 8) * cellH - (mPaint.ascent() + mPaint.descent()) / 2,
                mPaint);

        canvas.drawText("0%",
                cellW * (int) cellCountW - mPaint.measureText("0%") / 2,
                (topPadding + 10) * cellH - (mPaint.ascent() + mPaint.descent()) / 2,
                mPaint);

    }

    //画渐变背景
    private void DrawDataBackground(Canvas canvas) {
        if (data == null || data.size() == 0) {
            return;
        }
        LinearGradient lg = new LinearGradient(getWidth() / 2, topPadding * cellH, getWidth() / 2, (topPadding + 10) * cellH, 0xaaffffff, 0xaa61ccab, Shader.TileMode.CLAMP);
        mPaint.setShader(lg);

        float i = 0.5f;
        Path path = new Path();

        //起点和终点要多画2次,防止圆角出现
        path.moveTo(startX + cellW * i, (topPadding + 10) * cellH);
        path.lineTo(startX + cellW * i, (topPadding + 10) * cellH);
        path.lineTo(startX + cellW * i, getHByValue(data.get(0).getNum()));
        for (dataObject tmp : data) {
            path.lineTo(startX + cellW * i, getHByValue(tmp.getNum()));
            i++;
        }
        path.lineTo(startX + cellW * (i -1), getHByValue(data.get(data.size()-1).getNum()));
        path.lineTo(startX + cellW * (i - 1), (topPadding + 10) * cellH -1);
        path.lineTo(startX + cellW * (i - 1), (topPadding + 10) * cellH);
        path.close();
        mPaint.setPathEffect(mEffect);
        canvas.drawPath(path, mPaint);

    }

    //画数据线
    public void DrawDataLine(Canvas canvas) {

        float i = 0.5f;
        mPaint.reset();
        mPaint.setStrokeWidth(lineWidth);
        mPaint.setColor(0xffffffff);

        Path path = new Path();
        path.moveTo(startX + cellW * i -1, getHByValue(data.get(0).getNum()));
        path.lineTo(startX + cellW * i, getHByValue(data.get(0).getNum()));
        for (dataObject tmp : data) {
            path.lineTo(startX + cellW * i, getHByValue(tmp.getNum()));
            i++;
        }
        path.lineTo(startX + cellW * (i -1), getHByValue(data.get(data.size()-1).getNum()));
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setPathEffect(mEffect);
        canvas.drawPath(path, mPaint);

    }

    //显示数据气泡
    private void showPop(Canvas canvas) {
        //点击了
        if (state == MotionEvent.ACTION_UP && Math.abs(mLastX - mXDown) < a) {
            dataObject data = getDataByX(mLastX);
            if (data == null) {
                return;
            }
            initPaint();
            // 选中的线
            mPaint.setColor(0xaaffffff);
            canvas.drawLine(getXBykey(data.getHappenTime()), getHByValue(data.getNum()), getXBykey(data.getHappenTime()), (topPadding + 10f) * cellH, mPaint);
            //画气泡背景
            mPaint.setColor(0xffffffff);
            mPaint.setTextSize(getWidth() / cellCountW / 3f);
            Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
            RectF r;

            //气泡距离顶点有0.5个格子高度的距离,气泡的高度是文字高度的1.5倍。宽度是文字宽度的1.6倍(0.8+0.8)
            float left = getXBykey(data.getHappenTime()) - mPaint.measureText(data.getNum() + "%") * 0.8f;
            if(left < 0 ){
                left = 0;
            }
            float right = left + 2 * mPaint.measureText(data.getNum() + "%") * 0.8f;
            if (data.getNum() >= 10) {
                r = new RectF(left,
                        getHByValue(data.getNum()) + 0.5f * cellH,
                        right,
                        getHByValue(data.getNum()) + 0.5f * cellH + 1.5f * (fontMetrics.bottom - fontMetrics.top));
            } else {
                r = new RectF(left,
                        getHByValue(data.getNum()) - 0.5f * cellH - 1.5f * (fontMetrics.bottom - fontMetrics.top),
                        right,
                        getHByValue(data.getNum()) - 0.5f * cellH);
            }
            //画气泡上的文字
            canvas.drawRoundRect(r, 90, 90, mPaint);
            mPaint.setColor(0xff414141);

            float baseline = (r.bottom + r.top - fontMetrics.bottom - fontMetrics.top) / 2;

            canvas.drawText(data.getNum() + "%",
                    (r.left+ r.right)/2 - mPaint.measureText(data.getNum() + "%") / 2f,
                    baseline, mPaint);

            //画线上的圆
            mPaint.setStrokeWidth(lineWidth * 2);
            mPaint.setColor(0xff49c29d);
            canvas.drawCircle(getXBykey(data.getHappenTime()), getHByValue(data.getNum()), lineWidth * 5, mPaint);
            mPaint.setColor(0xffffffff);
            mPaint.setStyle(Paint.Style.STROKE);
            canvas.drawCircle(getXBykey(data.getHappenTime()), getHByValue(data.getNum()), lineWidth * 5, mPaint);


            mPaint.setStrokeWidth(lineWidth);

        }
    }

    //触摸处理
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        if (data == null || data.size() == 0) {
            return super.onTouchEvent(event);
        }
        final int action = event.getAction();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                // 按下
                mXDown = (int) event.getRawX();
                state = MotionEvent.ACTION_DOWN;
                break;

            case MotionEvent.ACTION_MOVE:
                // 移动
                mLastX = (int) event.getRawX();

                if (Math.abs(lastStartX - mXDown) < a) {
                    break;
                }

                //滑动限制
                if (lastStartX + mLastX - mXDown > 0.5f * cellW || lastStartX + mLastX - mXDown + cellW * (data.size() + 0.5f) < cellW * (cellCountW - 1)) {
                    break;
                }
                state = MotionEvent.ACTION_MOVE;
                startX = lastStartX + mLastX - mXDown;
                postInvalidate();
                break;

            case MotionEvent.ACTION_UP:
                // 抬起
                lastStartX = startX;
                state = MotionEvent.ACTION_UP;
                postInvalidate();
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

    //通过坐标,获得附近的点
    private dataObject getDataByX(int pointX) {
        float i = 0.5f;
        dataObject result = null;
        for (dataObject tmp : data) {
            float x = startX + cellW * i;
            if (Math.abs(x - pointX) < cellW / 2) {
                result = tmp;
                return result;
            }
            i++;
        }
        return result;
    }

    private float getHByValue(float value) {
        return (topPadding + 10) * cellH - (cellH * 10) * value / 100;
    }

    //通过横坐标文字获取该点的X坐标
    private float getXBykey(String key) {
        float i = 0.5f;
        for (dataObject tmp : data) {

            if (tmp.getHappenTime().equals(key)) {
                return startX + cellW * i;
            }
            i++;
        }
        return 0;
    }


    //显示最右边的最新数据
    public void gotoEnd() {
        if (data == null || data.size() == 0) {
            return;
        }
        if (data.size() < cellCountW - 1) {
            startX = 0;
            lastStartX = startX;
            postInvalidate();
            return;
        }

        startX = -(cellW) * (data.size() - cellCountW + 1);
        lastStartX = startX;
        postInvalidate();
    }
}

用到的数据结构

public class dataObject {
    String happenTime;
    float num;

    public dataObject(String happenTime, float num) {
        this.happenTime = happenTime;
        this.num = num;
    }

    public String getHappenTime() {
        return happenTime;
    }

    public void setHappenTime(String happenTime) {
        this.happenTime = happenTime;
    }

    public float getNum() {
        return num;
    }

    public void setNum(float num) {
        this.num = num;
    }
}

xml布局

<com.allrun.arsmartelevatorformanager.widget.PPChart
        android:id="@+id/chart"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="3"
        android:background="@color/colorPrimary" />

代码中设置数据

chart.setData(resultModel.getObj().getRows());