开源日历控件DatePicker源码解析

来源:互联网 发布:必修3基本算法语句ppt 编辑:程序博客网 时间:2024/06/16 10:14
在一些项目开发中,会使用日历去标识事务,所以根据美工出的效果图,我们可以采用不同的方法去实现。比如通过GridView扣扣你敢、自定义View实现日历控件,这些都是我们解决问题的手段,我也实现过一个自定义日历控件(Android自定义控件之日历控件55993)),由于我只是粗糙的进行实现,并没有进行过多的在控件的可扩展性上进行打磨设计,所以在本篇文章中,我秉着学习的态度分析下爱哥的鼎力巨作DatePicker-DatePicker。



DatePicker开源项目地址:[https://github.com/AigeStudio/DatePicker](https://github.com/AigeStudio/DatePicker)

一、项目概述
就我个人情况而言,我从源码的角度去分析这个项目的原因是为了学习该项目的设计,项目中是如何通过类之间的继承提高项目的扩展性,同时学习项目中对类的抽取与分层的实现。还是先看整个项目的结构图:



通过上面的图,我们可以概述项目中的类分布在以下五个包中:
bizs:包含相关的日历业务处理类
cons:包含日期的选择模式类
entities:包含日历涉及的实体类
utils:包含日历控件使用的工具类
views:日历控件View类

DatePicker项目通过将业务逻辑处理抽取出来,给View类的实现做减压工作,提高了代码的清晰度,避免View层过于臃肿,代码的可读性瞬间提供。下面我们就探究下各个类的作用与相关源码。
二、DatePicker源码结构简析

(一)、bizs包   
通过上面的结构图我们知道在bizs包下存在四个子包,这四个子包中分别包含响应的业务逻辑处理。
calendars:日历处理相关的业务逻辑,比如农历日期、节气的相关处理类
decors:装饰处理类,绘制左上方、右上方、下方等处的标记信息,该类需要重写相关方法自己实现。
languages:日历展示语言处理类,设计汉语展示、英语展示;
themes:日历主题颜色相关类,比如日历中字体颜色、背景颜色,通过类进行管理设定,当然也可以使用自定义属性,直接在xml文件中进行配置。
(1)、calendars包    
calendars包下包含日历View中所涉及到的节气节日相关的类,主要由五个类组成:
DPCalendar:月历抽象父类,包含一些常用方法:判断是否为闰年、给定日期是否为今天等。
DPCManager:日期管理类,包含一些常用的日期操作方法。
DPCNCalendar:中国日历类,继承自DPCalendar,里面新增中国节日相关的方法
DPUSCalendar:美国日历,继承自DPCalendar,包含美国节日处理的相关方法
SolarTerm:农历24节气算法处理类

为什么将节日处理的相关类这样设计呢?而不是将这个包下的类整合到utils包下形成一个方法类呢?我想这也是爱哥考虑到日历针对国语和英语版对节日处理不同造成的,如果完全封装在一个类中,显然逻辑处理过于复杂,代码量太大,可读性太差。所以,设计了DPCalendar这个抽象类,在这个抽象类中封装了一个protected修饰符的Calendar对象,保证了子类的可访问性,针对子类特点提取出公用方法,比如isLeapYear()、isToday(),同时提取出子类之间的差别方法,定义成抽象方法交由子类去实现,例如:buildMonthFestival()、buildMonthHoliday()。这样的设计是不是很有借鉴意义,对业务的深入分析,将类进行合理的设计很大程度上提高了代码的可读性,避免了代码的臃肿。我们就简要看下缩略版的代码结构,为了减少代码的量,我删除了部分代码:

   public abstract class DPCalendar {        protected final Calendar c = Calendar.getInstance();        public abstract String[][] buildMonthFestival(int year, int month);        public abstract Set<String> buildMonthHoliday(int year, int month);        public boolean isLeapYear(int year) {            return (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0));        }        public boolean isToday(int year, int month, int day) {            Calendar c1 = Calendar.getInstance();            Calendar c2 = Calendar.getInstance();            c1.set(year, month - 1, day);            return (c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR)) &&                    (c1.get(Calendar.MONTH) == (c2.get(Calendar.MONTH))) &&                    (c1.get(Calendar.DAY_OF_MONTH) == c2.get(Calendar.DAY_OF_MONTH));        }        .....    }
DPCNCalendar类简要实现: 
   public class DPCNCalendar extends DPCalendar {            private static final String[] NUMBER_CAPITAL = {"零", "一", "二", "三", "四", "五", "六", "七", "八", "九"};            private static final String[] LUNAR_HEADER = {"初", "十", "廿", "卅", "正", "腊", "冬", "闰"};            @Override        public String[][] buildMonthFestival(int year, int month) {            return buildMonthL(year, month);        }            private String[][] buildMonthL(int year, int month) {           ....        }            /**         * 判断某年某月某日是否为节气         *         * @param year  公历年         * @param month 公历月         * @param day   公历日         * @return ...         */        public boolean isSolarTerm(int year, int month, int day) {            return null == getSolarTerm(year, month, day);        }        ......    }
DPUSCalendar类的简要设计实现:
  public class DPUSCalendar extends DPCalendar {        ........            @Override        public String[][] buildMonthFestival(int year, int month) {            String[][] gregorianMonth = buildMonthG(year, month);            String tmp[][] = new String[6][7];            for (int i = 0; i < tmp.length; i++) {                for (int j = 0; j < tmp[0].length; j++) {                    tmp[i][j] = "";                    String day = gregorianMonth[i][j];                    if (!TextUtils.isEmpty(day)) {                        tmp[i][j] = getFestivalG(month, Integer.valueOf(day));                    }                }            }            return tmp;        }            @Override        public Set<String> buildMonthHoliday(int year, int month) {            Set<String> tmp = new HashSet<>();            if (year == 2015) {                Collections.addAll(tmp, HOLIDAY[month - 1]);            }            return tmp;        }            private String getFestivalG(int month, int day) {            String tmp = "";            int[] daysInMonth = FESTIVAL_G_DATE[month - 1];            for (int i = 0; i < daysInMonth.length; i++) {                if (day == daysInMonth[i]) {                    tmp = FESTIVAL_G[month - 1][i];                }            }            return tmp;        }    }

上面就是三个类之间的缩略版,类之间的合理设计很有必要,也很重要,有时真的不是仅仅提取出来当道utils工具类中那么简单的一件事情。所以还是很有必须深入研究业务逻辑,针对类进行合理的设计,能让代码的整体性看起来很“舒心”。在calendars包中还有最后一个类SolarTerm类,该类是针对农历日期进行处理的类,没什么特别说的,毕竟怎么处理的不是我们研究的重点,有兴趣的可以下载源码研究下。


(2)、decors包    
decors包下只包含DPDecor一个类用于处理装饰,这里说的装饰标记不如说事务标记更合适,通过在日期的左上方、右上方等位置绘制一些独特的标志,比如绘制圆形、正方形等。
   public class DPDecor {        /**         * 绘制当前日期区域左上角的装饰物         * Draw decor on Top left of current date area         *         * @param canvas 绘制图形的画布 Canvas of image drew         * @param rect   可以绘制的区域范围 Area you can draw         * @param paint  画笔对象 Paint         * @param data   日期         */        public void drawDecorTL(Canvas canvas, Rect rect, Paint paint, String data) {            drawDecorTL(canvas, rect, paint);        }            /**         * @see #drawDecorTL(Canvas, Rect, Paint, String)         */        public void drawDecorTL(Canvas canvas, Rect rect, Paint paint) {            }        public void drawDecorT(Canvas canvas, Rect rect, Paint paint, String data) {            drawDecorT(canvas, rect, paint);        }        public void drawDecorT(Canvas canvas, Rect rect, Paint paint) {            }            public void drawDecorTR(Canvas canvas, Rect rect, Paint paint, String data) {            drawDecorTR(canvas, rect, paint);        }        public void drawDecorTR(Canvas canvas, Rect rect, Paint paint) {            }            public void drawDecorL(Canvas canvas, Rect rect, Paint paint, String data) {            drawDecorL(canvas, rect, paint);        }        public void drawDecorL(Canvas canvas, Rect rect, Paint paint) {            }        public void drawDecorR(Canvas canvas, Rect rect, Paint paint, String data) {            drawDecorR(canvas, rect, paint);        }        public void drawDecorR(Canvas canvas, Rect rect, Paint paint) {            }            public void drawDecorBG(Canvas canvas, Rect rect, Paint paint, String data) {            drawDecorBG(canvas, rect, paint);        }        public void drawDecorBG(Canvas canvas, Rect rect, Paint paint) {            }    }
与我的[Android自定义控件之日历控件](http://blog.csdn.net/mr_dsw/article/details/48755993)对比会发现,这样做会大大减少View层中的代码量,我在[Android自定义控件之日历控件](http://blog.csdn.net/mr_dsw/article/details/48755993)中业务逻辑也掺杂在View的绘制中进行处理,代码臃肿性可想而知,这样做将这部分功能提取处理,只需要在View中持有该类的一个对象即可,用户可以通过继承该类进行自定义的“装饰”绘制,提高扩展性,当然如果有兴趣,也可以个用户实现几个通用常见的样式,方便使用。我个人理解,根据装饰类的设计思想,我们在进行设计时候完全可以把针对日历View的一些边角“装饰”的功能提取出来,提高扩展性,降低代码的臃肿。

(3)、languages包     
语言包,该包下包含了语言处理的相关类,比如标题头中对应中英文的:一月/January、二月/February等之间的差别展示,所以这个类的设计跟上面类的设计思路是一样的,同样是定义父类DPLManager作为语言管理抽象父类,抽取出子类之间的差异性形成抽象方法,然后交由子类去实现。
   public abstract class DPLManager {        private static DPLManager sLanguage;            /**         * 获取日历语言管理器         *         * Get DatePicker language manager         *         * @return 日历语言管理器 DatePicker language manager         */        public static DPLManager getInstance() {            if (null == sLanguage) {                String locale = Locale.getDefault().getLanguage().toLowerCase();                if (locale.equals("zh")) {                    sLanguage = new CN();                } else {                    sLanguage = new EN();                }            }            return sLanguage;        }            /**         * 月份标题显示         *         * Titles of month         *         * @return 长度为12的月份标题数组 Array in 12 length of month titles         */        public abstract String[] titleMonth();            /**         * 确定按钮文本         *         * Text of ensure button         *         * @return Text of ensure button         */        public abstract String titleEnsure();            /**         * 公元前文本         *         * Text of B.C.         *         * @return Text of B.C.         */        public abstract String titleBC();            /**         * 星期标题显示         *         * Titles of week         *         * @return 长度为7的星期标题数组 Array in 7 length of week titles         */        public abstract String[] titleWeek();    }
中文系统下:
    public class CN extends DPLManager {        @Override        public String[] titleMonth() {            return new String[]{"一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"};        }            @Override        public String titleEnsure() {            return "确定";        }            @Override        public String titleBC() {            return "公元前";        }            @Override        public String[] titleWeek() {            return new String[]{"日", "一", "二", "三", "四", "五", "六"};        }    }
英语系统下:
    public class EN extends DPLManager {        @Override        public String[] titleMonth() {            return new String[]{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"};        }            @Override        public String titleEnsure() {            return "Ok";        }            @Override        public String titleBC() {            return "B.C.";        }            @Override        public String[] titleWeek() {            return new String[]{"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"};        }    }
设计思想与上面是相同的。

(4)、themes包     
该包下包含了对常见模块的主题颜色设置,实现思路同上,也是首先抽取一个主题基类,然后派生出不同颜色的子类,好处不必多言,简单例子如下:
   public abstract class DPTheme {        /**         * 月视图背景色         *          * Color of MonthView's background         *         * @return 16进制颜色值 hex color         */        public abstract int colorBG();            /**         * 背景圆颜色         *          * Color of MonthView's selected circle         *         * @return 16进制颜色值 hex color         */        public abstract int colorBGCircle();            /**         * 标题栏背景色         *          * Color of TitleBar's background         *         * @return 16进制颜色值 hex color         */        public abstract int colorTitleBG();            /**         * 标题栏文本颜色         *          * Color of TitleBar text         *         * @return 16进制颜色值 hex color         */        public abstract int colorTitle();            /**         * 今天的背景色         *          * Color of Today's background         *         * @return 16进制颜色值 hex color         */        public abstract int colorToday();            /**         * 公历文本颜色         *          * Color of Gregorian text         *         * @return 16进制颜色值 hex color         */        public abstract int colorG();            /**         * 节日文本颜色         *          * Color of Festival text         *         * @return 16进制颜色值 hex color         */        public abstract int colorF();            /**         * 周末文本颜色         *          * Color of Weekend text         *         * @return 16进制颜色值 hex color         */        public abstract int colorWeekend();            /**         * 假期文本颜色         *          * Color of Holiday text         *         * @return 16进制颜色值 hex color         */        public abstract int colorHoliday();    }DPBaseTheme类的实现:    public class DPBaseTheme extends DPTheme {        @Override        public int colorBG() {            return 0xFFFFFFFF;        }            @Override        public int colorBGCircle() {            return 0x44000000;        }            @Override        public int colorTitleBG() {            return 0xFFF37B7A;        }            @Override        public int colorTitle() {            return 0xEEFFFFFF;        }            @Override        public int colorToday() {            return 0x88F37B7A;        }            @Override        public int colorG() {            return 0xEE333333;        }            @Override        public int colorF() {            return 0xEEC08AA4;        }            @Override        public int colorWeekend() {            return 0xEEF78082;        }            @Override        public int colorHoliday() {            return 0x80FED6D6;        }    }
(二)、cons包   
该包中就包含DPMode一个类,用于定义日历View的选择模式。 
   /**     * 日期选择模式     * 支持单选和多选和展示     * Date select mode     * Support SINGLE or MULTIPLE or Display only.     *     * @author AigeStudio 2015-07-02     */    public enum DPMode {        SINGLE, MULTIPLE, NONE    }
(三)、entities包 
该包是封装实体类的集合,在该项目中该包下仅包含一个类DPInfo,用于封装绘制日历所需要的一些数据。
    public class DPInfo {        public String strG, strF;        public boolean isHoliday;        public boolean isToday, isWeekend;        public boolean isSolarTerms, isFestival, isDeferred;        public boolean isDecorBG;        public boolean isDecorTL, isDecorT, isDecorTR, isDecorL, isDecorR;    }
(四)、utils包   
该包用于存放常用的工具类,在本项目中,该包下包含两个工具类DataUtils、MeasureUtil。
(五)、views包  
views包下存储我们的自定义日历View。该包下包含两个文件DatePicker、MonthView。其中MonthView是我们日历绘制处理的自定义View,DatePicker是一个继承自LinearLayout的自定义View,用于对日历控件的样式进行定义,比如月份的展示、星期的展示都是在DatePicker中进行处理。

三、DatePicker核心技术点简要分析
1、DatePicker基本样式是怎么开发实现?      
这里就需要我们透过DatePicker的源码去分析。
    public class DatePicker extends LinearLayout {        private DPTManager mTManager;// 主题管理器        private DPLManager mLManager;// 语言管理器            private MonthView monthView;// 月视图        private TextView tvYear, tvMonth;// 年份 月份显示        private TextView tvEnsure;// 确定按钮显示    .....    }
通过源码可知,我们的DatePicker其实继承自LinearLayout,然后在该View中包含了相关的View(monthView、tv_Year等),通过构造函数进行控件的布局设置,最后达到一个样式的展示。代码如下:
     public DatePicker(Context context, AttributeSet attrs) {        super(context, attrs);        mTManager = DPTManager.getInstance();        mLManager = DPLManager.getInstance();        // 设置排列方向为竖向        setOrientation(VERTICAL);        LayoutParams llParams =                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);        // 标题栏根布局        RelativeLayout rlTitle = new RelativeLayout(context);        rlTitle.setBackgroundColor(mTManager.colorTitleBG());        int rlTitlePadding = MeasureUtil.dp2px(context, 10);        rlTitle.setPadding(rlTitlePadding, rlTitlePadding, rlTitlePadding, rlTitlePadding);        // 周视图根布局        LinearLayout llWeek = new LinearLayout(context);        llWeek.setBackgroundColor(mTManager.colorTitleBG());        llWeek.setOrientation(HORIZONTAL);        int llWeekPadding = MeasureUtil.dp2px(context, 5);        llWeek.setPadding(0, llWeekPadding, 0, llWeekPadding);        LayoutParams lpWeek = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);        lpWeek.weight = 1;        // 标题栏子元素布局参数        RelativeLayout.LayoutParams lpYear =                new RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);        lpYear.addRule(RelativeLayout.CENTER_VERTICAL);        RelativeLayout.LayoutParams lpMonth =                new RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);        lpMonth.addRule(RelativeLayout.CENTER_IN_PARENT);        RelativeLayout.LayoutParams lpEnsure =                new RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);        lpEnsure.addRule(RelativeLayout.CENTER_VERTICAL);        lpEnsure.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);        // --------------------------------------------------------------------------------标题栏        // 年份显示        tvYear = new TextView(context);        tvYear.setText("2015");        tvYear.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);        tvYear.setTextColor(mTManager.colorTitle());        // 月份显示        tvMonth = new TextView(context);        tvMonth.setText("六月");        tvMonth.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20);        tvMonth.setTextColor(mTManager.colorTitle());        // 确定显示        tvEnsure = new TextView(context);        tvEnsure.setText(mLManager.titleEnsure());        tvEnsure.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);        tvEnsure.setTextColor(mTManager.colorTitle());        tvEnsure.setOnClickListener(new OnClickListener() {            @Override            public void onClick(View v) {                if (null != onDateSelectedListener) {                    onDateSelectedListener.onDateSelected(monthView.getDateSelected());                }            }        });        rlTitle.addView(tvYear, lpYear);        rlTitle.addView(tvMonth, lpMonth);        rlTitle.addView(tvEnsure, lpEnsure);        addView(rlTitle, llParams);        // --------------------------------------------------------------------------------周视图        for (int i = 0; i < mLManager.titleWeek().length; i++) {            TextView tvWeek = new TextView(context);            tvWeek.setText(mLManager.titleWeek()[i]);            tvWeek.setGravity(Gravity.CENTER);            tvWeek.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14);            tvWeek.setTextColor(mTManager.colorTitle());            llWeek.addView(tvWeek, lpWeek);        }        addView(llWeek, llParams);        // ------------------------------------------------------------------------------------月视图        monthView = new MonthView(context);        monthView.setOnDateChangeListener(new MonthView.OnDateChangeListener() {            @Override            public void onMonthChange(int month) {                tvMonth.setText(mLManager.titleMonth()[month - 1]);            }            @Override            public void onYearChange(int year) {                String tmp = String.valueOf(year);                if (tmp.startsWith("-")) {                    tmp = tmp.replace("-", mLManager.titleBC());                }                tvYear.setText(tmp);            }        });        addView(monthView, llParams);    }
在构造函数中,首先进行DPTManager(样式管理器)、DPLManager(语言管理器)的初始化,然后设置该DatePicker的展示方向。接着定义了名为rlTitle的RelativeLayout对象和一个名为llWeek的LinearLayout对象,分别用于存放头部的月份标题控件和星期控件。中间涉及较多的就是LayoutParams布局参数的使用,动态进行控件的布局(通过代码实现布局)。这样就完成了整体的样式结构搭建。

2、MonthView的简要分析。     
MonthView这个类是整个项目的核心部分,它完成了最主要的日历日期的绘制以及相关的动画处理和装饰效果的绘制。    
(1)、翻页效果的处理。    
在项目中,对日历控件进行滑动实现翻页效果,我们知道Android中通过Scroller类实现滑动,那么我们怎么达到这种滑动的时候下个月的月份也出来了呢?这就需要我们提前绘制好,
 
  @Override    protected void onDraw(Canvas canvas) {        canvas.drawColor(mTManager.colorBG());        draw(canvas, width * indexMonth, (indexYear - 1) * height, topYear, topMonth);        draw(canvas, width * (indexMonth - 1), height * indexYear, leftYear, leftMonth);        draw(canvas, width * indexMonth, indexYear * height, centerYear, centerMonth);        draw(canvas, width * (indexMonth + 1), height * indexYear, rightYear, rightMonth);        draw(canvas, width * indexMonth, (indexYear + 1) * height, bottomYear, bottomMonth);        drawBGCircle(canvas);    }
我们可以看到在onDraw()方法中已经提前绘制了选中月份的上下左右对应月份的日期,所以我们通过Scroller进行滑动的时候就能将下月/上月的日期缓慢展示出来。这里需要在onTouchEvent中判断是左右滑动还是上下滑动,左右滑动是翻月份,上下滑动是翻年份。
   @Override    public boolean onTouchEvent(MotionEvent event) {        switch (event.getAction()) {            case MotionEvent.ACTION_DOWN:                mScroller.forceFinished(true);                mSlideMode = null;                isNewEvent = true;                lastPointX = (int) event.getX();                lastPointY = (int) event.getY();                break;            case MotionEvent.ACTION_MOVE:                if (isNewEvent) {                    if (Math.abs(lastPointX - event.getX()) > 100) {                        mSlideMode = SlideMode.HOR;                        isNewEvent = false;                    } else if (Math.abs(lastPointY - event.getY()) > 50) {                        mSlideMode = SlideMode.VER;                        isNewEvent = false;                    }                }                if (mSlideMode == SlideMode.HOR) {                    int totalMoveX = (int) (lastPointX - event.getX()) + lastMoveX;                    smoothScrollTo(totalMoveX, indexYear * height);                } else if (mSlideMode == SlideMode.VER) {                    int totalMoveY = (int) (lastPointY - event.getY()) + lastMoveY;                    smoothScrollTo(width * indexMonth, totalMoveY);                }                break;            case MotionEvent.ACTION_UP:                if (mSlideMode == SlideMode.VER) {                    if (Math.abs(lastPointY - event.getY()) > 25) {                        if (lastPointY < event.getY()) {                            if (Math.abs(lastPointY - event.getY()) >= criticalHeight) {                                indexYear--;                                centerYear = centerYear - 1;                            }                        } else if (lastPointY > event.getY()) {                            if (Math.abs(lastPointY - event.getY()) >= criticalHeight) {                                indexYear++;                                centerYear = centerYear + 1;                            }                        }                        buildRegion();                        computeDate();                        smoothScrollTo(width * indexMonth, height * indexYear);                        lastMoveY = height * indexYear;                    } else {                        defineRegion((int) event.getX(), (int) event.getY());                    }                } else if (mSlideMode == SlideMode.HOR) {                    if (Math.abs(lastPointX - event.getX()) > 25) {                        if (lastPointX > event.getX() &&                                Math.abs(lastPointX - event.getX()) >= criticalWidth) {                            indexMonth++;                            centerMonth = (centerMonth + 1) % 13;                            if (centerMonth == 0) {                                centerMonth = 1;                                centerYear++;                            }                        } else if (lastPointX < event.getX() &&                                Math.abs(lastPointX - event.getX()) >= criticalWidth) {                            indexMonth--;                            centerMonth = (centerMonth - 1) % 12;                            if (centerMonth == 0) {                                centerMonth = 12;                                centerYear--;                            }                        }                        buildRegion();                        computeDate();                        smoothScrollTo(width * indexMonth, indexYear * height);                        lastMoveX = width * indexMonth;                    } else {                        defineRegion((int) event.getX(), (int) event.getY());                    }                } else {                    defineRegion((int) event.getX(), (int) event.getY());                }                break;        }        return true;    }
这里的判断都是在onTouchEvent的ACTION_MOVE事件中进行处理,当水平方法滑动差值达到100的时候就认为是水平滑动,当竖直方向滑动差值达到50就认为是竖直滑动,然后调用smoothScrollTo()方法进行滑动处理。同样当我们滑动一部分手指抬起时的处理在ACTION_UP中进行处理。

项目还有很多技术点需要仔细的分析研究,当然本文的目的是为了学习开源项目的一个架构组织,这是我的出发点,对于里面如何绘制、位置如何计算、动画的实现等相关技术点研究源码仔细学习吧!


============
作者:mr_dsw
地址:http://blog.csdn.net/mr_dsw
转载注明出处,谢谢

============
1 0
原创粉丝点击