有一段时间没有写东西了,是由于最近换工做了,忙着适应新的同事,新的环境和新的项目。得空的时候有个朋友给了我一个需求,让我有时间帮他看看,他在忙别的,没时间弄,因此我就作了一下。java
另外说点题外话,最近P2P暴雷特别多,我表示表面看上去冷静,心里其实慌的一匹,投资的标的又遇到了展期,非常担忧。也劝诫各位,投资需谨慎,P2P更加如此。下面的这个需求也是服务P2P项目的。git
首先会显示出当前日期,以及给定的回款的数据显示;日历区域能够左右滑动,上面月份会对应切换;上面月份能够左右滑动,点击对应月份完成切换github
其实对于每个需求,不管大小,咱们都须要进行一个需求的分析,而后分块来处理,将大需求划分红小的需求来实现。缓存
对于上面的需求,咱们能够将其分解成如下几点:
1、日历View的实现
2、月份选择区域的实现
3、效果联动ide
对于日历View的实现,咱们大体须要考虑三点,第一个是显示日期的View的建立和布局,这里咱们须要考虑到底须要多少个控件才能装下全部数据;第二个是每月的具体日期的显示,主要考虑的点是这个月的第一天是星期几,方便绘制;第三点就是对特殊点的处理,主要考虑的是被点击日期的展现效果和回款日期的展现效果;最后一点就是将View放进ViewPager实现左右滑动切换日历。布局
第一点:咱们须要初始化显示星期的控件和初始化显示具体日期的控件ui
//初始化头部布局 private void initHeadView(Context context) { //获取当前时间 记录当前年月日保存下来 Date dt = new Date(); SimpleDateFormat matter = new SimpleDateFormat("yyyy MM dd"); if (currYear == 0) tYear = currYear = Integer.parseInt(matter.format(dt).split(" ")[0]); if (currMonth == 0) tMonth = currMonth = Integer.parseInt(matter.format(dt).split(" ")[1]); tDay = currDay = Integer.parseInt(matter.format(dt).split(" ")[2]); //添加头部的显示星期的布局 LinearLayout llWeek = new LinearLayout(context); llWeek.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); llWeek.setOrientation(HORIZONTAL); for (int i = 0; i < 7; i++) { //把最终显示星期的TextView添加到LinearLayout里面去 TextView week = new TextView(context); LayoutParams lpWeek = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); lpWeek.weight = 1; week.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); week.setText(weeks[i]); week.setTextColor(mWeekColor); week.setTextSize(14); week.setGravity(Gravity.CENTER); llWeek.addView(week, lpWeek); } //将星期添加到视图中 addView(llWeek); } //初始化总体布局 private void initBodyView(Context context) { int margin = (int) (10 * scaleSize); setPadding(0, 20, 0, 50 - margin); //添加日期的数据 6行 for (int i = 0; i < 6; i++) { layouts[i] = new LinearLayout(context); layouts[i].setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 1)); layouts[i].setOrientation(HORIZONTAL); for (int j = 0; j < 7; j++) { containers[i][j] = new LinearLayout(context); containers[i][j].setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 1)); containers[i][j].setOrientation(LinearLayout.VERTICAL); containers[i][j].setGravity(Gravity.CENTER); days[i][j] = new TextView(context); LayoutParams lpWeek = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); lpWeek.weight = 1; lpWeek.setMargins(margin, margin, margin, margin); days[i][j].setLayoutParams(new LayoutParams(defaultWidth, defaultWidth)); days[i][j].setTextColor(mDaysColor); days[i][j].setTextSize(14); days[i][j].setGravity(Gravity.CENTER); containers[i][j].addView(days[i][j]); LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); layoutParams.setMargins(0, (int) (2 * scaleSize), 0, 0); huikuan[i][j] = new TextView(context); huikuan[i][j].setLayoutParams(layoutParams); huikuan[i][j].setTextColor(Color.parseColor("#F8E71C")); huikuan[i][j].setTextSize(10); huikuan[i][j].setGravity(Gravity.CENTER); ----------------------------------------- containers[i][j].addView(huikuan[i][j]); containers[i][j].setOnClickListener(this); layouts[i].addView(containers[i][j]); } addView(layouts[i]); } }
第二点:获取每个月第一天对应星期几,这样方便从对应的位置开始绘制日期数this
获取指定年份指定月份的第一天的位置spa
//获取指定年份指定月份的第一天的位置 private int getDaysOfMonth(int year, int month) { Calendar cal = Calendar.getInstance(); cal.set(Calendar.YEAR, year); cal.set(Calendar.MONTH, month - 1); return cal.getActualMaximum(Calendar.DATE); }
获取指定年份指定月份的最后一天.net
//获取指定年份指定月份的最后一天 public int getLastDayOfMonth(int year, int month) { month = month - 1; if (month == 0) { month = 12; year = year - 1; } month = month - 1; if (month == 0) { month = 12; year = year - 1; } Calendar cal = Calendar.getInstance(); cal.set(Calendar.YEAR, year); cal.set(Calendar.MONTH, month); cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DATE)); return Integer.parseInt(new SimpleDateFormat("dd").format(cal.getTime())); }
第三点:根据上面获取的数据进行日期的设置
咱们获取到指定年份指定月份的第一天的位置以后,开始日后遍历累加设置日期,直到累加到指定年份指定月份的最后一天。剩下的就是设置设置上个月的日期,以及下个月的日期。上个月的日期直接首先获取上个月的天数,而后从这个月第一天的位置向前依次累减便可,下个月的直接从0直接累计设置便可。最后就是处理点击事件,给每一个TextView设置一个事件监听,而后统一作回调处理并设置相关样式便可,为了清晰简单,这里就再也不赘述了。
具体代码实现
//填充数据 private void putData(List<String> dates) { //获取上个月的最后一天 int lastMonthLastDay = getLastDayOfMonth(currYear, currMonth); //获取这个月第一天对应的星期 int firstDayOfMonth = getFisrtDayOfMonth(currYear, currMonth); //获取这个月的天数 int daysOfMonth = getDaysOfMonth(currYear, currMonth); int index = 1; int temp = 0; for (int i = 0; i < 6; i++) { for (int j = 0; j < 7; j++) { //第一行数据 可能包含上个月日期 if (i == 0) { if (j >= firstDayOfMonth - 1) { days[i][j].setText((firstDay++) + ""); days[i][j].setTextColor(Color.parseColor("#FFFFFF")); temp++; } else { //上个月的日期 days[i][j].setText((lastMonthLastDay - firstDayOfMonth + 2 + j) + ""); days[i][j].setTextColor(Color.parseColor("#AEEC8B")); } } else { if (firstDay <= daysOfMonth) { days[i][j].setText((firstDay++) + ""); days[i][j].setTextColor(Color.parseColor("#FFFFFF")); if (i < 5) { temp++; } } else { //下个月的日期 days[i][j].setText((index++) + ""); days[i][j].setTextColor(Color.parseColor("#AEEC8B")); } } } } if (temp < daysOfMonth) { //说明5行并无显示完 须要显示第6行 layouts[5].setVisibility(VISIBLE); } else { //说明5行显示完了 隐藏第6行 layouts[5].setVisibility(GONE); } }
第四点:结合ViewPager实现左右滑动效果
要实现需求中左右滑动的效果,不二选择就是ViewPager了。不过咱们须要注意一下View的复用,因此咱们要对视图进行缓存,这一块不用多说,看下具体适配器的实现,注释很清楚就很少说了。而后经过setPrimaryItem获取到当前操做的视图,对此视图进行数据的绑定操做。
public class CalendarPagerAdapter extends PagerAdapter { //缓存上一次回收的CalendarView private LinkedList<CalendarView> cache = new LinkedList<>(); //记录当前展现的View private CalendarView currView; //记录须要展现的数量 private int count; //初始化事件监听 private CalendarView.OnItemClickListener listener; public CalendarPagerAdapter(int count, CalendarView.OnItemClickListener listener) { this.count = count; this.listener = listener; } public void setHuiKuan(List<String> dates) { if (currView != null) currView.setHuiKuan(dates); } @Override public void setPrimaryItem(ViewGroup container, int position, Object object) { currView = (CalendarView) object; } @Override public int getCount() { return count; } @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override public Object instantiateItem(ViewGroup container, int position) { //尽量复用View final CalendarView view; if (!cache.isEmpty()) { view = cache.removeFirst(); //根据position获取当前页面须要展现的年份和月份 int[] yearAndMonth = getYearAndMonth(position); view.setData(yearAndMonth[0], yearAndMonth[1]); } else { view = new CalendarView(container.getContext()); //根据position获取当前页面须要展现的年份和月份 int[] yearAndMonth = getYearAndMonth(position); view.setData(yearAndMonth[0], yearAndMonth[1]); } if (listener != null) view.setListener(listener); container.addView(view); return view; } //记录开始的年份 咱们这里是从2014-5 到 这个月后面的36个月 int startYear = 2014; int startMonth = 5; int afterMonth = 36; //根据position获取到此页面须要展现年月份的数据 public int[] getYearAndMonth(int position) { int cMonth = startMonth + position; int cYear = startYear + cMonth / 12; if (cMonth % 12 == 1) { //增长一年 cMonth = 1; } else if (cMonth % 12 == 0) { //正好12月 cMonth = 12; cYear--; } else { cMonth = cMonth % 12; } return new int[]{cYear, cMonth}; } //根据年月反推position public int getPosition(int[] yearAndMonth) { int year = yearAndMonth[0]; int month = yearAndMonth[1]; //计算须要展现的全部月数 if (year == startYear) { if (month > startMonth) { return month - startMonth; } else { return 0; } } return (year - startYear - 1) * 12 + (12 - startMonth) + month; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView((CalendarView) object); cache.addLast((CalendarView) object); } }
需求中有一个很重要的点就是当前月份须要居中展现在全部的可视月份中。好比你当前选择了8月,那么上面的区域会展现6,7,8,9,10月,并且8月是在正中间的。首先,能够在86个月中进行滑动选择(86个月指的是咱们的需求,从2014-5到本月以后的36个月),咱们考虑使用RecyclerView来实现。为了保证居中,咱们须要RecyclerView的item的宽度是整个RecyclerView的五分之一,那么每次显示5个,5个正好能够充满整个容器布局;其次,在每次选定月份以后,咱们须要自动的获取须要滑动的距离,而后自动滚动到对应的月份,并保证该月份处于正中间状态。
其中最难的部分可能就是怎么保证被选中的月份如何居中显示在RecyclerView中吧。核心方法就是mRecycleView.smoothScrollBy(distance,duration);
//自动滚动到指定position private void autoSmooth(int position){ //记录第一个可视Item距离最左边的距离 int top = 0; //获取可视的第一个item下标 int pFirst = linearLayoutManager.findFirstVisibleItemPosition(); //获取可视的第一个View View viewByPosition = linearLayoutManager.findViewByPosition(pFirst); //获取可视的第一个View的left 做为滑动的依据 if (viewByPosition != null) top = viewByPosition.getLeft(); //获取每个Item的宽度 咱们默认一屏显示5条 因此除以5 float itemViewHeight = recyclerViewWidth / 5; //计算须要滑动的Item的数量 这个本身比划算一算 int needScrollPostion = position - pFirst - 3; //计算最终须要滑动的距离 为负数就是向相反方向滑动 int distance = (int) (needScrollPostion * itemViewHeight + (itemViewHeight - Math.abs(top))); //开始滑动 recyclerview.smoothScrollBy(distance, 10); }
这一部分就是将ViewPager的左右选择做用到上面的月份选择上,实现实时匹配;再将上面的月份选择的点击选择某一个月份的事件做用到ViewPager上,也就是viewPager.setCurrentItem(position);这一部分的实现都在MainActivity.java中,感兴趣能够去看看。
其实相似日历的成熟的解决方案有不少不少,为何仍是选择了本身写呢?一是由于相似的解决方案很是多,却不必定是百分百契合你的需求,别人的解决方案在功能上或多或少会与你产品不一样,你须要去修改别人的方案了;再者,可能别人的效果更加炫酷,却不必定是你产品经理真正须要的。二是本身写的代码会带来什么样的坑本身内心是清楚的,第三方的却不必定清楚,在使用的时候内心不免不踏实。因此,在有时间和精力的状况下,能够尝试参考别人的实现本身去写属于本身的功能。
还有一个点须要解释一下,为何没有将这个需求封装成一个library让别人引用而是直接将逻辑写在MainActivity中呢?由于我以为这个需求并无什么特别多的公用元素在里面,他跟实际的业务须要结合很紧(回款),若是包成library,开发者在调用的时候须要对很是多的地方进行修改,不够通用,要封装只能对自定义的那个CalendarView进行封装,可是他的功能又很简单(这彻底是基于需求),全部就索性不作特殊处理了。
最后献上项目地址: SuperCalendarDemo
若是链接失效就直接点击这个连接吧!https://github.com/MZCretin/SuperCalendarDemo
我就是比较喜欢用代码解决生活中的问题,感受很开心,哈哈哈。也但愿你们关注个人简书,掘金,Github和CSDN。
简书首页,连接是 https://www.jianshu.com/u/123...
掘金首页,连接是 https://juejin.im/user/5838d5...
Github首页,连接是 https://github.com/MZCretin
CSDN首页,连接是 http://blog.csdn.net/u010998327
我是Cretin,一个可爱的小男孩。