自定义view学习-手把手教你制作一个可扩展日历控件


来看看效果图先,手把手教你实现一个简易,但高扩展度的日历控件,可自由扩展成签到,单选,多选日期。

首先我们来分析实现思路。对于上图的效果,很明显是一个6x7的表格。

我们可以两个for循环控制绘制每个元素,第一行特殊处理,绘制出星期符号。

接下来确定1号在星期几,这个月最大有几号,依次循环绘制就行。

对于日历控件,不可避免要采取这个类:Calendar 这是java为我们提供的日历相关,这是最简单的实现日历方案。

对于这个类,我们主要用到下面的方法

Calendar c = Calendar.getInstance();
 c.set(Calendar.DATE,1);
 int start = c.get(Calendar.DAY_OF_WEEK);//获得1号是星期几
 int maxDay = c.getActualMaximum(Calendar.DATE);//获得当前月的最大日期数
 c.get(Calendar.YEAR)//x年
 c.get(Calendar.MONTH)+1;//x月
通过这些方法,我们拥有了1号是星期几,最大是几号。


获取到上面的参数后,我们可以开始正式的绘制

新建一个类,继承view,实现三个构造方法,重写onDraw

我们先在中间画一个正方形,边长为view宽和高两个之间较小那个。

然后分割为7x7的49个小正方形。(对于这里有问题的,参考我上一篇博客九宫格解锁那个)

然后,两个for循环~先把第一行星期标识符画出来

 mPaint.setColor(Color.BLACK);
        mPaint.setTextAlign(Paint.Align.CENTER);
        mPaint.setTextSize(tWidth / 2);//字体大小,单位px
        Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
        //开始绘制
        for(int j=0;j<7;j++){
            int tY=startY+j*tWidth+tWidth/2;
            for(int k=0;k<7;k++){
                int tX=startX+k*tWidth+tWidth/2;
                //tX,tY为每个cell的中心
                if(j==0){//画星期标示符
                    int baseline=tY+((fontMetrics.bottom - fontMetrics.top)/2-fontMetrics.bottom);
                    canvas.drawText(weekString[k],tX,baseline,mPaint);
                }
这里有一个难点,关于文字的绘制,请参考这篇博客:http://blog.csdn.net/hursing/article/details/18703599

写的很详细,也很好。

 int nDay=2-start-7;//用这个获取到这个元素的"日期"
if(nDay>0 && nDay<=maxDay){//绘画日期
                    //回调接口获得cell的元素
                    CalendarCell cell=mOnDrawCellListener.onDrawCell(c.get(Calendar.YEAR),c.get(Calendar.MONTH)+1,nDay,k);
                    mPaint.setColor(cell.backgroundColor);
                    canvas.drawCircle(tX, tY, tWidth / 3, mPaint);//画背景圆圈
                    mPaint.setColor(cell.textColor);
                    int baseline=tY+((fontMetrics.bottom - fontMetrics.top)/2-fontMetrics.bottom);
                    canvas.drawText(cell.text, tX, baseline, mPaint);
                }
                nDay++;
根据nday变量控制绘制日期。这里有一个
mOnDrawCellListener.onDrawCell
这是为了自定义扩展所定义的接口,CalendarCell 这个类是这样的

class CalendarCell{
    int backgroundColor;//背景色,背景画圆圈
    int textColor;//文字颜色
    String text;//画什么字

    public CalendarCell(int backgroundColor, int textColor, String text) {
        this.backgroundColor = backgroundColor;
        this.textColor = textColor;
        this.text = text;
    }
}
用这个类定义每个元素的背景色,文字颜色,画什么文字上去

接口回调以后根据结果设置画笔绘制文字。

public CalendarCell defaultReturnCell(int year,int month,int day){
        if (isToday(year,month,day)){
            return new CalendarCell(Color.GRAY,Color.WHITE,"今");
        }else{
            return new CalendarCell(Color.WHITE,Color.BLACK,day+"");
        }
    }
这是一个默认的回调接口实现,所实现的功能就是上面截图所展示的。日期白底黑字,今天灰底白字。

正是这个接口的提供,所以我们的日历可以有很高的可扩展性。

ps:dayOfWeek表示星期几,0表示星期日,1表示星期一,2表示。。。。等等


接下来提供日期被点击以后的接口回调

@Override
    public boolean onTouchEvent(MotionEvent event) {
        int x= (int) event.getRawX();
        int y= (int) event.getRawY();
        //坐标转换
        int[] location = new  int[2] ;
        getLocationInWindow(location);
        x=x-location [0];
        y=y-location [1];
        // Log.e("绝对坐标",x+"--"+y);
        int which=whichPath(x,y);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //触屏事件回调
                which=which-start-6;
                if (which>0 && which<=maxDay){
//                    Log.i("click","->"+which);
                   if(mOnCellClickListener!=null){
                       mOnCellClickListener.onCellClick(which);
                   }
                }
                break;
        }

        return true;
    }
这里的坐标转换是因为触屏事件的坐标是以手机屏幕,除去最上面标题栏,就是你应用实际所占区域为起始点计算的。

而我们的控件可不是完全在界面左上的。需要得到坐标再转换为以最开始所画的大正方形为起始点。

通过坐标判断在哪行哪列,再用序号表示所在区域。

private int whichPath(int x, int y) {
        if(x>startX+width | y>startY+width |x<startX |y<startY){
            // Log.i("xy",x+"-"+y);
            return 0;
        }else {
            //以起始点开始计算坐标
            int nX = x - startX;
            int nY = y - startY;
            int hang = (int) nY / tWidth;//在哪行,从零开始
            int lie = (int) nX / tWidth;//在哪列,从零开始
            // Log.i("nXY",nX+"-"+nY);
            int reInt = hang * 7 + 1 + lie;
            return reInt;
        }
    }

至此,我们的日历显示和点击回调都完成了。然后再提供切换月份,年份,刷新,设置接口回调等等方法。


整个类的代码如下:

package com.toxicant.hua.mycalendarviewdemo;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import java.util.Calendar;

/**
 * Created by hua on 2016/2/4.
 */
public class MyCalendarView extends View {
    private int startX=0;
    private int startY=0;
    private int width=0;//大正方形的边长
    private int tWidth=0;//小正方形的边长
    private Paint mPaint=new Paint();//画笔
    private Calendar c=Calendar.getInstance();//用于记录当前年月
    Calendar today=Calendar.getInstance();//今天
    int start=0;//一号是星期几
    int maxDay=0;//最大日期
    private String[] weekString={"日","一","二","三","四","五","六"};//标示符
    //两个回调接口初始化
    private OnDrawCellListener mOnDrawCellListener2;
    private OnDrawCellListener mOnDrawCellListener=new OnDrawCellListener() {
        @Override
        public CalendarCell onDrawCell(int years, int month, int day, int dayOfWeek) {
           return defaultReturnCell(years,month,day);
        }
    };
    private OnCellClickListener mOnCellClickListener;


    //绘制接口
    public interface OnDrawCellListener{
        CalendarCell onDrawCell(int year,int month,int day,int dayOfWeek);
    }
    //触屏接口
    public interface OnCellClickListener{
        void onCellClick(int day);
    }

    public MyCalendarView(Context context) {
        super(context);
    }

    public MyCalendarView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyCalendarView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //日历相关参数初始化
        c.set(Calendar.DATE,1);
        start = c.get(Calendar.DAY_OF_WEEK);//获得1号是星期几
        maxDay = c.getActualMaximum(Calendar.DATE);//获得当前月的最大日期数

        width=Math.min(getMeasuredHeight(),getMeasuredWidth());//获取正方形区域边长
        //获取起始绘制点
        startY=(getMeasuredHeight()-width)/2;
        startX=(getMeasuredWidth()-width)/2;
        //初始化画笔
        mPaint.setColor(Color.WHITE);
        mPaint.setStrokeWidth(2);
        //绘制大正方形
        canvas.drawRect(startX, startY, startX + width, startY + width, mPaint);
        tWidth=width/7;
        int nDay=2-start-7;
        //画星期标识符的画笔设置
        mPaint.setColor(Color.BLACK);
        mPaint.setTextAlign(Paint.Align.CENTER);
        mPaint.setTextSize(tWidth / 2);//字体大小,单位px
        Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
        //开始绘制
        for(int j=0;j<7;j++){
            int tY=startY+j*tWidth+tWidth/2;
            for(int k=0;k<7;k++){
                int tX=startX+k*tWidth+tWidth/2;
                //tX,tY为每个cell的中心
                if(j==0){//画星期标示符
                    int baseline=tY+((fontMetrics.bottom - fontMetrics.top)/2-fontMetrics.bottom);
                    canvas.drawText(weekString[k],tX,baseline,mPaint);
                }

                if(nDay>0 && nDay<=maxDay){//绘画日期
                    //回调接口获得cell的元素
                    CalendarCell cell=mOnDrawCellListener.onDrawCell(c.get(Calendar.YEAR),c.get(Calendar.MONTH)+1,nDay,k);
                    mPaint.setColor(cell.backgroundColor);
                    canvas.drawCircle(tX, tY, tWidth / 3, mPaint);//画背景圆圈
                    mPaint.setColor(cell.textColor);
                    int baseline=tY+((fontMetrics.bottom - fontMetrics.top)/2-fontMetrics.bottom);
                    canvas.drawText(cell.text, tX, baseline, mPaint);
                }
                nDay++;


            }//for inside
        }//for outside
    }//onDraw

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x= (int) event.getRawX();
        int y= (int) event.getRawY();
        //坐标转换
        int[] location = new  int[2] ;
        getLocationInWindow(location);
        x=x-location [0];
        y=y-location [1];
        // Log.e("绝对坐标",x+"--"+y);
        int which=whichPath(x,y);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //触屏事件回调
                which=which-start-6;
                if (which>0 && which<=maxDay){
//                    Log.i("click","->"+which);
                   if(mOnCellClickListener!=null){
                       mOnCellClickListener.onCellClick(which);
                   }
                }
                break;
        }

        return true;
    }

    /**
     * 根据相对坐标计算所在区域
     * @param x 坐标x
     * @param y 坐标y
     * @return 返回区域代号
     */
    private int whichPath(int x, int y) {
        if(x>startX+width | y>startY+width |x<startX |y<startY){
            // Log.i("xy",x+"-"+y);
            return 0;
        }else {
            //以起始点开始计算坐标
            int nX = x - startX;
            int nY = y - startY;
            int hang = (int) nY / tWidth;//在哪行,从零开始
            int lie = (int) nX / tWidth;//在哪列,从零开始
            // Log.i("nXY",nX+"-"+nY);
            int reInt = hang * 7 + 1 + lie;
            return reInt;
        }
    }

    /**
     * 设置元素点击事件
     * @param l
     */
    public void setOnCellClickListener(OnCellClickListener l){
        mOnCellClickListener=l;
    }

    /**
     * 设置绘画每个元素的接口
     * @param l
     */
    public void setOnDrawCellListener(OnDrawCellListener l){
        mOnDrawCellListener=l;
    }

    /**
     * 判断是不是今天
     * @param y 年
     * @param m 月
     * @param d 日
     * @return 若是今天返回true
     */
    public boolean isToday(int y,int m,int d){
        return y==today.get(Calendar.YEAR) && m==today.get(Calendar.MONTH)+1 && d==today.get(Calendar.DATE);
    }

    /**
     * 切换下一月视图
     */
    public void nextMonth(){
        c.add(Calendar.MONTH,1);
        invalidate();
    }

    /**
     * 返回上一月视图
     */
    public void backMonth(){
        c.add(Calendar.MONTH,-1);
        invalidate();
    }

    /**
     * 获取日历的年份
     * @return 日历当前年份
     */
    public int getYear(){
        return c.get(Calendar.YEAR);
    }

    /**
     * 获取日历的月份
     * @return 日历当前月份
     */
    public int getMonth(){
        return c.get(Calendar.MONTH)+1;
    }

    /**
     * 设置日历年份,例如2016年对应2016
     * @param year 实际年份
     */
    public void setYear(int year){
        c.set(Calendar.YEAR,year);
        invalidate();
    }

    /**
     * 设置日历的月份,一月对应1,以此类推
     * @param month 实际月份
     */
    public void setMonth(int month){
        c.set(Calendar.MONTH,month-1);
        invalidate();
    }
    public void refresh(){
        invalidate();
    }
    public CalendarCell defaultReturnCell(int year,int month,int day){
        if (isToday(year,month,day)){
            return new CalendarCell(Color.GRAY,Color.WHITE,"今");
        }else{
            return new CalendarCell(Color.WHITE,Color.BLACK,day+"");
        }
    }

    /**
     * 获得标示时间的文本,例如2016年2月1日,返回20160201
     * @param year 年
     * @param month 月
     * @param day 日
     * @return 返回日期戳
     */
    public String getDateString(int year,int month,int day){
        return (year*10000+month*100+day)+"";
    }
}//class

class CalendarCell{
    int backgroundColor;//背景色,背景画圆圈
    int textColor;//文字颜色
    String text;//画什么字

    public CalendarCell(int backgroundColor, int textColor, String text) {
        this.backgroundColor = backgroundColor;
        this.textColor = textColor;
        this.text = text;
    }
}



复制粘贴就可以迅速使用到自己的项目中啦~

下面通过一个例子来讲下控件扩展成多选日期。

布局xml:日历,两个按钮

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.toxicant.hua.mycalendarviewdemo.MainActivity">

    <com.toxicant.hua.mycalendarviewdemo.MyCalendarView
        android:id="@+id/calendar"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Back"
        android:id="@+id/button"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="New Button"
        android:id="@+id/button2"
        android:layout_alignBottom="@+id/button"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true" />
</RelativeLayout>

activity代码:

package com.toxicant.hua.mycalendarviewdemo;

import android.graphics.Color;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    Button btnBack;
    Button btnNext;
    MyCalendarView calendarView;
    List<String> dateList=new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        calendarView= (MyCalendarView) findViewById(R.id.calendar);
        btnBack= (Button) findViewById(R.id.button);
        btnNext= (Button) findViewById(R.id.button2);
        calendarView.setOnCellClickListener(new MyCalendarView.OnCellClickListener() {
            @Override
            public void onCellClick(int day) {
                String md=calendarView.getDateString(calendarView.getYear(),calendarView.getMonth(),day);
                if (dateList.contains(md)){
                    dateList.remove(md);
                }else{
                    dateList.add(md);
                }
                calendarView.refresh();
                Toast.makeText(MainActivity.this,"click->"+day,Toast.LENGTH_SHORT).show();
            }
        });
        calendarView.setOnDrawCellListener(new MyCalendarView.OnDrawCellListener() {
            @Override
            public CalendarCell onDrawCell(int year, int month, int day, int dayOfWeek) {
                String md=calendarView.getDateString(year,month,day);
                if (dateList.contains(md)){
                    return new CalendarCell(Color.GREEN,Color.WHITE,"√");
                }
                return calendarView.defaultReturnCell(year,month,day);
            }
        });
        btnBack.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                calendarView.backMonth();
            }
        });
        btnNext.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                calendarView.nextMonth();
            }
        });
    }
}
点击日期后保存到list中,再次点击取消选择,从list中移除。然后刷新日历控件。

效果图如下:








博文到此结束,可以根据需求,自行扩展日历控件,单选多选,连续选择日期,签到啊,根据颜色显示每日活动频率都不在话下。

(如果你是学习自定义view的新人,可以根据思路自行实现周六和周日字体变灰的日历)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值