撸一个简单的TV版焦点控制的日历控件

1、效果

最近需求要一个遥控控制的日历控件,找了半天没找到轮子,就自己撸一个,先看效果图:


效果图.gif

2、XML属性,所有属性默认为效果图

  • calender_textSize:星期和日期的字体大小;
  • calender_textColor:日期的字体颜色;
  • calender_currentDayColor:当天日期的颜色;
  • calender_focusDrawable:有焦点的日期的背景图,即日期遥控选中效果;
  • calender_startAndEndPadding:左右的起步距离;
  • calender_horizontalInterval:水平间距的大小;
  • calender_verticalInterval:垂直间距的大小;
  • calender_weekTextColor: 星期的字体颜色;
  • calender_headBg:头部的背景图(选择器有焦点和无焦点效果);
  • calender_headTextSize:头部日期字体大小;
  • calender_headTextColor:头部日期字体颜色;
  • calender_focusTextColor:有焦点的日期的字体颜色,即日期遥控选中效果;

3、抱着学习心态撸这个控件,写这篇的目的是记录分享下这个TvCalenderView

先讲个大概,TvCalenderView继承LinearLayout垂直排序,三个子View,先后为HeadView,WeekView,MonthView,就贴一些关键点。

  • MonthView:
    先看下onDraw,关键地方写了注释:

      @Override
      protected void onDraw(Canvas canvas) {
          super.onDraw(canvas);
          mTempDay = mDay;  //临时变量记录当前的焦点日期
          for (int day = 0; day < mDaysCountOfMonth; day++) {
              /**
               *  mFirstDayOfWeek 这月第一天是星期几,比如,效果图第一天是周二,mFirstDayOfWeek为3,
               *  因为是从1开始,周二就是3,那么,按照下面的公式,算错来的row为0,column为2,两者都是
               *  重0开始,1号的位置就是(0,2)
               */
              int row = (day + mFirstDayOfWeek - 1)/7;
              int column = (day+mFirstDayOfWeek - 1) % 7;
              mDayStr[row][column] = day+1;   //记录每个月日期的行列位置
              String dayStr = String.valueOf(day+1);
              if (day == 0 && isChangeMonth){  //初始化焦点位置用,默认切换月份1号有焦点
                  isChangeMonth = false;
                  mCurRow = row;   //一开始在0行
                  mCurColumn = column;  //一开始在0列
              }
              if (day+1 == mDay){
                  if (mHasFocus){   //是选中日期,且有焦点
                      mTextPaint.setColor(mTextColor);
                      Rect rect = new Rect();
                      mTextPaint.getTextBounds(dayStr, 0, dayStr.length(), rect);
                      int height = rect.height();//文字高
                      //水平位置:起步+列数*列间距+文字大小*列间距+文字的一半-图片的一半
                      //垂直位置:起步+行数*水平间距+文字大小*(行数+1)- 文字高度/2 - 图片的一半
                      //说明一下,垂直位置,文字大小*(行数+1),+1是因为drawText传入的位置是左下角
                      //drawBitmap传入的位置的图片中心,所以要-文字高度/2
                      canvas.drawBitmap(mBitmap,padding+column*verticalInterval+textSize*column+ mTextPaint.measureText("10") / 2-mBitmap.getWidth()/2,
                              startTop+row * horizontalInterval + textSize * (row + 1) - height / 2-mBitmap.getHeight()/2,mFocusBgPaint);
                      canvas.drawText(dayStr, padding + mTextPaint.measureText("10")/2+column * verticalInterval +textSize*column - mTextPaint.measureText(dayStr) / 2,
                              startTop+ row * horizontalInterval + textSize * (row + 1), mFocusTextPaint);
                  }else {
                      drawDay(canvas, day, row, column, dayStr);
                  }
              }else {
                  drawDay(canvas, day, row, column, dayStr);
              }
          }
      }
    
      private void drawDay(Canvas canvas, int day, int row, int column, String dayStr) {
          mTextPaint.setColor(day+1 == mCurrDay && mYear == mCurrYear && mMonth == mCurrMonth ? mCurrentDayColor : mTextColor);
          canvas.drawText(dayStr, padding + mTextPaint.measureText("10")/2+column * verticalInterval +textSize*column - mTextPaint.measureText(dayStr) / 2,
                  startTop+row * horizontalInterval + textSize * (row + 1), mTextPaint);
      }  
    

    图片讲解一波:
    例如日期8:
    8的位置的第1行,第2列(都是0开始),那么画8这个字:
    水平方向:
    一个起步+两个垂直距离+两个文字大小,为什么是两个不是三个,因为drawText左下角开始画,再-mTextPaint.measureText(dayStr) / 2,举个例子,1和10,两个默认画出来是左对齐的,这就不美观了,要居中对齐,所以就大家都减去文字宽度的一半,这样就居中对齐了,最后再加 mTextPaint.measureText("10")/2,这一步主要是要整体居中,因为前面--mTextPaint.measureText(dayStr) / 2使得整体偏左,这点可以开下开发者模式显示布局边界试下;
    垂直方向:
    一个起步+一个水平间距+两个文字大小
    这样子,画背景图就猫画虎了,都是计算的东西,就不详细讲,自己体会下


    8.png

接下来看onkeyDown:
处理遥控器的事件:
mCurRow和mCurColumn在onDraw拿到1号的位置,然后根据遥控事件拿到对应的日期,再invalidate重新绘制:

  //遥控事件
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_UP:
                if (mCurRow <= 0) {   //如果是在第0行,再按上键的话,这时MonthView会失去焦点
                    return super.onKeyDown(keyCode, event);
                }
                mCurRow--;
                mDay = mDayStr[mCurRow][mCurColumn]; //拿出存的day,后面调用invalidate重新绘制
                break;
            case KeyEvent.KEYCODE_DPAD_DOWN:
                if (mCurRow >= mDayStr.length - 1) {  //下限
                    return super.onKeyDown(keyCode, event);
                }
                mCurRow++;
                mDay = mDayStr[mCurRow][mCurColumn];
                if (mDay == 0) {  //拿不到的情况,比如效果图25号下面没有日期
                    mDay = mTempDay;  //mTempDay记录了上一次的日期
                    mCurRow--;   //上面加了拿不到,减回去
                }
                break;
            case KeyEvent.KEYCODE_DPAD_LEFT:
                if (mCurColumn <= 0) {
                    preMonth();     //往左到界限,跳到上一个月
                    return super.onKeyDown(keyCode, event);
                }
                mCurColumn--;
                mDay = mDayStr[mCurRow][mCurColumn];
                if (mDay == 0) {   //拿不到的情况,比如效果图1号左边没有日期
                    preMonth();
                    return super.onKeyDown(keyCode, event);
                }
                break;
            case KeyEvent.KEYCODE_DPAD_RIGHT:
                if (mCurColumn >= mDayStr[0].length - 1) {
                    nextMonth();  //往右到界限,跳到下一个月
                    return super.onKeyDown(keyCode, event);
                }
                mCurColumn++;
                mDay = mDayStr[mCurRow][mCurColumn];
                if (mDay == 0) {   //拿不到的情况,比如效果图31号右边没有日期
                    nextMonth();
                    return super.onKeyDown(keyCode, event);
                }
                break;
            case KeyEvent.KEYCODE_ENTER:  //确认按钮
                if (mOnDateSeletedListener != null) {
                    mOnDateSeletedListener.onDateSelected(mYear, mMonth, mDay);
                }
                return super.onKeyDown(keyCode, event);
            default:
                return super.onKeyDown(keyCode, event);
        }
        invalidate();
        return true;
    }

    public void nextMonth() {
        if (mMonth >= 11) {
            mMonth = 0;
            mYear++;
            setYearAndMonth(mYear, mMonth);  //换月份
            ((TvCalenderView) getParent()).changeHeadData(mMonth, mYear);  //调用改变头部的日期
            return;
        }
        mMonth++;
        setYearAndMonth(mYear, mMonth);
        ((TvCalenderView) getParent()).changeHeadData(mMonth, mYear);
    }
  • WeekView不讲了,HeadView就是组合控件,主要帖下遥控事件:

       @Override
          public boolean onKeyDown(int keyCode, KeyEvent event) {
              switch (keyCode){
                  case KeyEvent.KEYCODE_DPAD_LEFT:
                      mIvPre.setImageResource(R.drawable.date_last_focused);
                      ((TvCalenderView) getParent()).preMonth();  //按左刷新Month
                      mHandler.postDelayed(new Runnable() {
                          @Override
                          public void run() {
                              mIvPre.setImageResource(R.drawable.date_last_normal);   //伪装点击效果
                          }
                      },100);
                      break;
                  case KeyEvent.KEYCODE_DPAD_RIGHT:
                      mIvNext.setImageResource(R.drawable.date_next_focused);
                      ((TvCalenderView) getParent()).nextMonth();
                      mHandler.postDelayed(new Runnable() {
                          @Override
                          public void run() {
                              mIvNext.setImageResource(R.drawable.date_next_normal);
                          }
                      },100);
                      break;
              }
              return super.onKeyDown(keyCode, event);
          }
    

最后

贴上Demo地址:
https://github.com/CzdCoder/TvCalenderView
分享下自定义View学习系列:
Android 开发进阶: 自定义 View 1-1 绘制基础:http://hencoder.com/ui-1-1/
Android 开发进阶: 自定义 View 1-2 Paint 详解:http://hencoder.com/ui-1-2/
Android 开发进阶:自定义 View 1-3 文字的绘制:http://hencoder.com/ui-1-3/
Android 开发进阶:自定义 View 1-4 Canvas 对绘制的辅助:http://hencoder.com/ui-1-4/
Android 开发进阶:自定义 View 1-5 绘制顺序:http://hencoder.com/ui-1-5/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容