android仿小米日历,实现一个仿小米日历控件

先看效果图:

f9bf72938d64

效果图

根据效果,我们可以看到,要实现该控件,需要具备:

容器以及触摸事件处理

周日历布局以及选择,切换上下周处理

月日历布局以及选择,切换上下月处理

首先说说容器

对于其他使用者来说,整个日历都应该是一个类似于RelativeLayout之类的容器,然后里面包含有我们需要的日历控件,而且因为在滑动日历的时候也会移动下面的listview或者scrollView部分,所以其实这是一个嵌套滑动控件,它必须能合理的处理不同的手势场景(比如在listView的内容滑动到顶部时再下滑会滑出日历,以及上滑收起日历等),所以它在设计上应该类似于drawerLayout等手势类容器,并且需要在滑动时通过滑动进度来动态设置动画。

因此,容器控件需要做到:

触摸事件的拦截与处理

周日历和月日历的收起和展开动画

凡是触摸事件处理与拦截都是通过复写onInterceptTouchEvent和onTouchEvent来实现,一般有如下方式:

逻辑全部写在这两个方法里,需要开发者自己去记录位置坐标,计算方向等等,并且需要记录若干状态

用安卓提供的GestureDetector类简化某些常用手势场景,当然依然需要复写onInterceptTouchEvent和onTouchEvent,相对来说代码简化一些

用安卓提供的ViewDragHelper类(以下简称VDH)来处理,该类更简化,相对GestureDetector来说其增加了容器内孩子视图的判断,正是因为这点,可以让开发者方便的得知触摸点由哪个孩子视图处理,很大程度上避免处理坐标系信息

接下来是代码的架构方面

更深入的分析需求,我们会发现,容器类其实应该负责处理内容区与周日历/月日历的联动,而至于日历内部渲染,逻辑处理应该交由其他类来处理,这里我们再次细分:

逻辑部分(Presenter层):向日历视图发出刷新,定位命令,给调用者暴露关闭,返回今天方法,并且给调用者提供日期选中,滚动到某月/某周的消息回调

视图部分(View层):从效果图来看,周日历/月日历采用viewPager来实现即可,其重点负责日历的渲染功能,同时当Presenter层被调用返回今天以及更新了数据源需要刷新时,最终需要视图层来刷新页面以及滑动到正确的周/月

逻辑部分Presenter

根据使用对象和场景,Presenter提供的能力分为三类:

供控件使用者调用的,属于开放API部分

供视图层调用,一般是用户对视图操作了之后由视图通知Presenter做某事

消息回传,视图通知了Presenter之后,由Presenter来通知使用者来更新UI或者做其他事务

开放API

返回今天(供控件使用者调用)

关闭月日历(供控件使用者调用)

设置数据源

f9bf72938d64

示例图

开放API由使用者调用,因为根据效果图来看,日历的标题栏实为固定在界面顶部,正常情况下被toolBar所遮挡,滑动时逐步显现,因此将标题栏单独实现为一个控件,所以需要日历控件和标题栏控件互动:

标题栏上有两个按钮,返回今天和收起,点击后应该通知日历控件做相应操作

日历控件滑动或者选择后,可能会导致标题栏上文字显示改变,因此需要日历控件提供回调

从代码实现角度,由于这些操作实际为逻辑控制部分,因此应该交由presenter来实现,调用者应该通过presenter作为桥梁来操作控件视图,且控件视图回调的消息通过presenter回传给调用者

根据需求,在月日历下,会根据某接口返回的参数来标识当天是否有数据

f9bf72938d64

month_card.png

此数据来源于网络请求,是异步操作,因此只能有调用者在网络请求返回之后将数据传入控件且刷新,与

返回今天和关闭相同,调用者最好不要直接操作控件视图,而通过presenter作为桥接,间接通知视图刷新页面,

使得调用者与视图解耦,将日历视图具体实现逻辑隐蔽起来。几个主要代码实现如下:

// 设置数据源

public void parseData(List sources) {

if (calendarDotVO == null) {

throw new IllegalArgumentException("Dot Data must not be null");

}

calendarDotVO.parseData(sources);

viewBuilder().dragCalendarLayout.reDraw();

}

// 返回今天

public void backToday() {

setSelectTime(todayTime);

viewBuilder().dragCalendarLayout.backToday();

}

// 关闭月日历

public void close() {

viewBuilder().dragCalendarLayout.setExpand(false);

}

当点击返回今天时,需要做到:

回滚周/月视图至今天所在的周/月,后文就讨论具体实现

通知调用者重新选中今天

供日历视图调用的presenter接口,一般为通知调用者进行业务处理

根据设计,日历视图有如下几个会引发调用者业务处理的操作:

周日历下,左右滑动切换会导致日期的自动切换,比如选中日期为周二且滑至上一周时,同时日期切换至该周周二

月日历下,左右滑动切换日历标题栏上展示日期

月日历下标题栏展示日期或者选择日期非今日,展示返回今日按钮

之前说过,周日历/月日历实际为viewPager实现,因此要实现滑动切换逻辑只需监听ViewPager.OnPageChangeListener,因月日历和周日历的实际实现不同,这里用枚举CalendarPagerChangeEnum来区分:

MONTH{

@Override

public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

}

@Override

public void onPageSelected(int position) {

CalendarPresenter.instance()

.setCurrentScrollDate(DateUtils.getTagTimeStr(

CalendarType.MONTH.calculateByOffset(position)));

}

@Override

public void onPageScrollStateChanged(int state) {

if (stateChangeListener != null) {

stateChangeListener.onStateChange(state);

}

if (state == ViewPager.SCROLL_STATE_DRAGGING) {

((MonthCalendarAdapter)adapter).showDivider(true);

} else if (state == ViewPager.SCROLL_STATE_IDLE) {

((MonthCalendarAdapter)adapter).showDivider(false);

}

}

},

WEEK{

@Override

public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

}

@Override

public void onPageSelected(int position) {

Calendar calendar = CalendarPresenter.instance().selectCalendar();

int week = CalendarType.WEEK.defPosition() - position;

if (week != CalendarPresenter.instance().getWeekDiff()) {

calendar.add(Calendar.DATE, -(week - CalendarPresenter.instance().getWeekDiff()) * 7);

CalendarPresenter.instance().setSelectTime(DateUtils.getTagTimeStr(calendar),true);

}

}

@Override

public void onPageScrollStateChanged(int state) {

}

};

这里有一个小tips,为了提升用户体验,月日历滑动时需展示边界线,因此才有

@Override

public void onPageScrollStateChanged(int state) {

if (state == ViewPager.SCROLL_STATE_DRAGGING) {

((MonthCalendarAdapter)adapter).showDivider(true);

} else if (state == ViewPager.SCROLL_STATE_IDLE) {

((MonthCalendarAdapter)adapter).showDivider(false);

}

}

同时Presenter提供选择日期和设置月日历滚动日期接口:

// 设置选中日期并触发消息回传,通知调用者进行业务处理

public void setSelectTime(String selectTime, boolean autoReset) {

if (TextUtils.isEmpty(selectTime)) {

throw new IllegalArgumentException("selectTime can not be empty");

}

if (DateUtils.diff(todayTime, selectTime) < 0) {

if (autoReset) {

selectTime = todayTime;

} else {

return;

}

}

boolean close = false;

this.selectTime = selectTime;

if (callbk != null) {

close = callbk.onSelect(selectTime);

}

notifyCalendarBar(selectTime);

viewBuilder().dragCalendarLayout.focusCalendar();

if (close) {

close();

}

}

// 月日历下当前滚动到某月时的日期设置

public void setCurrentScrollDate(String currentScrollDate) {

if (TextUtils.isEmpty(currentScrollDate)) {

throw new IllegalArgumentException("currentScrollDate can not be empty");

}

if (!currentDate.equals(currentScrollDate)) {

currentDate = currentScrollDate;

currentDateCallbk();

notifyCalendarBar(currentScrollDate);

}

}

// 当前滚动日期的消息回传

private void currentDateCallbk() {

if (callbk != null) {

callbk.onScroll(currentDate);

}

}

// 日历标题栏的消息回传

private void notifyCalendarBar(String barDate) {

if (callbk != null) {

boolean isToday;

if (DateUtils.diffMonth(todayTime, barDate) == 0) {

isToday = TextUtils.equals(todayTime, selectTime);

} else {

isToday = false;

}

callbk.onCalendarBarChange(barDate,isToday);

}

}

消息回传通知

根据之前的约定,调用者只与presenter交互,同样的,presenter接受到日历视图的操作后,由presenter通知调用者进行业务处理

// presenter提供的消息通知接口

public interface ICallbk {

void onCalendarBarChange(String currentTime, boolean isToday);

void onScroll(String currentTime);

boolean onSelect(String selectTime);

}

ICallbk callbk = null;

public void setCallbk(ICallbk callbk) {

this.callbk = callbk;

currentDateCallbk();

notifyCalendarBar(currentDate);

}

此处在设置消息通知接口时需强制触发消息一次,目的是为了在初始阶段刷新日历标题栏

视图部分(VIEW)

视图层主要负责:

周视图渲染以及用户操作后对presenter发起消息通知

月视图渲染以及用户操作后对presenter发起消息通知

从结构上来说,两者都是采用viewPager实现,不同点即其渲染方式不同,因此这里也可采用枚举CalendarType加以区分:

public enum CalendarType implements IAdapterRefresh,IAdapterConstant {

MONTH {

@Override

public void refresh(ViewGroup view, int position) {

//给view 填充内容

//设置开始时间为本周日

Calendar day = calculateByOffset(position);

view.setTag(day.get(Calendar.MONTH) + "");

//找到这个月的第一天所在星期的周日

day.add(Calendar.DAY_OF_MONTH, -(day.get(Calendar.DAY_OF_MONTH) - 1));

int day_of_week = day.get(Calendar.DAY_OF_WEEK) - 1;

day.add(Calendar.DATE, -day_of_week);

((ICalendarCard)view).render(day);

}

@Override

public int getCount() {

return 1200;

}

@Override

public int defPosition() {

return getCount() - 1;

}

},

WEEK {

@Override

public void refresh(ViewGroup view, int position) {

//给view 填充内容

//设置开始时间为本周日

Calendar day = calculateByOffset(position);

int day_of_week = day.get(Calendar.DAY_OF_WEEK) - 1;

day.add(Calendar.DATE, -day_of_week);

((ICalendarCard)view).render(day);

}

@Override

public int getCount() {

return 4800;

}

@Override

public int defPosition() {

return getCount() - 1;

}

}

}

public interface IAdapterRefresh {

void refresh(ViewGroup view, int position);

}

public interface IAdapterConstant {

int getCount();

int defPosition();

}

枚举CalendarType中只需处理逻辑部分,这里为计算出每周/月上起始时间(这里的起始时间并非每一周/月的第一天,而应该是每一张周卡片/月卡片第一行第一列开始的那个日期,因日历横向是从周日开始,所以只需算出第一行的周日即可),并调用相应的周/月视图进行渲染。而周/月视图来源于不同的PagerAdapter(因为周/月为两个不想干的viewpager),以下以周日历适配器为例:

public class WeekCalendarAdapter extends CalendarBaseAdapter {

private List views = new ArrayList<>();

WeekCard currentCard;

public WeekCalendarAdapter(Context context) {

views.clear();

for (int i = 0; i < 4; i++) {

views.add(new WeekCard(context));

}

}

@Override

public int getCount() {

return CalendarType.WEEK.getCount();

}

@Override

public boolean isViewFromObject(View view, Object object) {

return view == object;

}

@Override

public void destroyItem(ViewGroup container, int position, Object object) {

}

public WeekCard currentCard() {

return currentCard;

}

@Override

public void setPrimaryItem(ViewGroup container, int position, Object object) {

currentCard = (WeekCard) object;

super.setPrimaryItem(container, position, object);

}

@Override

public Object instantiateItem(ViewGroup container, final int position) {

ViewGroup view = (ViewGroup) views.get(position % views.size());

int index = container.indexOfChild(view);

if (index != -1) {

container.removeView(view);

}

try {

container.addView(view);

} catch (Exception e) {

}

CalendarType.WEEK.refresh(view, position);

return view;

}

}

其中,适配器用4个视图循环使用达到节省资源的目的,WeekCard实现了ICalendarCard接口:

public interface ICalendarCard {

void render(final Calendar today);

}

然后是周日历viewPager:

public class WeekView extends LinearLayout implements ICalendarView {

ViewPager weekPager;

WeekCalendarAdapter adapter;

public WeekView(Context context) {

super(context);

setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));

setOrientation(VERTICAL);

View.inflate(getContext(), R.layout.calendar_pager, this);

weekPager = (ViewPager) findViewById(R.id.cal_pager);

ViewGroup.LayoutParams layoutParams = weekPager.getLayoutParams();

layoutParams.height = dp2px(getContext(), WEEK_HEIGHT);

weekPager.setLayoutParams(layoutParams);

adapter = new WeekCalendarAdapter(context);

weekPager.setAdapter(adapter);

weekPager.setCurrentItem(CalendarType.WEEK.defPosition());

weekPager.setOnPageChangeListener(CalendarPagerChangeEnum.WEEK.setAdapter(adapter));

}

@Override

public void backToday() {

weekPager.setCurrentItem(CalendarType.WEEK.defPosition(), true);

}

@Override

public int currentIdx() {

return weekPager.getCurrentItem();

}

@Override

public void focusCalendar() {

weekPager.setCurrentItem(CalendarType.WEEK.defPosition() - CalendarPresenter.instance().getWeekDiff(), true);

reDraw();

}

@Override

public void reDraw() {

adapter.notifyDataSetChanged();

}

}

我们会发现,因为ViewPager以及包含它的容器为动态实例化,因此需要手动的设置高度而无法用系统的wrap_content属性,因此这里需要开发者自我计算一个合理的高度WEEK_HEIGHT,此处作者设置每周高45dp,一个月最高为305dp(6行的周加上上下边距总共305dp,详细设置见Range类)

public class Range {

public static final int MONTH_HEIGHT = 305;

public static final int WEEK_HEIGHT = 45;

public static final int DAY_HEIGHT = 45;

public static final int MONTH_PADDING_TOP = 25;

public static final int MONTH_PADDING_BOTTOM = 10;

}

月日历实现与之类似,就不赘述。

另外,在前述的presenter实现中,提到返回今日时需要通时回滚周/月日历视图到当前周/月,其实际为相应的ViewPager重设当前页,因此在前述的presenter的backToday实现中调用的viewBuilder().dragCalendarLayout.backToday();实际上是调用周视图WeekView的weekPager.setCurrentItem(CalendarType.WEEK.defPosition(), true);以及月视图MonthView的monthPager.setCurrentItem(CalendarType.MONTH.defPosition(), true);

周,月视图渲染实现

周卡片的渲染,实际上只需要7个横向排列的日期,而月卡片实际上是纵向排6个周卡片,这里给出主要的渲染代码:

// 周卡片

@Override

public void render(Calendar today) {

for (int a = 0; a < 7; a++) {

final int dayOfMonth = today.get(Calendar.DAY_OF_MONTH);

final ViewGroup dayOfWeek = (ViewGroup) getChildAt(a);

dayOfWeek.setTag(DateUtils.getTagTimeStr(today));

dayOfWeek.setOnClickListener(v -> CalendarPresenter.instance().setSelectTime(dayOfWeek.getTag().toString()));

//如果是选中天的话显示为蓝色

if (CalendarPresenter.instance().getSelectTime().equals(DateUtils.getTagTimeStr(today))) {

((TextView) dayOfWeek.findViewById(R.id.gongli)).setText(DateUtils.getTagTimeStrByMouthandDay(today));

renderSelect(dayOfWeek, DateUtils.getTagTimeStr(today));

} else {

((TextView) dayOfWeek.findViewById(R.id.gongli)).setText(dayOfMonth + "");

if (DateUtils.diff(CalendarPresenter.instance().today(), DateUtils.getTagTimeStr(today)) >= 0) {

renderNormal(dayOfWeek, DateUtils.getTagTimeStr(today));

} else {

renderGray(dayOfWeek, DateUtils.getTagTimeStr(today));

}

}

today.add(Calendar.DATE, 1);

}

}

// 月卡片

@Override

public void render(Calendar today) {

int pageMonth = (Integer.parseInt((String) getTag()));

//一页显示一个月+7天,为42;

for (int b = 0; b < 6; b++) {

final ViewGroup view = (ViewGroup) monthContent.getChildAt(b);

int currentMonth = today.get(Calendar.MONTH);

if (pageMonth != currentMonth && b != 0) {

view.setVisibility(INVISIBLE);

today.add(Calendar.DATE, 7);

} else {

view.setVisibility(VISIBLE);

for (int a = 0; a < 7; a++) {

final int dayOfMonth = today.get(Calendar.DAY_OF_MONTH);

final ViewGroup dayOfWeek = (ViewGroup) view.getChildAt(a);

((TextView) dayOfWeek.findViewById(R.id.gongli)).setText(dayOfMonth + "");

dayOfWeek.setTag(DateUtils.getTagTimeStr(today));

dayOfWeek.setOnClickListener(v -> CalendarPresenter.instance().setSelectTime(dayOfWeek.getTag().toString()));

//不是当前月浅色显示

currentMonth = today.get(Calendar.MONTH);

if (pageMonth != currentMonth) {

renderInvisible(dayOfWeek);

// renderGray(dayOfWeek,DateUtils.getTagTimeStr(today));

today.add(Calendar.DATE, 1);

} else {

//如果是选中天的话显示为蓝色

if (CalendarPresenter.instance().getSelectTime().equals(DateUtils.getTagTimeStr(today))) {

selectPos = calculatePos(b);

renderSelect(dayOfWeek, DateUtils.getTagTimeStr(today));

} else {

if (DateUtils.diff(CalendarPresenter.instance().today(), DateUtils.getTagTimeStr(today)) >= 0) {

renderNormal(dayOfWeek, DateUtils.getTagTimeStr(today));

} else {

renderGray(dayOfWeek, DateUtils.getTagTimeStr(today));

}

}

today.add(Calendar.DATE, 1);

}

}

}

}

}

关于仿小米日历的实现到此结束,祝各位天天开心,生活愉快!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值