自定义View合辑(9)-计划表

作者: 业志陈 | 来源:发表于2019-06-11 09:09 被阅读20次

    为了加强对自定义 View 的认知以及开发能力,我计划这段时间陆续来完成几个难度从易到难的自定义 View,并简单的写几篇博客来进行介绍,所有的代码也都会开源,也希望读者能给个 star 哈
    GitHub 地址:https://github.com/leavesC/CustomView
    也可以下载 Apk 来体验下:https://www.pgyer.com/CustomView

    先看下效果图:

    一、思路解析

    这是一个类似于课程表的自定义 View,横向和纵向均是以时间作为计量单位,通过设置当前计划处于哪个星期数下以及跨度时间,在该范围内绘制出相应的背景以及文本

    PlanBean 中有两个比较重要字段,一个是该计划的绘制范围,即坐标系 rectF,另一个字段 isEllipsis 是用于标记当前计划的文本是否以省略的形式出现

    public class PlanBean {
    
        private String planId;
        private String planName;
        private String planStartTime;
        private String planEndTime;
        private String color;
        private int dayIndex;
        
        //计划的坐标
        private RectF rectF;
        //文本是否被省略
        private boolean isEllipsis;
    
    }
    

    边框以及时间文本的绘制比较简单,只需要计算出各个起始点和终点的坐标系即可

    @Override
        protected void onDraw(Canvas canvas) {
            //先画背景
            bgPaint.setStyle(Paint.Style.FILL);
            bgPaint.setColor(Color.WHITE);
            canvas.drawRect(0, 0, width, realHeight, bgPaint);
    
            //画左边和上边的边框
            bgPaint.setColor(rectColor);
            bgPaint.setStyle(Paint.Style.FILL);
            canvas.drawRect(0, 0, leftTimeWidth, height, bgPaint);
            canvas.drawRect(leftTimeWidth, 0, width, headerHeight, bgPaint);
    
            //画线
            canvas.save();
            canvas.translate(leftTimeWidth, 0);
            bgPaint.setColor(lineColor);
            bgPaint.setStrokeWidth(getResources().getDisplayMetrics().density);
            for (int i = 0; i < 7; i++) {
                canvas.drawLine(itemWidth * i, 0, itemWidth * i, height, bgPaint);
            }
            canvas.translate(0, headerHeight);
            for (int i = 0; i < 20; i++) {
                canvas.drawLine(0, i * itemHeight, width - leftTimeWidth + 2, i * itemHeight, bgPaint);
            }
            canvas.restore();
    
            //画星期数
            canvas.save();
            canvas.translate(leftTimeWidth, 0);
            bgPaint.setTextSize(sp2px(DAY_TEXT_SIZE));
            bgPaint.setColor(Color.BLACK);
            bgPaint.setTextAlign(Paint.Align.CENTER);
            for (String day : DAYS) {
                bgPaint.getTextBounds(day, 0, day.length(), textBounds);
                float offSet = (textBounds.top + textBounds.bottom) >> 1;
                canvas.drawText(day, itemWidth / 2, headerHeight / 2 - offSet, bgPaint);
                canvas.translate(itemWidth, 0);
            }
            canvas.restore();
    
            //画时间
            for (int i = 0; i < TIMES.length; i++) {
                String time = TIMES[i];
                bgPaint.getTextBounds(time, 0, time.length(), textBounds);
                float offSet = (textBounds.top + textBounds.bottom) >> 1;
                canvas.drawText(time, leftTimeWidth / 2, headerHeight + itemHeight * i - offSet, bgPaint);
            }
    
           ···
        }
    

    难点在于需要判断计划名的文本高度是否超出了其本身的高度,如果超出了则截断文本,并用省略号结尾
    。可是 canvas.drawText() 方法本身是无法获取到文本高度以及自动换行的,此时就需要用到 StaticLayout 了,可以设置最大文本宽度,其内部实现了文本自动换行的功能,并且可以获取到文本换行后的整体高度,通过这个就可以完成想要的效果

        if (planListBeanList != null && planListBeanList.size() > 0) {
                for (PlanBean bean : planListBeanList) {
                    bgPaint.setColor(Color.parseColor(bean.getColor()));
                    measurePlanBound(bean, planRectF);
                    canvas.drawRect(planRectF, bgPaint);
                    String planName = bean.getPlanName();
                    if (TextUtils.isEmpty(planName)) {
                        continue;
                    }
                    float planItemHeight = planRectF.bottom - planRectF.top;
                    StaticLayout staticLayout = null;
                    for (int length = planName.length(); length > 0; length--) {
                        staticLayout = new StaticLayout(planName, planTextPaint, (int) (itemWidth - 4 * getResources().getDisplayMetrics().density),
                                Layout.Alignment.ALIGN_CENTER, 1.1f, 1.1f, true);
                        if (staticLayout.getHeight() > planItemHeight) {
                            planName = planName.substring(0, length) + "...";
                            bean.setEllipsis(true);
                        }
                    }
    
                    if (staticLayout == null) {
                        staticLayout = new StaticLayout(planName, planTextPaint, (int) (itemWidth - 4 * getResources().getDisplayMetrics().density),
                                Layout.Alignment.ALIGN_CENTER, 1.1f, 1.1f, true);
                    }
    
                    if (staticLayout.getHeight() > planItemHeight) {
                        continue;
                    }
    
                    canvas.save();
    
                    canvas.translate(planRectF.left + (itemWidth - staticLayout.getWidth()) / 2, planRectF.top + (planItemHeight - staticLayout.getHeight()) / 2);
    
                    staticLayout.draw(canvas);
                    canvas.restore();
                }
            }
    

    由于 StaticLayout 并没有向外提供设置整体最大高度的 API ,所以需要自己来循环判断文本的整体高度是否已经超出最大高度,是的话则对文本进行截取。如果最大高度太小,无法容纳一行文本,则直接不绘制文本

                   for (int length = planName.length(); length > 0; length--) {
                        staticLayout = new StaticLayout(planName, planTextPaint, (int) (itemWidth - 4 * getResources().getDisplayMetrics().density),
                                Layout.Alignment.ALIGN_CENTER, 1.1f, 1.1f, true);
                        if (staticLayout.getHeight() > planItemHeight) {
                            planName = planName.substring(0, length) + "...";
                            bean.setEllipsis(true);
                        }
                    }
    
                    if (staticLayout == null) {
                        staticLayout = new StaticLayout(planName, planTextPaint, (int) (itemWidth - 4 * getResources().getDisplayMetrics().density),
                                Layout.Alignment.ALIGN_CENTER, 1.1f, 1.1f, true);
                    }
    
                    if (staticLayout.getHeight() > planItemHeight) {
                        continue;
                    }
    

    另外一个比较重要的点是需要通过计划的时间跨度来计算其坐标系,并将坐标系存储下来,方便判断点击事件

        private void measurePlanBound(PlanBean bean, RectF rect) {
            measurePlanBound(bean.getDayIndex(), bean.getPlanStartTime(), bean.getPlanEndTime(), rect);
            RectF rectF = new RectF(rect);
            bean.setRectF(rectF);
        }
    
        private void measurePlanBound(int day, String startTime, String endTime, RectF rect) {
            try {
                float left = leftTimeWidth + itemWidth * (day - 1);
                float right = left + itemWidth;
                String[] split = startTime.split(":");
                int startHour = Integer.parseInt(split[0]);
                int startMinute = Integer.parseInt(split[1]);
                float top = ((startHour - START_TIME) * 60 + startMinute) * singleMinuteHeight + headerHeight;
                split = endTime.split(":");
                int endHour = Integer.parseInt(split[0]);
                int endMinute = Integer.parseInt(split[1]);
                float bottom = ((endHour - START_TIME) * 60 + endMinute) * singleMinuteHeight + headerHeight;
    
                float offset = 1;
    
                rect.set(left + offset, top + offset, right - offset, bottom - offset);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    相关文章

      网友评论

        本文标题:自定义View合辑(9)-计划表

        本文链接:https://www.haomeiwen.com/subject/ycywxctx.html