This is the 10th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021
Android custom calendar control
Let’s analyze the renderings and determine our requirements. (1) Draw a custom View of the week, used to identify the week of the date. (2) Draw a custom View of the date. (3), draw the transaction circle, from the renderings of our red circle to identify today’s transaction. (4) Draw the color of the selected date. (5) Handle click events on the selected date.
Through the analysis of the rendering, our needs, we are careful analysis result.the found is the text and draw the lines, so we just back to the Canvas of the two functions, the main difficulty is how to manage the location of the date arrangement, then we analysis how to implement a custom View them one by one.
Implement a custom View for Week
rendering
Analyzing the renderings, we need to draw the upper and lower lines, and then draw the description text (day, one, two, three, four, five, six). Here’s a look at our implementation. First look at part of the source code, and then in separate explanation.
Public class WeekDayView extends View {private int mTopLineColor = color.parsecolor ("#CCE4F2"); private int mTopLineColor = color.parsecolor ("#CCE4F2"); Private int mBottomLineColor = color.parsecolor ("#CCE4F2"); Private int mWeedayColor = color.parsecolor ("#1FC2F3"); private int mWeedayColor = color.parsecolor ("#1FC2F3"); Private int mWeekendColor = color.parsecolor ("#fa4451"); private int mWeekendColor = color.parsecolor ("#fa4451"); Private int mStrokeWidth = 4; private int mWeekSize = 14; private Paint paint; private DisplayMetrics mDisplayMetrics; Private String [] weekString = new String [] {" day ", "a", "2", "three", "four", "five", "six"}; public WeekDayView(Context context, AttributeSet attrs) { super(context, attrs); mDisplayMetrics = getResources().getDisplayMetrics(); paint = new Paint(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); if(heightMode == MeasureSpec.AT_MOST){ heightSize = mDisplayMetrics.densityDpi * 30; } if(widthMode == MeasureSpec.AT_MOST){ widthSize = mDisplayMetrics.densityDpi * 300; } setMeasuredDimension(widthSize, heightSize); } @Override protected void onDraw(Canvas canvas) { int width = getWidth(); int height = getHeight(); Paint. SetStyle (style.stroke); paint.setColor(mTopLineColor); paint.setStrokeWidth(mStrokeWidth); canvas.drawLine(0, 0, width, 0, paint); // Draw a line paint. SetColor (mBottomLineColor); canvas.drawLine(0, height, width, height, paint); paint.setStyle(Style.FILL); paint.setTextSize(mWeekSize * mDisplayMetrics.scaledDensity); int columnWidth = width / 7; for(int i=0; i < weekString.length; i++){ String text = weekString[i]; int fontWidth = (int) paint.measureText(text); int startX = columnWidth * i + (columnWidth - fontWidth)/2; int startY = (int) (height/2 - (paint.ascent() + paint.descent())/2); If (text. IndexOf (" day ") > 1 | | text. The indexOf (" six ") > 1) {paint. SetColor (mWeekendColor); }else{ paint.setColor(mWeedayColor); } canvas.drawText(text, startX, startY, paint); } @param mTopLineColor */ public void setmTopLineColor(int mTopLineColor) {this.mtoplinecolor = this.mtoplinecolor mTopLineColor; } @bottomlinecolor public void setmBottomLineColor(int bottomlinecolor) { this.mBottomLineColor = mBottomLineColor; Public void setmWeedayColor(int mWeedayColor) {this.mWeedayColor = mWeedayColor; } @param mWeekendColor */ public void setmWeekendColor(int mWeekendColor) {this.mweekendcolor = this.mweekendcolor mWeekendColor; } @param mStrokeWidth public void setmStrokeWidth(int mStrokeWidth) {this.mstrokewidth = @param mStrokeWidth public void setmStrokeWidth(int mStrokeWidth) {this.mstrokewidth = @param mStrokeWidth mStrokeWidth; } @param mWeekSize */ public void setmWeekSize(int mWeekSize) {this.mweeksize = mWeekSize; } public void setWeekString(String[] weekString) public void setWeekString(String[] weekString) { this.weekString = weekString; }}Copy the code
(1) First, we define the member variables we need, such as the color of the upper and lower lines, the width, the size of the font, and the expression of the cycle. These are needed for flexible customization. Easy to use. (2) Now look at the onMeasure method, we know that in the custom view, we have the wrap_content property, which is not the size we want, so in the onMeasure method, we specify the size of this condition, the default size is 300*30. (3), onDraw method, we need to draw content in onDraw method. Int columnWidth = width / 7; int columnWidth = width / 7; Compute the width of each column. Why compute the width? Because we want to put the seven words “day “,” one “,” two “,” three “,” four “,” five “,” six” in the center of the corresponding grid. To drawText with the drawText method, we need to specify the starting position for the text to be drawn, and to get to the middle position, we need to calculate.
int startX = columnWidth * i + (columnWidth - fontWidth)/2;
int startY = (int) (height/2 - (paint.ascent() + paint.descent())/2);
Copy the code
Here is not very understanding, can refer to the article of love elder brother. After that, there are some Settings properties, nothing to talk about.
At this point, it’s easy to implement a custom view for our week. Let’s look at the implementation of dates.
Implement Date Date custom View
public class MonthDateView extends View {
private static final int NUM_COLUMNS = 7;
private static final int NUM_ROWS = 6;
private Paint mPaint;
private int mDayColor = Color.parseColor("#000000");
private int mSelectDayColor = Color.parseColor("#ffffff");
private int mSelectBGColor = Color.parseColor("#1FC2F3");
private int mCurrentColor = Color.parseColor("#ff0000");
private int mCurrYear,mCurrMonth,mCurrDay;
private int mSelYear,mSelMonth,mSelDay;
private int mColumnSize,mRowSize;
private DisplayMetrics mDisplayMetrics;
private int mDaySize = 18;
private TextView tv_date,tv_week;
private int weekRow;
private int [][] daysString;
private int mCircleRadius = 6;
private DateClick dateClick;
private int mCircleColor = Color.parseColor("#ff0000");
private List<Integer> daysHasThingList;
public MonthDateView(Context context, AttributeSet attrs) {
super(context, attrs);
mDisplayMetrics = getResources().getDisplayMetrics();
Calendar calendar = Calendar.getInstance();
mPaint = new Paint();
mCurrYear = calendar.get(Calendar.YEAR);
mCurrMonth = calendar.get(Calendar.MONTH);
mCurrDay = calendar.get(Calendar.DATE);
setSelectYearMonth(mCurrYear,mCurrMonth,mCurrDay);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if(heightMode == MeasureSpec.AT_MOST){
heightSize = mDisplayMetrics.densityDpi * 200;
}
if(widthMode == MeasureSpec.AT_MOST){
widthSize = mDisplayMetrics.densityDpi * 300;
}
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onDraw(Canvas canvas) {
initSize();
daysString = new int[6][7];
mPaint.setTextSize(mDaySize*mDisplayMetrics.scaledDensity);
String dayString;
int mMonthDays = DateUtils.getMonthDays(mSelYear, mSelMonth);
int weekNumber = DateUtils.getFirstDayWeek(mSelYear, mSelMonth);
Log.d("DateView", "DateView:" + mSelMonth+"月1号周" + weekNumber);
for(int day = 0;day < mMonthDays;day++){
dayString = (day + 1) + "";
int column = (day+weekNumber - 1) % 7;
int row = (day+weekNumber - 1) / 7;
daysString[row][column]=day + 1;
int startX = (int) (mColumnSize * column + (mColumnSize - mPaint.measureText(dayString))/2);
int startY = (int) (mRowSize * row + mRowSize/2 - (mPaint.ascent() + mPaint.descent())/2);
if(dayString.equals(mSelDay+"")){
//绘制背景色矩形
int startRecX = mColumnSize * column;
int startRecY = mRowSize * row;
int endRecX = startRecX + mColumnSize;
int endRecY = startRecY + mRowSize;
mPaint.setColor(mSelectBGColor);
canvas.drawRect(startRecX, startRecY, endRecX, endRecY, mPaint);
//记录第几行,即第几周
weekRow = row + 1;
}
//绘制事务圆形标志
drawCircle(row,column,day + 1,canvas);
if(dayString.equals(mSelDay+"")){
mPaint.setColor(mSelectDayColor);
}else if(dayString.equals(mCurrDay+"") && mCurrDay != mSelDay && mCurrMonth == mSelMonth){
//正常月,选中其他日期,则今日为红色
mPaint.setColor(mCurrentColor);
}else{
mPaint.setColor(mDayColor);
}
canvas.drawText(dayString, startX, startY, mPaint);
if(tv_date != null){
tv_date.setText(mSelYear + "年" + (mSelMonth + 1) + "月");
}
if(tv_week != null){
tv_week.setText("第" + weekRow +"周");
}
}
}
private void drawCircle(int row,int column,int day,Canvas canvas){
if(daysHasThingList != null && daysHasThingList.size() >0){
if(!daysHasThingList.contains(day))return;
mPaint.setColor(mCircleColor);
float circleX = (float) (mColumnSize * column + mColumnSize*0.8);
float circley = (float) (mRowSize * row + mRowSize*0.2);
canvas.drawCircle(circleX, circley, mCircleRadius, mPaint);
}
}
@Override
public boolean performClick() {
return super.performClick();
}
private int downX = 0,downY = 0;
@Override
public boolean onTouchEvent(MotionEvent event) {
int eventCode= event.getAction();
switch(eventCode){
case MotionEvent.ACTION_DOWN:
downX = (int) event.getX();
downY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
int upX = (int) event.getX();
int upY = (int) event.getY();
if(Math.abs(upX-downX) < 10 && Math.abs(upY - downY) < 10){//点击事件
performClick();
doClickAction((upX + downX)/2,(upY + downY)/2);
}
break;
}
return true;
}
/**
* 初始化列宽行高
*/
private void initSize(){
mColumnSize = getWidth() / NUM_COLUMNS;
mRowSize = getHeight() / NUM_ROWS;
}
/**
* 设置年月
* @param year
* @param month
*/
private void setSelectYearMonth(int year,int month,int day){
mSelYear = year;
mSelMonth = month;
mSelDay = day;
}
/**
* 执行点击事件
* @param x
* @param y
*/
private void doClickAction(int x,int y){
int row = y / mRowSize;
int column = x / mColumnSize;
setSelectYearMonth(mSelYear,mSelMonth,daysString[row][column]);
invalidate();
//执行activity发送过来的点击处理事件
if(dateClick != null){
dateClick.onClickOnDate();
}
}
/**
* 左点击,日历向后翻页
*/
public void onLeftClick(){
int year = mSelYear;
int month = mSelMonth;
int day = mSelDay;
if(month == 0){//若果是1月份,则变成12月份
year = mSelYear-1;
month = 11;
}else if(DateUtils.getMonthDays(year, month) == day){
//如果当前日期为该月最后一点,当向前推的时候,就需要改变选中的日期
month = month-1;
day = DateUtils.getMonthDays(year, month);
}else{
month = month-1;
}
setSelectYearMonth(year,month,day);
invalidate();
}
/**
* 右点击,日历向前翻页
*/
public void onRightClick(){
int year = mSelYear;
int month = mSelMonth;
int day = mSelDay;
if(month == 11){//若果是12月份,则变成1月份
year = mSelYear+1;
month = 0;
}else if(DateUtils.getMonthDays(year, month) == day){
//如果当前日期为该月最后一点,当向前推的时候,就需要改变选中的日期
month = month + 1;
day = DateUtils.getMonthDays(year, month);
}else{
month = month + 1;
}
setSelectYearMonth(year,month,day);
invalidate();
}
/**
* 获取选择的年份
* @return
*/
public int getmSelYear() {
return mSelYear;
}
/**
* 获取选择的月份
* @return
*/
public int getmSelMonth() {
return mSelMonth;
}
/**
* 获取选择的日期
* @param mSelDay
*/
public int getmSelDay() {
return this.mSelDay;
}
/**
* 普通日期的字体颜色,默认黑色
* @param mDayColor
*/
public void setmDayColor(int mDayColor) {
this.mDayColor = mDayColor;
}
/**
* 选择日期的颜色,默认为白色
* @param mSelectDayColor
*/
public void setmSelectDayColor(int mSelectDayColor) {
this.mSelectDayColor = mSelectDayColor;
}
/**
* 选中日期的背景颜色,默认蓝色
* @param mSelectBGColor
*/
public void setmSelectBGColor(int mSelectBGColor) {
this.mSelectBGColor = mSelectBGColor;
}
/**
* 当前日期不是选中的颜色,默认红色
* @param mCurrentColor
*/
public void setmCurrentColor(int mCurrentColor) {
this.mCurrentColor = mCurrentColor;
}
/**
* 日期的大小,默认18sp
* @param mDaySize
*/
public void setmDaySize(int mDaySize) {
this.mDaySize = mDaySize;
}
/**
* 设置显示当前日期的控件
* @param tv_date
* 显示日期
* @param tv_week
* 显示周
*/
public void setTextView(TextView tv_date,TextView tv_week){
this.tv_date = tv_date;
this.tv_week = tv_week;
invalidate();
}
/**
* 设置事务天数
* @param daysHasThingList
*/
public void setDaysHasThingList(List<Integer> daysHasThingList) {
this.daysHasThingList = daysHasThingList;
}
/***
* 设置圆圈的半径,默认为6
* @param mCircleRadius
*/
public void setmCircleRadius(int mCircleRadius) {
this.mCircleRadius = mCircleRadius;
}
/**
* 设置圆圈的半径
* @param mCircleColor
*/
public void setmCircleColor(int mCircleColor) {
this.mCircleColor = mCircleColor;
}
/**
* 设置日期的点击回调事件
* @author shiwei.deng
*
*/
public interface DateClick{
public void onClickOnDate();
}
/**
* 设置日期点击事件
* @param dateClick
*/
public void setDateClick(DateClick dateClick) {
this.dateClick = dateClick;
}
/**
* 跳转至今天
*/
public void setTodayToView(){
setSelectYearMonth(mCurrYear,mCurrMonth,mCurrDay);
invalidate();
}
}
Copy the code
(1) First of all, we still define some member variables we need, such as the color of the font, the color of the circle, the selected background color, and also we need to record our correct date of year, month and day, and the selected date of year, month and day to distinguish, mainly so much. (2) then rewrite the onMeasure method, similar to WeekView, without too much explanation, almost. (3), in the onDraw method for drawing, the principle of drawing, we according to the Calendar to obtain the days of the current month, and the first day is the week, only calculate the week, we know our Calendar from which column to start, so we can calculate the location of each drawing date:
int column = (day+weekNumber - 1) % 7;
int row = (day+weekNumber - 1) / 7;
daysString[row][column]=day + 1;
int startX = (int) (mColumnSize * column + (mColumnSize - mPaint.measureText(dayString))/2);
int startY = (int) (mRowSize * row + mRowSize/2 - (mPaint.ascent() + mPaint.descent())/2);
Copy the code
There are seven days in a week, and we calculate the corresponding row of the date based on the date number and the start, and then multiply the row width to calculate the actual position of each date number. So we can draw the date with drawText. We have a member variable that records the selected date number and then draws the selected background color as follows:
If (dayString.equals(mSelDay+"")){// Draw a background color rectangle int startRecX = mColumnSize * column; int startRecY = mRowSize * row; int endRecX = startRecX + mColumnSize; int endRecY = startRecY + mRowSize; mPaint.setColor(mSelectBGColor); canvas.drawRect(startRecX, startRecY, endRecX, endRecY, mPaint); WeekRow = row + 1; }Copy the code
(4) We also need to draw transaction flags. We define the List object of List daysHasThingList, which we use to ‘hold’ the date number of the transaction. We then determine if the date is included in the list in the onDraw method and draw the corresponding circle.
private void drawCircle(int row,int column,int day,Canvas canvas){ if(daysHasThingList ! = null && daysHasThingList.size() >0){ if(! daysHasThingList.contains(day))return; mPaint.setColor(mCircleColor); Float circleX = (float) (mColumnSize * column + mColumnSize*0.8); Float circley = (float) (mRowSize * row + mRowSize*0.2); canvas.drawCircle(circleX, circley, mCircleRadius, mPaint); }}Copy the code
(5) At this point, the date drawing and transaction are complete, but there is no click on the event to switch the date selection, how to do this? So we need to override the onTouchEvent method of the View, and then judge the click event, and then calculate the row and row based on the X and Y values that we get, and then we get the day that we selected in the daysString based on the row and row, set the selected date, and then refresh the View.
public boolean onTouchEvent(MotionEvent event) { int eventCode= event.getAction(); switch(eventCode){ case MotionEvent.ACTION_DOWN: downX = (int) event.getX(); downY = (int) event.getY(); break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: int upX = (int) event.getX(); int upY = (int) event.getY(); If (math.abs (upx-downx) < 10 && math.abs (upy-downy) < 10){// Click the event performClick(); doClickAction((upX + downX)/2,(upY + downY)/2); } break; } return true; }Copy the code
(5) If you need to click on something, you just need to write a simple callback and handle it in the activity.
private void doClickAction(int x,int y){ int row = y / mRowSize; int column = x / mColumnSize; setSelectYearMonth(mSelYear,mSelMonth,daysString[row][column]); invalidate(); // Execute the click processing event sent by the activity if(dateClick! = null){ dateClick.onClickOnDate(); }} @author shiwei. Deng ** / public interface DateClick{public void onClickOnDate(); } @param dateClick public void setDateClick(dateClick) {this.dateclick = dateClick; }Copy the code
(6) The main processing has been completed, the remaining need for us to obtain the date display and display the week, click “today” to return to today, the logic of these processing is to set the selected date, and then refresh the view. The code will not stick, the above source code annotations are quite detailed.
Finally, we use a custom View for display. Such as:
<? The XML version = "1.0" encoding = "utf-8"? > <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_centerInParent="true" android:orientation="vertical" > <! <RelativeLayout Android :layout_width="fill_parent" Android :layout_height="wrap_content" android:gravity="center_vertical" android:background="#ffffff" android:layout_marginLeft="10dp" android:layout_marginRight="10dp" android:paddingTop="3dp"> <ImageView android:id="@+id/iv_left" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:contentDescription="@null" android:background="@drawable/left_arrow" /> <ImageView android:id="@+id/iv_right" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:contentDescription="@null" android:background="@drawable/right_arrow" /> <LinearLayout android:id="@+id/date_operator_ll" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:gravity="center" android:layout_centerInParent="true" android:orientation="horizontal" > <TextView android:id="@+id/tv_today" android:layout_width="25dp" Android :layout_height="25dp" Android :layout_marginRight="5dp" Android :text=" now "Android :gravity="center" android:background="#FFD700" android:textColor="#ffffff" android:textSize="17sp" /> <TextView android:id="@+id/date_text" style="@style/myschedule_current_month_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_horizontal" android:textColor="#93C73C" android:textSize="20sp" android:text="" /> <TextView android:id="@+id/week_text" style="@style/myschedule_current_month_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_horizontal" android:layout_marginLeft="10dp" android:textColor="#93C73C" android:textSize="20sp" android:text="" /> </LinearLayout> </RelativeLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:layout_marginRight="10dp" android:background="#ffffff" android:orientation="vertical" > <com.dsw.datepicker.WeekDayView android:layout_width="match_parent" android:layout_height="30dp" /> <com.dsw.datepicker.MonthDateView android:id="@+id/monthDateView" android:layout_width="fill_parent" android:layout_height="200dp" /> </LinearLayout> </LinearLayout>Copy the code
So we can use it in our activity:
public class MainActivity extends FragmentActivity { private ImageView iv_left; private ImageView iv_right; private TextView tv_date; private TextView tv_week; private TextView tv_today; private MonthDateView monthDateView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); List<Integer> list = new ArrayList<Integer>(); list.add(10); list.add(12); list.add(15); list.add(16); setContentView(R.layout.activity_date); iv_left = (ImageView) findViewById(R.id.iv_left); iv_right = (ImageView) findViewById(R.id.iv_right); monthDateView = (MonthDateView) findViewById(R.id.monthDateView); tv_date = (TextView) findViewById(R.id.date_text); tv_week =(TextView) findViewById(R.id.week_text); tv_today = (TextView) findViewById(R.id.tv_today); monthDateView.setTextView(tv_date,tv_week); monthDateView.setDaysHasThingList(list); monthDateView.setDateClick(new DateClick() { @Override public void onClickOnDate() { Toast.makeText(getApplication(), + monthdateView.getmselday (), toast.length_short).show(); }}); setOnlistener(); } private void setOnlistener(){ iv_left.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { monthDateView.onLeftClick(); }}); iv_right.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { monthDateView.onRightClick(); }}); tv_today.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { monthDateView.setTodayToView(); }}); }}Copy the code
At this point, all the content has been completed, the use of a simple custom view, in the actual project used quite a lot, of course, this example has a lot of perfect places, such as in the onTouchEvent for sliding monitoring, by sliding to modify the date, these interested students can try.
Welcome to leave a message.
Modified version of the improved version of the renderings:
[ImG-RoaastyH-1635672088027]
Github download address
Download the source code
I will complete a new calendar control by the end of August. The new github address is github.com/dengshiwei/… Comments are welcome.