先看一下效果图

android 自定义 xml drawable Android 自定义折线图_折线

1.创建一个类


MiuiWeatherView


public class MiuiWeatherView extends View {

    private static int DEFAULT_BULE = 0XFF00BFFF;
    private static int DEFAULT_GRAY = Color.GRAY;

    private int backgroundColor;
    private int minViewHeight; //控件的最低高度
    private int minPointHeight;//折线最低点的高度
    private int lineInterval; //折线线段长度
    private float pointRadius; //折线点的半径
    private float textSize; //字体大小
    private float pointGap; //折线单位高度差
    private int defaultPadding; //折线坐标图四周留出来的偏移量
    private float iconWidth;  //天气图标的边长
    private int viewHeight;
    private int viewWidth;
    private int screenWidth;
    private int screenHeight;

    private Paint linePaint; //线画笔
    private Paint textPaint; //文字画笔
    private Paint circlePaint; //圆点画笔

    private List<WeatherBean> data = new ArrayList<>(); //元数据
    private List<Pair<Integer, String>> weatherDatas = new ArrayList<>();  //对元数据中分组后的集合
    private List<Float> dashDatas = new ArrayList<>(); //之间虚线的x坐标集合
    private List<PointF> points = new ArrayList<>(); //折线拐点的集合
    private int maxTemperature;//元数据中的最高和最低温度
    private int minTemperature;

    private VelocityTracker velocityTracker;
    private Scroller scroller;
    private ViewConfiguration viewConfiguration;


    public MiuiWeatherView(Context context) {
        this(context, null);
    }

    public MiuiWeatherView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MiuiWeatherView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        scroller = new Scroller(context);
        viewConfiguration = ViewConfiguration.get(context);

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MiuiWeatherView);
        minPointHeight = (int) ta.getDimension(R.styleable.MiuiWeatherView_min_point_height, dp2pxF(context, 60));
        lineInterval = (int) ta.getDimension(R.styleable.MiuiWeatherView_line_interval, dp2pxF(context, 60));
        backgroundColor = ta.getColor(R.styleable.MiuiWeatherView_background_color, Color.WHITE);
        ta.recycle();

        setBackgroundColor(backgroundColor);

        initSize(context);

        initPaint(context);

    }

    /**
     * 初始化默认数据
     */
    private void initSize(Context c) {
        screenWidth = getResources().getDisplayMetrics().widthPixels;
        screenHeight = getResources().getDisplayMetrics().heightPixels;

        minViewHeight = 3 * minPointHeight;  //默认3倍
        pointRadius = dp2pxF(c, 2.5f);
        textSize = sp2pxF(c, 10);
        defaultPadding = (int) (0.5 * minPointHeight);  //默认0.5倍
        iconWidth = (1.0f / 3.0f) * lineInterval; //默认1/3倍
    }

    /**
     * 计算折线单位高度差
     */
    private void calculatePontGap() {
        int lastMaxTem = -Integer.MAX_VALUE;
        int lastMinTem = Integer.MAX_VALUE;
        for (WeatherBean bean : data) {
            if (bean.temperature > lastMaxTem) {
                maxTemperature = bean.temperature;
                lastMaxTem = bean.temperature;
            }
            if (bean.temperature < lastMinTem) {
                minTemperature = bean.temperature;
                lastMinTem = bean.temperature;
            }
        }
        float gap = (maxTemperature - minTemperature) * 1.0f;
        gap = (gap == 0.0f ? 1.0f : gap);  //保证分母不为0
        pointGap = (viewHeight - minPointHeight - 2 * defaultPadding) / gap;
    }

    private void initPaint(Context c) {
        linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        linePaint.setStrokeWidth(dp2px(c, 1));

        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setTextSize(textSize);
        textPaint.setColor(Color.BLACK);
        textPaint.setTextAlign(Paint.Align.CENTER);

        circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        circlePaint.setStrokeWidth(dp2pxF(c, 1));
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        initSize(getContext());
        calculatePontGap();
    }

    /**
     * 公开方法,用于设置元数据
     *
     * @param data
     */
    public void setData(List<WeatherBean> data) {
        if (data == null) {
            return;
        }
        this.data = data;
        notifyDataSetChanged();
    }

    public List<WeatherBean> getData(){
        return data;
    }

    public void notifyDataSetChanged(){
        if(data == null){
            return;
        }
        weatherDatas.clear();
        points.clear();
        dashDatas.clear();

        initWeatherMap(); //初始化相邻的分组
        requestLayout();
        invalidate();
    }
    

    /**
     * 根据元数据中连续相同的做分组,
     * pair中的first值为连续相同的数量,second值为对应天气
     */
    private void initWeatherMap() {
        weatherDatas.clear();
        String lastWeather = "";
        int count = 0;
        for (int i = 0; i < data.size(); i++) {
            WeatherBean bean = data.get(i);
            if (i == 0) {
                lastWeather = bean.weather;
            }
            if (bean.weather != lastWeather) {
                Pair<Integer, String> pair = new Pair<>(count, lastWeather);
                weatherDatas.add(pair);
                count = 1;
            } else {
                count++;
            }
            lastWeather = bean.weather;

            if (i == data.size() - 1) {
                Pair<Integer, String> pair = new Pair<>(count, lastWeather);
                weatherDatas.add(pair);
            }
        }

        for (int i = 0; i < weatherDatas.size(); i++) {
            int c = weatherDatas.get(i).first;
            String w = weatherDatas.get(i).second;
            Log.d("ccy", "weatherMap i =" + i + ";count = " + c + ";weather = " + w);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        if (heightMode == MeasureSpec.EXACTLY) {
            viewHeight = Math.max(heightSize, minViewHeight);
        } else {
            viewHeight = minViewHeight;
        }

        int totalWidth = 0;
        if (data.size() > 1) {
            totalWidth = 2 * defaultPadding + lineInterval * (data.size() - 1);
        }
        viewWidth = Math.max(screenWidth, totalWidth);  //默认控件最小宽度为屏幕宽度

        setMeasuredDimension(viewWidth, viewHeight);
        calculatePontGap();
        Log.d("ccy", "viewHeight = " + viewHeight + ";viewWidth = " + viewWidth);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (data.isEmpty()) {
            return;
        }
        drawAxis(canvas);

        drawLinesAndPoints(canvas);

        drawTemperature(canvas);

        drawWeatherDash(canvas);

        drawWeatherIcon(canvas);

    }

    /**
     * 画时间轴
     *
     * @param canvas
     */
    private void drawAxis(Canvas canvas) {
        canvas.save();
        linePaint.setColor(DEFAULT_GRAY);
        linePaint.setStrokeWidth(dp2px(getContext(), 1));

        canvas.drawLine(defaultPadding,
                viewHeight - defaultPadding,
                viewWidth - defaultPadding,
                viewHeight - defaultPadding,
                linePaint);

        float centerY = viewHeight - defaultPadding + dp2pxF(getContext(), 15);
        float centerX;
        for (int i = 0; i < data.size(); i++) {
            String text = data.get(i).time;
            centerX = defaultPadding + i * lineInterval;
            Paint.FontMetrics m = textPaint.getFontMetrics();
            canvas.drawText(text, 0, text.length(), centerX, centerY - (m.ascent + m.descent) / 2, textPaint);
        }
        canvas.restore();
    }

    /**
     * 画折线和它拐点的园
     *
     * @param canvas
     */
    private void drawLinesAndPoints(Canvas canvas) {
        canvas.save();
        linePaint.setColor(DEFAULT_BULE);
        linePaint.setStrokeWidth(dp2pxF(getContext(), 1));
        linePaint.setStyle(Paint.Style.STROKE);

        Path linePath = new Path(); //用于绘制折线
        points.clear();
        int baseHeight = defaultPadding + minPointHeight;
        float centerX;
        float centerY;
        for (int i = 0; i < data.size(); i++) {
            int tem = data.get(i).temperature;
            tem = tem - minTemperature;
            centerY = (int) (viewHeight - (baseHeight + tem * pointGap));
            centerX = defaultPadding + i * lineInterval;
            points.add(new PointF(centerX, centerY));
            if (i == 0) {
                linePath.moveTo(centerX, centerY);
            } else {
                linePath.lineTo(centerX, centerY);
            }
        }
        canvas.drawPath(linePath, linePaint); //画出折线

        //接下来画折线拐点的园
        float x, y;
        for (int i = 0; i < points.size(); i++) {
            x = points.get(i).x;
            y = points.get(i).y;

            //先画一个颜色为背景颜色的实心园覆盖掉折线拐角
            circlePaint.setStyle(Paint.Style.FILL_AND_STROKE);
            circlePaint.setColor(backgroundColor);
            canvas.drawCircle(x, y,
                    pointRadius + dp2pxF(getContext(), 1),
                    circlePaint);
            //再画出正常的空心园
            circlePaint.setStyle(Paint.Style.STROKE);
            circlePaint.setColor(DEFAULT_BULE);
            canvas.drawCircle(x, y,
                    pointRadius,
                    circlePaint);
        }
        canvas.restore();
    }

    /**
     * 画温度描述值
     *
     * @param canvas
     */
    private void drawTemperature(Canvas canvas) {
        canvas.save();

        textPaint.setTextSize(1.2f * textSize); //字体放大一丢丢
        float centerX;
        float centerY;
        String text;
        for (int i = 0; i < points.size(); i++) {
            text = data.get(i).temperatureStr;
            centerX = points.get(i).x;
            centerY = points.get(i).y - dp2pxF(getContext(), 13);
            Paint.FontMetrics metrics = textPaint.getFontMetrics();
            canvas.drawText(text,
                    centerX,
                    centerY - (metrics.ascent + metrics.descent)/2,
                    textPaint);
        }
        textPaint.setTextSize(textSize);
        canvas.restore();
    }

    /**
     * 画不同温度之间的虚线
     *
     * @param canvas
     */
    private void drawWeatherDash(Canvas canvas) {
        canvas.save();
        linePaint.setColor(DEFAULT_GRAY);
        linePaint.setStrokeWidth(dp2pxF(getContext(), 0.5f));
        linePaint.setAlpha(0xcc);

        //设置画笔画出虚线
        float[] f = {dp2pxF(getContext(), 5), dp2pxF(getContext(), 1)};  //两个值分别为循环的实线长度、空白长度
        PathEffect pathEffect = new DashPathEffect(f, 0);
        linePaint.setPathEffect(pathEffect);

        dashDatas.clear();
        int interval = 0;
        float startX, startY, endX, endY;
        endY = viewHeight - defaultPadding;

        //0坐标点的虚线手动画上
        canvas.drawLine(defaultPadding,
                points.get(0).y + pointRadius + dp2pxF(getContext(), 2),
                defaultPadding,
                endY,
                linePaint);
        dashDatas.add((float) defaultPadding);

        for (int i = 0; i < weatherDatas.size(); i++) {
            interval += weatherDatas.get(i).first;
            if(interval > points.size()-1){
                interval = points.size()-1;
            }
            startX = endX = defaultPadding + interval * lineInterval;
            startY = points.get(interval).y + pointRadius + dp2pxF(getContext(), 2);
            dashDatas.add(startX);
            canvas.drawLine(startX, startY, endX, endY, linePaint);
        }

        //这里注意一下,当最后一组的连续天气数为1时,是不需要计入虚线集合的,否则会多画一个天气图标
        //若不理解,可尝试去掉下面这块代码并观察运行效果
        if(weatherDatas.get(weatherDatas.size()-1).first == 1
                && dashDatas.size() > 1){
            dashDatas.remove(dashDatas.get(dashDatas.size()-1));
        }

        linePaint.setPathEffect(null);
        linePaint.setAlpha(0xff);
        canvas.restore();
    }


    /**
     

     * 若相邻虚线都在屏幕内,图标的x位置即在两虚线的中间

     * 若有一条虚线在屏幕外,图标的x位置即在屏幕边沿到另一条虚线的中间

     * 若两条都在屏幕外,图标x位置紧贴某一条虚线或屏幕中间
     

     * @param canvas

     */

    private void drawWeatherIcon(Canvas canvas) {

        canvas.save();

        textPaint.setTextSize(0.9f * textSize); //字体缩小一丢丢



        boolean leftUsedScreenLeft = false;

        boolean rightUsedScreenRight = false;



        int scrollX = getScrollX();  //范围控制在0 ~ viewWidth-screenWidth

        float left, right;

        float iconX, iconY;

        float textY;     //文字的x坐标跟图标是一样的,无需额外声明

        iconY = viewHeight - (defaultPadding + minPointHeight / 2.0f);

        textY = iconY + iconWidth / 2.0f + dp2pxF(getContext(), 10);

        Paint.FontMetrics metrics = textPaint.getFontMetrics();

        for (int i = 0; i < dashDatas.size() - 1; i++) {

            left = dashDatas.get(i);

            right = dashDatas.get(i + 1);



            //以下校正的情况为:两条虚线都在屏幕内或只有一条在屏幕内



            if (left < scrollX &&    //仅左虚线在屏幕外

                    right < scrollX + screenWidth) {

                left = scrollX;

                leftUsedScreenLeft = true;

            }

            if (right > scrollX + screenWidth &&  //仅右虚线在屏幕外

                    left > scrollX) {

                right = scrollX + screenWidth;

                rightUsedScreenRight = true;

            }



            if (right - left > iconWidth) {    //经过上述校正之后左右距离还大于图标宽度

                iconX = left + (right - left) / 2.0f;

            } else {                          //经过上述校正之后左右距离小于图标宽度,则贴着在屏幕内的虚线

                if (leftUsedScreenLeft) {

                    iconX = right - iconWidth / 2.0f;

                } else {

                    iconX = left + iconWidth / 2.0f;

                }

            }



            //以下校正的情况为:两条虚线都在屏幕之外



            if (right < scrollX) {  //两条都在屏幕左侧,图标紧贴右虚线

                iconX = right - iconWidth / 2.0f;

            } else if (left > scrollX + screenWidth) {   //两条都在屏幕右侧,图标紧贴左虚线

                iconX = left + iconWidth / 2.0f;

            } else if (left < scrollX && right > scrollX + screenWidth) {  //一条在屏幕左一条在屏幕右,图标居中

                iconX = scrollX + (screenWidth / 2.0f);

            }

            leftUsedScreenLeft = rightUsedScreenRight = false; //重置标志位

        }



        textPaint.setTextSize(textSize);

        canvas.restore();

    }

    private float lastX = 0;
    private float x = 0;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (velocityTracker == null) {
            velocityTracker = VelocityTracker.obtain();
        }
        velocityTracker.addMovement(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!scroller.isFinished()) {  //fling还没结束
                    scroller.abortAnimation();
                }
                lastX = x = event.getX();
                return true;
            case MotionEvent.ACTION_MOVE:
                x = event.getX();
                int deltaX = (int) (lastX - x);
                if (getScrollX() + deltaX < 0) {    //越界恢复
                    scrollTo(0, 0);
                    return true;
                } else if (getScrollX() + deltaX > viewWidth - screenWidth) {
                    scrollTo(viewWidth - screenWidth, 0);
                    return true;
                }
                scrollBy(deltaX, 0);
                lastX = x;
                break;
            case MotionEvent.ACTION_UP:
                x = event.getX();
                velocityTracker.computeCurrentVelocity(1000);  //计算1秒内滑动过多少像素
                int xVelocity = (int) velocityTracker.getXVelocity();
                if (Math.abs(xVelocity) > viewConfiguration.getScaledMinimumFlingVelocity()) {  //滑动速度可被判定为抛动
                    scroller.fling(getScrollX(), 0, -xVelocity, 0, 0, viewWidth - screenWidth, 0, 0);
                    invalidate();
                }
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            invalidate();
        }
    }

    //工具类
    public static int dp2px(Context c, float dp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, c.getResources().getDisplayMetrics());
    }

    public static int sp2px(Context c, float sp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, c.getResources().getDisplayMetrics());
    }

    public static float dp2pxF(Context c, float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, c.getResources().getDisplayMetrics());
    }

    public static float sp2pxF(Context c, float sp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, c.getResources().getDisplayMetrics());
    }
}

 

2.在values文件下创建attr布局

<resources>
    <declare-styleable name="MiuiWeatherView">
        <attr name="line_interval" format="dimension"/>
        <attr name="min_point_height" format="dimension"/>
        <attr name="background_color" format="color"/>
    </declare-styleable>
</resources>

 

3.创建一个bean类


WeatherBean


public class WeatherBean {

    public String weather;  //取值
    public int temperature; //温度值
    public String temperatureStr; //温度的描述值
    public String time; //时间值

    public WeatherBean( int temperature,String time) {

        this.temperature = temperature;
        this.time = time;
        this.temperatureStr = temperature + "℃";
    }
}


4.XML布局


<包名.MiuiWeatherView

        android:id="@+id/weather"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        app:line_interval="60dp"

        app:min_point_height="60dp"

        app:background_color="#ffffff"></包名.MiuiWeatherView>

 

5.MainActivity

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        MiuiWeatherView weatherView = (MiuiWeatherView) findViewById(R.id.weather);

        List<WeatherBean> data = new ArrayList<>();

        WeatherBean b1 = new WeatherBean(26,"00:00");
        WeatherBean b2 = new WeatherBean(28,"01:00");
        WeatherBean b3 = new WeatherBean(30,"02:00");
        WeatherBean b4 = new WeatherBean(34,"03:00");
        WeatherBean b5 = new WeatherBean(35,"04:00");
        WeatherBean b6 = new WeatherBean(36,"05:00");
        WeatherBean b7 = new WeatherBean(37,"06:00");
        WeatherBean b8 = new WeatherBean(38,"07:00");
        WeatherBean b9 = new WeatherBean(39,"08:00");
        WeatherBean b10 = new WeatherBean(36,"09:00");
        WeatherBean b11= new WeatherBean(37,"10:00");
        WeatherBean b12= new WeatherBean(38,"11:00");
        WeatherBean b13= new WeatherBean(34,"12:00");
        WeatherBean b14= new WeatherBean(36,"13:00");
        WeatherBean b15= new WeatherBean(35,"14:00");
        WeatherBean b16= new WeatherBean(37,"15:00");
        WeatherBean b17= new WeatherBean(40,"16:00");
        WeatherBean b18= new WeatherBean(36,"17:00");
        WeatherBean b19= new WeatherBean(38,"18:00");
        WeatherBean b20= new WeatherBean(35,"19:00");
        WeatherBean b21= new WeatherBean(37,"20:00");
        WeatherBean b22= new WeatherBean(34,"21:00");
        WeatherBean b23= new WeatherBean(36,"22:00");
        WeatherBean b24= new WeatherBean(38,"23:00");

        //...b3、b4......bn

        data.add(b1);
        data.add(b2);
        data.add(b3);
        data.add(b4);
        data.add(b5);
        data.add(b6);
        data.add(b7);
        data.add(b8);
        data.add(b9);
        data.add(b10);
        data.add(b11);
        data.add(b12);
        data.add(b13);
        data.add(b14);
        data.add(b15);
        data.add(b16);
        data.add(b17);
        data.add(b18);
        data.add(b19);
        data.add(b20);
        data.add(b21);
        data.add(b22);
        data.add(b23);
        data.add(b24);

        weatherView.setData(data);
    }