先看效果图:

效果图

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

容器以及触摸事件处理

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

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

首先说说容器

对于其他使用者来说,整个日历都应该是一个类似于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

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

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

设置数据源

示例图

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

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

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

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

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

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);

}

}

}

}

}

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

android仿小米日历,实现一个仿小米日历控件相关推荐

  1. android学习笔记---50_样式与主题,给控件使用样式,给应用使用主题

    50_样式与主题 android学习笔记---50_样式与主题,给控件使用样式,给应用使用主题 2013/5/12 50_样式与主题 ----------------- android样式和主题(st ...

  2. Android之RemoteViews篇上————通知栏和桌面小控件

    Android之RemoteViews篇上----通知栏和桌面小控件 一.目录 文章目录 Android之RemoteViews篇上----通知栏和桌面小控件 一.目录 二.RemoteViews的概 ...

  3. 《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统

    <深入理解Android 卷III>即将发布,作者是张大伟.此书填补了深入理解Android Framework卷中的一个主要空白,即Android Framework中和UI相关的部分. ...

  4. 一起实现一个健壮的课程表控件

    前言 一年前我在业余的时间做了一个课程表的界面,过程中基本上也很顺利的,近期由于一个校园项目的需要,所以就对其简单封装成了控件用在了项目中,但是在真正的项目中发现了很多当初没有考虑到的问题,所以在此将 ...

  5. 【Android自定义View实战】之自定义评价打分控件RatingBar,可以自定义星星大小和间距...

    [Android自定义View实战]之自定义评价打分控件RatingBar,可以自定义星星大小和间距

  6. SAP UI5 应用开发教程之三十二 - 如何创建一个自定义 SAP UI5 控件试读版

    一套适合 SAP UI5 初学者循序渐进的学习教程 教程目录 SAP UI5 本地开发环境的搭建 SAP UI5 应用开发教程之一:Hello World SAP UI5 应用开发教程之二:SAP U ...

  7. 窗体中实现按 回车键 跳到下一个可选的TabIndex控件

    Form中一"textbox",两"button",如何实现在textbox中按下回车响应button.click事件 : 1)把按钮的tabindex依次设置 ...

  8. android menu item 显示,Android 如何通过menu id来得到menu item 控件 .

    Android 如何通过menu id来得到menu item 控件 . (2012-07-21 06:43:31) 标签: android 如何 杂谈 Android 如何通过menu id来得到m ...

  9. 新发现的一个pyqt5的绘图控件QCustomPlot2

    今天发现了一个新的绘图控件QCustomPlot2,据说性能也非常不错,这个图表库原生支持很多种图表类型,这点比pyqtgraph要好点,pyqtgraph本身支持的图表种类不是特别全,但是也算够用吧 ...

  10. 用Silverlight打造一个相对安全的密码控件

    笔者最近的一个项目涉及到了支付动作,出于安全考虑,需要在密码控制上防键盘记录,传输加密等进行处理,其中难点在于防键盘记录. 现有的银行.支付宝.财付通.快钱等的支付控件都是自行开发,还需要对控件进行证 ...

最新文章

  1. Kali Linux发布2020.1a版本
  2. Linux -- Samba用户认证
  3. 零元学Expression Blend 4 - Chapter 1 缘起
  4. 命中率_三分命中率暴涨19%!卡皇进化已无弱项,顶级3D练成何须布拉
  5. Ext grid js上移下移样例
  6. SpringMvc-HandlerMapping/RequestCondition
  7. 和利时dcs系统服务器设置,和利时DCS系统组态流程
  8. 社会化媒体驱动营销转型
  9. ps scavenge java_JVM源码分析(四)Parralel Scavenge 收集器工作流程
  10. VisualStudio2019 安装时下载不动或者显示下载失败
  11. Java中long与float
  12. ss-libev 分析
  13. C++对我来说简直就是星辰大海,为了避免翻船,我选择从小河沟出发
  14. sklearn降维算法1 - 降维思想与PCA实现
  15. 模型微调(finetune)
  16. 前端工程师必备的 10款开发工具
  17. ofo发布“小黄蜂”,想试试一贴即开的新体验吗
  18. 深腾8800型超级计算机,深网|中国超级计算机TOP100榜单:联想曙光各39套并列第一...
  19. 安卓开发培训!一次违反常规的安卓大厂面试经历,实战解析
  20. 野狐网游分析手记(2016年3月28日更新)

热门文章

  1. Efficient and Effective Data Imputation with Influence Functions
  2. 花拳绣腿的「融360」:金融AI第一股今安在?|| 新芒X
  3. html鼠标各种坐标,HTML坐标系与鼠标事件坐标
  4. python 切割图_python切割图片的示例
  5. 如何查看Steam的17位Id
  6. 飞思卡尔智能车之舵机算法
  7. 医院在线预约挂号系统开源
  8. ★☆★新书已经到手《Java程序员,上班那点事儿》正式销售纪念帖★☆★
  9. 【虚拟机里测试Windows PE的方法】
  10. 在2016年度山东省计算机技能大赛中,学院在2016年山东省职业院校技能大赛中再获佳绩...