自定义控件从入门到轻生之---来个结晶

来源:互联网 发布:花纹板理论价格算法 编辑:程序博客网 时间:2024/05/01 05:43

注:所有blog局限于博主水平有限,很多不足之处大家可以指出共同探讨进步。
尊重原创转载请注明:From 倪大叶(http://blog.csdn.net/renyi0109) 侵权必究!

一般自定义控件分成两大类:
1. 将几个已有的控件“拼接”成一个特有的控件,一般用到的知识就是上两篇blog所写的事件分发,测量等等。。 中心思想就是让几个独立的已有控件根据自己特有的属性加上外部控制协调的一起工作
2. 上篇我们最后一带而过的onDraw方法的丰富实现,也就是自己去画出一个特有的控件,这个的重点主要就是画的工具的学习,path,paint,layer等等类的使用以及他们提供的海量api,不仅如此一些较复杂的控件绘制还需要扎实的数学基础,这个基础可不是指初中高中那些小混混,而是在大学以高数带头,手下集结了离散,线数等一帮凶神恶煞小弟的大学第一帮派, 该帮派在大学为非作歹,奸淫掳掠,无恶不作,人神共愤,多少青年俊杰,黄花闺女折在这帮孙子手上。 鉴于我这几年也是依附在寝室一个天生神力,战无不胜的大神麾下才在这帮恶徒手下勉强偷生,所以这方面就不多讨论。但是也别怕 还是有很多这类需求用一点简单的数学知识加逻辑就能画出来,就算碰到复杂的需求还可以去github上找类似的,如果找不到更不用怕了,直接告诉产品实现不了,所以说问题嘛总有解决办法

今天我就结合前两篇的知识写个通用的下拉刷新控件demo,这demo主要是讲解为主,临时花了点时间做的,很多东西没有考虑进去,BUG什么的也没有调,所以并不适合拿来直接用到项目中,主要是学习一些用法而已。过程中主要讲一些关键点,效果图什么的就不贴了,大家可以到我github(https://github.com/renyindy/SupperRefreshVIew)上去下然后跑起来结合本篇blog来看(千万别直接用到项目中,如果出了问题这锅老子可不背,切记!) 废话不多说 上demo

先分析下结构应该是什么样的,首先不能像传统ListView加头部的方法去做这样违背了通用原则要知道这是listView独有的接口,虽然其他View没有但是我们可以模仿这种做法去给所有需要刷新的View加个”头部”以及”底部”, 试想一下 我们有这么一个容器,中间是放我们需要刷新的View,上下分别是刷新头部和底部那问题不就解决了吗? 这结构脑子随便一想就知道用Linearlayout再合适不过

既然是通用我们就不能只在内容上允许随意更改;刷新,加载更多的样式也应该是可替换的,所以我们先设计一下头部和底部的通用接口:

    public abstract class UpdateSuperView extends LinearLayout {        public static final int STATE_NORMAL = 0;  //常规状态        public static final int STATE_ALREADY = 1;   //已可触发刷新        public static final int STATE_REFRESHING = 2;  //正在刷新        public static final int STATE_LOADING = 3; //正在加载        protected RefreshAndLoadListener mRefreshAndLoadListener;        public UpdateSuperView(Context context) {            super(context);        }        public UpdateSuperView(Context context, AttributeSet attrs) {            super(context, attrs);        }        /**        * 用来更改头部或底部的可见高度        * @param value        */        public abstract void updateHeight(int value);        /**        * 重置头部或底部的可见高度,这个取决于当前state以决定重置为什么高度,不一定是不可见        */        public abstract void reseatHeight();        /**        * 设置当前状态        * @param state        */        public abstract void setState(int state);        /**        * 获取当前状态        * @return        */        public abstract int getState();        /**        * 获取当前头部或底部的可见高度        * @return        */        public abstract int getVisableHeight();        /**        * 上拉加载和下拉刷新触发回调监听        */        public interface RefreshAndLoadListener{            void onRefresh();            void onLoadMore();        }        protected void setRefeshAndLoarListener(RefreshAndLoadListener  refeshAndLoarListener){           this.mRefreshAndLoadListener = refeshAndLoarListener;        }    }

头部和底部有了,再来弄个内容,这里我们就不能让需要刷新的View去继承一个接口,然后再面向接口设计了,因为你不可能让别人还要让想刷新的View都去继承这个接口接着还得自己实现这个接口中的方法吧??这不是拿人作宝搞吗。。 我们设计应该是这样,用的人只用传一个想刷新的View进来,其他就不用管了。 既然不能面向接口设计,那么我们就用一个Holder来包裹一下需要刷新的View,然后面向这个Holder编程就OK了,看一下Holder设计:

    public class RefreshHolder implements AbsListView.OnScrollListener {        //需要刷新的View        private View mChild;        //...........         public void setContentView(View view) {            this.mChild = view;        }        //...........        /**         * 达到顶部监听         *         * @return         */        public boolean isTop() {            if (mChild instanceof AbsListView) { //ListView到达顶部监听                AbsListView absListView = (AbsListView) mChild;                return !canScrollVertically(mChild, -1)                        || absListView.getChildCount() > 0                        && (absListView.getFirstVisiblePosition() == 0 &&   absListView.getChildAt(0).getTop() == 0);           } else if (mChild instanceof ScrollView) { //ScrollView达到顶部监听               ScrollView scrollView = (ScrollView) mChild;               return scrollView.getScrollY() == 0;           } else {               return canScrollVertically(mChild, -1) || mChild.getScrollY() > 0;          }        }        /**         * 到达底部监听         *         * @return         */        public boolean isBottom() {            if (mChild instanceof AbsListView) {//ListView到达底部监听                AbsListView absListView = (AbsListView) mChild;                return !canScrollVertically(mChild, 1);            } else if (mChild instanceof ScrollView) { //ScrollView顶部底部监听              ScrollView scrollView = (ScrollView) mChild;               View childView = scrollView.getChildAt(0);              if (childView != null) {                 return !canScrollVertically(mChild, 1)                         || childView.getMeasuredHeight() <= scrollView.getHeight()     + scrollView.getScrollY();              }          }           return false;        }        //..............    }

这个Holder最关键的就是isTop方法和isBottom方法,我们控件刷新的设计思想就是如果达到顶部继续下拉或者到底部继续上拉就中断事件下发由我们的刷新控件接管事件,并进行头部或者底部的相应处理, 如果不满足这两个控件就将事件传递下去,让内在的View自己去处理。 这里我们只做了ListView和ScrollView的顶部底部监听判断,如果想兼容其他View,比如RecyclerView甚至自定义View就直接在这方面里面添加条件判断即可。

现在我们来看看这个刷新ViewGroup怎么设计,首先先把HeaderView,ContentView(需要刷新的内容View),FooterView依次加入我们的刷新容器中,因为我们的刷新GroupView是一个竖直的Linearlayout,我们只需要将hearView的高度设置为0,ContentView设置为match_parent,FooterView随意给需要的高度就行,这样初始化显示就只有一个ContentView,当我们满足下拉条件的时候依次增加HeaderView的高度,就能让HeadView显示出来达到下拉刷新的效果,而当我们上拉条件满足的时候,我们让FooterView和ContentView一起像上做偏移就能让FooterView显示出来, 可能有人会问了干嘛不用和头部一样的方式先把高度设置为0然后逐渐增大呢? 这么问我只能说你连LinearLayout都没想明白,稍稍动下脑如果用增加高度的方法,那么Linearlayout总共就这么大footerView占用了高度那ContentView是否会被挤压变形呢?如果你又问头部怎么不会被挤压。。。。 那么这个话题我觉得就聊不下去了 。
虽然用偏移的方式是可行的但是我们得改一点Linearlayout的onMeasure逻辑,因为LinearLayout的测量规则是不包含超出显示区域的子View的宽高的,所以我们这里要让Linearlayout根据子View总共有多高我就要设置LinearLayout多高

    /**     * 更改LinearLayout测量逻辑,height=childHeight*childCount     *     * @param widthMeasureSpec     * @param heightMeasureSpec     */    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        int width = MeasureSpec.getSize(widthMeasureSpec);        int childCount = getChildCount();        int finalHeight = 0;        for (int i = 0; i < childCount; i++) {            View child = getChildAt(i);            LayoutParams margins = (LayoutParams) child.getLayoutParams();            if (child.getVisibility() != View.GONE) {                final int childWidthMeasureSpec =                 final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, width);                final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, 0, margins.height);                measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec);                finalHeight += child.getMeasuredHeight();            }        }        setMeasuredDimension(width, finalHeight);    }  

这里我就简单的依次累加了,并没有考虑margin进去,margin对这个控件有点鸡肋,是不建议给里面的ContentView设置margin的,会影响刷新效果很难看, 万事具备就差最关键的事件分发模块了:

    public class SuperRefreshView extends LinearLayout {        @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        switch (ev.getAction()) {            case MotionEvent.ACTION_DOWN:                isIntercept = false;                mLastY = ev.getRawY();                mLastX = ev.getRawX();                break;            case MotionEvent.ACTION_MOVE:                mLastMoveEvent = ev;                int deltaY = (int) (ev.getRawY() - mLastY);                int deltaX = (int) (ev.getRawX() - mLastX);                //headView  处理滑动                if ((mHeadView.getVisableHeight() > 0 || (deltaY > 0 && mRefreshHolder.isTop())) && isRefresh) {                    if (!isShowHeader) {                        isShowHeader = true;                    }                    sendCancelEvent();                    updateHeaderHeight(deltaY);                } else if ((mRefreshHolder.getOffsetY() < 0 || deltaY < 0 && mRefreshHolder.isBottom()) && isLoadMore) { //footerView 处理滑动                    if (!isShowFooter) {                        isShowFooter = true;                    }                    sendCancelEvent();                    updateFooterHeight(deltaY / 2);                }                if (mHeadView.getVisableHeight() <= 0 && isShowHeader && deltaY < 0) {                    isShowHeader = false;                    sendDownEvent();                } else if (mRefreshHolder.getOffsetY() >= 0 && isShowFooter && deltaY > 0) {                    isShowFooter = false;                    sendDownEvent();                }                mLastY = ev.getRawY();                break;            case MotionEvent.ACTION_UP:                if (mHeadView.getVisableHeight() > 0) {                    reseatHeaderHeight();                }                if (mRefreshHolder.getOffsetY() != 0) {                    reseatFooterHeight();                }                break;        }        return super.dispatchTouchEvent(ev);    }

逻辑很简单就不细讲了,但是有个关键的地方大家应该注意到有两个方法 sendCancelEent()和sendDownEvent(), 这两个方法是干嘛的先暂且不说,我们来看看整个刷新控件中唯一的难点,试想一种情况: 当一开始判定没有满足刷新规则,直接将事件分发下去让子View做处理,这时候达到刷新条件父View需要接管事件自己处理而不让子View处理,好了,肯定有人会说简单啊拦截掉不就行了么,最开始我也这么天真过,拦呗~~, OK问题解决了提交代码上传测试
转天测试过来:XX你这刷新有问题啊! “what? 我的代码有问题?你是认真的吗?”,”真的 你看我拉到刷新这里以后不松手,再慢慢放回去,然后继续往下移动,里面的View不能跟着滑动了”,仿佛一道惊雷打在我天灵盖上, 对啊 我拦截了事件传不下去了,这时候只要不放手里面的内容View就再也不能接收到事件,所以当再滑回头部继续滑动的时候,按道理应该是子View接管事件做自己的滑动处理,可是现在不行了!怎么办 当时这个问题也的确难住了我,拦截是中断形式的,一次拦截终生受用,最后为了赶着上线当这种情况下我根据父View拿到的滑动事件信息去手动的调ListView(当时内部是listView)的滑动方法强行滑动,效果烂不说,还要处理一堆杂事,比如放手后做根据放手时的加速度去模拟listView做惯性滑动等等。。。 事后我决定从根源上找到解决办法而不是这么low的外部辅助方法,我再一次看了事件方面的源码想从里面找到思路,可是中断拦截貌似是铁律除非你去自己写分发逻辑,这种傻逼想法就不说了。。。 当我卡在传统思维死胡同中的时候突然惊醒,我既然能外部模拟ListView滑动去处理,为什么我不能模拟事件给子View呢?为什么非要死板的认为事件中断下发了一定要用户重新按下才能传递下去,一旦想通这一点这问题就迎刃而解了,再看上面的代码当hearView处理滑动的时候我不是拦截事件而是调用了sendCancelEent()方法,我们进去看一下:

    /**     * 模拟 cancel 用于分发到 内部子View     */    private void sendCancelEvent() {        //根据当前move事件模拟一个cancel事件传递给子View,这时候作用其实就是相当于拦截        MotionEvent last = mLastMoveEvent;        MotionEvent e = MotionEvent.obtain(                last.getDownTime(),                last.getEventTime()                        + ViewConfiguration.getLongPressTimeout(),                MotionEvent.ACTION_CANCEL, last.getX(), last.getY(),                last.getMetaState());        dispatchTouchEventSupper(e);    }

接下来子View不会再接收到事件当然就不会再有响应,当发生到上诉所说的情况只需同样的思想再模拟一个down事件传递给子View,那么就可以完美的绕开中断机制实现事件分发桥接

    /**     * 模拟 down事件 用于分发到 内部子View     */    private void sendDownEvent() {        final MotionEvent last = mLastMoveEvent;        if (last == null)            return;        MotionEvent e = MotionEvent.obtain(last.getDownTime(),                last.getEventTime(), MotionEvent.ACTION_DOWN, last.getX(),                last.getY(), last.getMetaState());        dispatchTouchEventSupper(e);    }

这小玩意儿差不多就讲完了,这东西大家自己强化强化改吧改吧完全可以用到自己项目中,当然网上已经有很多成熟强大的下拉刷新库,我喜欢自己写纯属因为改起来快,想怎么弄怎么弄,碰到BUG定位也很快,也比较轻。

0 0
原创粉丝点击