美文网首页自定义程序员Android知识
自定义view之实现日历界面(二)

自定义view之实现日历界面(二)

作者: sakasa | 来源:发表于2017-03-20 12:00 被阅读179次

    原文地址

    上一篇文章中讲过的内容有两点需要复习一下:

    1. 划分每天占的方格。这个首先是计算本月当前日期总天数加上第一天是星期几然后减去1然后对应的上月的天数即可算出本月赢有多少行,通过行列的分割计算出每天的方格中心坐标点。
    2. 为显示的每天设定标记。目前我们做的标记有上一个月、本月、下一个月、周末、特殊日期。

    我们将上面两点获得的信息全部保存在一个数组对象中,这个数组对象就是我们用来绘制的基准。

    绘制日历界面

    view的绘制都是在onDraw方法中,在ondraw方法中会传递一个Canvas参数,这个就是我们需要绘制的画布。在这里我们需要绘制出所有的天的状态。

    在绘制的过程中,我们需要根据每天的标记(State)来分情况绘制,所以此处要做一个便利循环来去除出每天的内容:

    private void drawDays(Canvas canvas, CellDay[] cellDays) {
            for (CellDay c : cellDays) {
                switch (c.getDayState()) {
                    case LASTMONTH:
                        if (c.isSelected()) {
                            circlePaint.setColor(Color.TRANSPARENT);
                        } else {
                            circlePaint.setColor(Color.TRANSPARENT);
                            textPaint.setColor(Color.GRAY);
                        }
                        break;
                    case CURRENTMONTH:
                        if (c.isSelected()) {
                            circlePaint.setColor(Color.YELLOW);
                            textPaint.setColor(Color.BLACK);
                        } else {
                            circlePaint.setColor(Color.RED);
                            textPaint.setColor(Color.RED);
                            canvas.drawText("班",
                                    c.getPointX() + textPaint.measureText(tempDate) / 2,
                                    c.getPointY() - textPaint.getTextSize() / 2,
                                    textPaint);
                        }
                        break;
                    case NEXTMONTH:
                        if (c.isSelected()) {
                            circlePaint.setColor(Color.TRANSPARENT);
                        } else {
                            circlePaint.setColor(Color.TRANSPARENT);
                            textPaint.setColor(Color.GRAY);
                        }
                        break;
                    case CURRENTDAY:
                        circlePaint.setColor(Color.YELLOW);
                        textPaint.setColor(Color.BLACK);
                        break;
                    case WEEKEND:
                        circlePaint.setColor(Color.CYAN);
                        textPaint.setColor(Color.CYAN);
                        canvas.drawText("休",
                                c.getPointX() + textPaint.measureText(tempDate) / 2,
                                c.getPointY() - textPaint.getTextSize() / 2,
                                textPaint);
                        break;
                    case SPECIALDAY:
                        circlePaint.setColor(Color.GREEN);
                        textPaint.setColor(Color.GREEN);
                        canvas.drawText("假",
                                c.getPointX() + textPaint.measureText(tempDate) / 2,
                                c.getPointY() - textPaint.getTextSize() / 2,
                                textPaint);
                }
                canvas.drawCircle(tempPositionX, tempPositionY, radius - 10, selectPaint);
    //            canvas.drawText(tempDate,
    //                    tempPositionX - textPaint.measureText(tempDate) / 2,
    //                    tempPositionY + textPaint.getTextSize() / 2,
    //                    selectTextPaint);
                canvas.drawText(c.getDate(),
                        c.getPointX() - textPaint.measureText(c.getDate()) / 2,
                        c.getPointY() + textPaint.getTextSize() / 2,
                        textPaint);
    //            canvas.drawCircle(c.getPointX(), c.getPointY(), radius - 10, circlePaint);
                //这个地方改为rectf可以向下兼容
                canvas.drawArc(c.getPointX() - radius + 10,
                        c.getPointY() - radius + 10,
                        c.getPointX() + radius - 10,
                        c.getPointY() + radius - 10,
                        0, 270, false, circlePaint);
                c.setSelected(false);
                oldPositionX = newPositionX;
                oldPositionY = newPositionY;
            }
        }
    

    对于是否选中的状态我们采用的是特殊的标记,所以没有在siwtch中直接使用,而是通过if来细分每个switch的状态。在LASTMONTH、NEXTMONTH中我们对画笔做了透明处理,这样做的目的是防止在选中的状态下选中状态是透明的,那么也就不会产生视觉高亮效果。(后面会讲如何处理上个月和下个月不被选中)。

    case SPECIALDAY:
                        circlePaint.setColor(Color.GREEN);
                        textPaint.setColor(Color.GREEN);
                        canvas.drawText("假",
                                c.getPointX() + textPaint.measureText(tempDate) / 2,
                                c.getPointY() - textPaint.getTextSize() / 2,
                                textPaint);
    

    在SPECIALDAY中我们直接绘制了特殊日期显示的文字。当然此处可以传任意参数进来作为绘制文字,在后期的改进中我会将这个作为接口供使用者调用。

    canvas.drawText(c.getDate(),
                        c.getPointX() - textPaint.measureText(c.getDate()) / 2,
                        c.getPointY() + textPaint.getTextSize() / 2,
                        textPaint);
    //            canvas.drawCircle(c.getPointX(), c.getPointY(), radius - 10, circlePaint);
                //这个地方改为rectf可以向下兼容
                canvas.drawArc(c.getPointX() - radius + 10,
                        c.getPointY() - radius + 10,
                        c.getPointX() + radius - 10,
                        c.getPointY() + radius - 10,
                        0, 270, false, circlePaint);
    

    这段代码是用来绘制日期的数字和一个半圆的效果。

    c.setSelected(false);
    

    这段代码的作用是每次选中后清除选中状态,防止下次同时选中两个或者多个。此处可以延伸,作为网上较流行的选中多日期的状态表示。

    至此,我们的界面基本完成了。看一下效果图:

    本月的绘制完美完成。

    设置点击事件

    此处我们需要重写public boolean onTouchEvent(MotionEvent event)方法。

    此处我们假定自定义view的事件不会被viewgroup拦截,则触摸事件的顺序依次为:
    MotionEvent.ACTION_DOWN->MotionEvent.ACTION_MOVE->MotionEvent.ACTION_UP

    我发现我特别懒,所以我只计算了DOWN和UP事件之间移动的距离

    case MotionEvent.ACTION_DOWN:
                    touchRawX = event.getX();
                    touchRawY = event.getY();
                    break;
    

    记录原始按下的点的坐标位置。

    case MotionEvent.ACTION_UP:
                    Log.d(TAG, "MotionEvent:ACTION_UP");
                    Log.d(TAG, "rawY= " + touchRawY + ",touchY= " + event.getY());
                    float touchX = event.getX();
                    float touchY = event.getY();
                    if (touchRawY - touchY < -200) {
                        //下滑事件
                        Log.d(TAG, "下滑事件");
                        setMaxView();
                        setCellDay();
                        this.clearCanvas = false;
                        invalidate();
                        cutGrid();
                        init();
                        this.layout(0, 0, this.viewWidth, this.viewHeight / 2);
                        setCellDay();
                        invalidate();
                    }
                    if (touchRawY - touchY > 200) {
                        //上划事件
                        Log.d(TAG, "上划事件");
                        setMinView();
                        this.clearCanvas = true;
                        invalidate();
                    }
                    if (Math.abs(touchRawX - touchX) < 100 && Math.abs(touchY - touchRawY) < 100) {
                        //点击事件
                        Log.d(TAG, "点击事件");
                        int touchRow = (int) (touchX / cellWidth);
                        int touchLine = (int) (touchY / cellHeight);
                        final int touchId = touchLine * ROW_COUNT + touchRow;
                        if (canClickNextOrPreMonth) {
                            setClickEvent(touchId);
                        } else {
                            if (touchId > firstDayOfWeek - 2 && touchId < monthDaySum + firstDayOfWeek - 1) {
                                setClickEvent(touchId);
                                tempCellDay = cellDays[touchId];
                                tempState = cellDays[touchId].getDayState();
                                cellDays[touchId].setSelected(true);
                                newPositionX = cellDays[touchId].getPointX();
                                newPositionY = cellDays[touchId].getPointY();
                                setselectAnimator(touchId);
                            }
                        }
    
                    }
                   break;
    

    在up事件中,首先根据触摸的位置换算为touchId,也就是在数组对象cellDays中的索引。然后做了三种状态判断,上滑下滑和点击事件,其中上滑和下滑事件可以先不管,因为到最后你们都不会用到。讲一下点击事件中的内容,首先判断是不是设置了上月或者下月是否可以点击,canClickNextOrPreMonth为真,则可以点击,为假则不可以点击。为假的时候,我们只需要再设置一个条件,即点击的坐标点没有落在上月或下月的索引内。
    先看如何设置点击事件:

    private void setClickEvent(int touchId) {
            CustomDate customDate = cellDays[touchId].getCustomDate();
            if (clickCellListener != null) {
                clickCellListener.onClickCell(customDate);
            }
        }
    

    在这个方法中传进了一个int值,这个值就是cellday数组的索引值。
    只要实现接口即可获得这个传出的customDate。

    最后看setSelectAnimator方法:

    private void setselectAnimator(final int touchId) {
            selectAnimatorX.removeAllUpdateListeners();
            selectAnimatorX.setFloatValues(oldPositionX, newPositionX);
            selectAnimatorX.setDuration(300);
            selectAnimatorX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    tempPositionX = (float) animation.getAnimatedValue();
                }
            });
            selectAnimatorY.removeAllUpdateListeners();
            selectAnimatorY.setFloatValues(oldPositionY, newPositionY);
            selectAnimatorY.setDuration(300);
            selectAnimatorY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    tempPositionY = (float) animation.getAnimatedValue();   
                    postInvalidate();
                }
            });
            animatorSet = new AnimatorSet();
            animatorSet.playTogether(selectAnimatorX, selectAnimatorY);
            animatorSet.start();
        }
    

    这个方法看我文章的应该都很熟悉了,做一个valueanimator来作为世间引擎不断引起XY坐标的变化,产生一个动画移动的效果。然后调用inalidate作为标记,刷新界面。

    此时看一下效果图:


    至此,我们日历的点击、选中状态改变、特殊日期、周末等高亮显示都已经完成。

    滑动至上一个月和下一个月

    我是利用了viewpager来实现滑动,我假定大家都对viewpager的使用很擅长。此处只看一下代码即可:

    private class CalendarAdapter extends PagerAdapter {
            private MyCalendar calendar;
            private LayoutParams lp;
    
            public CalendarAdapter(MyCalendar calendar) {
                this.calendar = calendar;
            }
    
            public CalendarAdapter(MyCalendar calendar, LayoutParams lp) {
                this.calendar = calendar;
                this.lp = lp;
            }
    
            @Override
            public int getCount() {
                return 1000;
            }
    
            @Override
            public boolean isViewFromObject(View view, Object object) {
                return view == object;
            }
    
            @Override
            public void destroyItem(ViewGroup container, int position, Object object) {
    
            }
    
            @Override
            public Object instantiateItem(ViewGroup container, int position) {
                if (myCalendars.size() > position) {
                    MyCalendar myCalendar = myCalendars.get(position);
                    if (myCalendar != null) {
                        return myCalendar;
                    }
                }
                calendar = new MyCalendar(context);
                calendar.setClickCellListener(MyViewPager.this);
                customeDate = new CustomDate(position / 12 + 2000, position % 12, 1);
                calendar.setDate(customeDate);
                calendar.setLayoutParams(lp);
                calendar.setWeekendHighLight(weekendHightLight);
                calendar.setSpecialDay(new int[]{10, 20});
                calendar.setCanClickNextOrPreMonth(false);
                while (myCalendars.size() <= position) {
                    myCalendars.add(null);
                }
                myCalendars.set(position, calendar);
                ViewParent vp = calendar.getParent();
                if (vp != null) {
                    ViewGroup parent = (ViewGroup) vp;
                    parent.removeView(calendar);
                }
                container.addView(calendar);
                return calendar;
            }
    
    
        }
    

    这里是前一个取巧的方式,我设定的是1000个月,从2000年开始计算。然后根据position来计算是哪个月,customeDate = new CustomDate(position / 12 + 2000, position % 12, 1);

    就是这段代码,然后将calendar的接口全都设置了一下。

    再来看一下如何使用:

    viewPager.setCurrentItem((calendar.get(Calendar.YEAR) - 2000) * 12 + calendar.get(Calendar.MONTH));
    

    这个设置当前日期我并未封装到calendar中,正在不断改进。

    再为viewpager添加一个ViewPager.OnPageChangeListener监听器。

     public void onPageSelected(int position) {
            StringBuilder builder = new StringBuilder();
            builder.append(position / 12 + 2000);
            builder.append("年");
            builder.append(position % 12 + 1);
            builder.append("月");
            date.setText(builder.toString());
        }
    

    在这里设置显示的头的日期。

    好了,所有的工作完成了。

    讲的不是很好,思路基本都是这样实现的,有问题可以直接联系我。

    相关文章

      网友评论

      本文标题:自定义view之实现日历界面(二)

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