1. 程式人生 > >可下拉的PinnedHeaderExpandableListView的實現

可下拉的PinnedHeaderExpandableListView的實現

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow

也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!

               

轉載請註明出處:http://blog.csdn.net/singwhatiwanna/article/details/25546871(來自singwhatiwanna的csdn部落格)

前言

Android中,大家都用過ListView,ExpandableListView等,也許你還用過PinnedHeaderListView,但是如果我說PinnedHeaderExpandableListView,你聽過嗎?還有可下拉的PinnedHeaderExpandableListView呢?沒聽過也不要緊,本文就是介紹這個東西的,為了讓大家有更直觀的瞭解,先上效果圖。通過效果圖可以看出,首先它是一個ExpandableListView,但是它的頭部可以固定,其次,在它的上面還有一個頭部可以來回伸縮,恩,這就是本文要介紹的自定義view。為了提高複用性,這個效果我分成來了2個view來實現,第一個是PinnedHeaderExpandableListView來實現頭部固定的ExpandableListView,第二個view是StickyLayout,這個view具有一個可以上下滑動的頭部,最後將這2個view組合在一起,就達到了如下的效果。

PinnedHeaderExpandableListView的實現

關於ExpandableListView的使用方法請自己瞭解下,網上很多。關於這個view,它的實現方式是這樣的:

首先繼承自ExpandableListView,然後再它滾動的時候我們要監聽頂部的item是屬於哪個group的,當知道是哪個group以後,我們就在view的頂部繪製這個group,這樣就完成了頭部固定這個效果。當然過程遠沒有我描述的這個簡單,期間有一些問題需要正確處理,下面分別說明:

1.如何知道頂部的item是哪個group,這個簡單,略過;

 

2. 如何在頂部繪製group,這個我們可以重寫dispatchDraw這個方法,在這個方法裡drawChild即可,dispatchDraw是被draw方法用來繪製子元素的,和onDraw不同,onDraw是用來繪製自己的,我們要知道,view繪圖的過程是先背景再自己最後在繪製子元素;

 

3. 滑動過程中header的更新,當滑動的時候,要去判斷最上面的group是否發生改變,如果改變了就需要重新繪製group,這個很簡單。注意到有一個效果,就是當兩個group接近的時候,下面的group會把上面的header推上去,這個效果就難處理一些,推動的效果可以用layout來實現,通過layout將上面的group的位置給改變就可以了;

 

4.header的點選,要知道固定的頭部是繪製上去的,並且它也不是ExpandableListView的子元素,可以理解為我們憑空繪製的一個view,如果處理它的點選,這個貌似很難,但是可以這麼解決,當點選事件發生的時候,判斷其區域是否落在header內部,如果落在了內部將可以處理點選事件了,處理後要講事件消耗掉;

 

同時,我還提供了一個介面,OnHeaderUpdateListener,通過實現這個介面,PinnedHeaderExpandableListView就知道如何繪製和更新header了。下面看程式碼:

/**The MIT License (MIT)Copyright (c) 2014 singwhatiwannahttps://github.com/singwhatiwannahttp://blog.csdn.net/singwhatiwannaPermission is hereby granted, free of charge, to any person obtaining a copyof this software and associated documentation files (the "Software"), to dealin the Software without restriction, including without limitation the rightsto use, copy, modify, merge, publish, distribute, sublicense, and/or sellcopies of the Software, and to permit persons to whom the Software isfurnished to do so, subject to the following conditions:The above copyright notice and this permission notice shall be included in allcopies or substantial portions of the Software.THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS ORIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THEAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHERLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THESOFTWARE.*/package com.ryg.expandable.ui;import android.content.Context;import android.graphics.Canvas;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.widget.AbsListView;import android.widget.ExpandableListView;import android.widget.AbsListView.OnScrollListener;public class PinnedHeaderExpandableListView extends ExpandableListView implements OnScrollListener {    private static final String TAG = "PinnedHeaderExpandableListView";    public interface OnHeaderUpdateListener {        /**         * 採用單例模式返回同一個view物件即可         * 注意:view必須要有LayoutParams         */        public View getPinnedHeader();        public void updatePinnedHeader(int firstVisibleGroupPos);    }    private View mHeaderView;    private int mHeaderWidth;    private int mHeaderHeight;    private OnScrollListener mScrollListener;    private OnHeaderUpdateListener mHeaderUpdateListener;    private boolean mActionDownHappened = false;    public PinnedHeaderExpandableListView(Context context) {        super(context);        initView();    }    public PinnedHeaderExpandableListView(Context context, AttributeSet attrs) {        super(context, attrs);        initView();    }    public PinnedHeaderExpandableListView(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);        initView();    }    private void initView() {        setFadingEdgeLength(0);        setOnScrollListener(this);    }    @Override    public void setOnScrollListener(OnScrollListener l) {        if (l != this) {            mScrollListener = l;        }        super.setOnScrollListener(this);    }        public void setOnHeaderUpdateListener(OnHeaderUpdateListener listener) {        mHeaderUpdateListener = listener;        if (listener == null) {            return;        }        mHeaderView = listener.getPinnedHeader();        int firstVisiblePos = getFirstVisiblePosition();        int firstVisibleGroupPos = getPackedPositionGroup(getExpandableListPosition(firstVisiblePos));        listener.updatePinnedHeader(firstVisibleGroupPos);        requestLayout();        postInvalidate();    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        if (mHeaderView == null) {            return;        }        measureChild(mHeaderView, widthMeasureSpec, heightMeasureSpec);        mHeaderWidth = mHeaderView.getMeasuredWidth();        mHeaderHeight = mHeaderView.getMeasuredHeight();    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        super.onLayout(changed, l, t, r, b);        if (mHeaderView == null) {            return;        }        mHeaderView.layout(0, 0, mHeaderWidth, mHeaderHeight);    }    @Override    protected void dispatchDraw(Canvas canvas) {        super.dispatchDraw(canvas);        if (mHeaderView != null) {            drawChild(canvas, mHeaderView, getDrawingTime());        }    }    @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        int x = (int) ev.getX();        int y = (int) ev.getY();        Log.d(TAG, "dispatchTouchEvent");        int pos = pointToPosition(x, y);        if (y >= mHeaderView.getTop() && y <= mHeaderView.getBottom()) {            if (ev.getAction() == MotionEvent.ACTION_DOWN) {                mActionDownHappened = true;            } else if (ev.getAction() == MotionEvent.ACTION_UP) {                int groupPosition = getPackedPositionGroup(getExpandableListPosition(pos));                if (groupPosition != INVALID_POSITION && mActionDownHappened) {                    if (isGroupExpanded(groupPosition)) {                        collapseGroup(groupPosition);                    } else {                        expandGroup(groupPosition);                    }                    mActionDownHappened = false;                }                            }            return true;        }        return super.dispatchTouchEvent(ev);    }    protected void refreshHeader() {        if (mHeaderView == null) {            return;        }        int firstVisiblePos = getFirstVisiblePosition();        int pos = firstVisiblePos + 1;        int firstVisibleGroupPos = getPackedPositionGroup(getExpandableListPosition(firstVisiblePos));        int group = getPackedPositionGroup(getExpandableListPosition(pos));        if (group == firstVisibleGroupPos + 1) {            View view = getChildAt(1);            if (view.getTop() <= mHeaderHeight) {                int delta = mHeaderHeight - view.getTop();                mHeaderView.layout(0, -delta, mHeaderWidth, mHeaderHeight - delta);            }        } else {            mHeaderView.layout(0, 0, mHeaderWidth, mHeaderHeight);        }        if (mHeaderUpdateListener != null) {            mHeaderUpdateListener.updatePinnedHeader(firstVisibleGroupPos);        }    }    @Override    public void onScrollStateChanged(AbsListView view, int scrollState) {        if (mHeaderView != null && scrollState == SCROLL_STATE_IDLE) {            int firstVisiblePos = getFirstVisiblePosition();            if (firstVisiblePos == 0) {                mHeaderView.layout(0, 0, mHeaderWidth, mHeaderHeight);            }        }        if (mScrollListener != null) {            mScrollListener.onScrollStateChanged(view, scrollState);        }    }    @Override    public void onScroll(AbsListView view, int firstVisibleItem,            int visibleItemCount, int totalItemCount) {        if (totalItemCount > 0) {            refreshHeader();        }        if (mScrollListener != null) {            mScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);        }    }}

下拉效果的實現

現在介紹第二個view,即StickyLayout,字面意思是黏性的layout,這個view內部分為2部分,header和content,並且header可以來回收縮。至於如何讓header上下收縮,有幾個看似可行的方案,我們分析下:

1.通過scrollTo/scrollBy來實現view的滾動,由於這兩個api是對view內容的滾動,不管怎麼滾動,內容都不會覆蓋到別的view上去,除非你用了FrameLayout、RelativeLayout且經過精心佈局,否則很難實現將內容滾動到別的view上面,即便如此,如果將header展開和收縮也是一個很大的問題,除非你動態地去調整header的佈局,通過分析,這個方法不可行;

 

2. 通過動畫來實現view的平移,從效果上來說,這個可行的,使用平移和縮放動畫並結合手勢的監聽,可以實現這個效果,但是動畫有一個問題,就是點選事件的處理,我們知道view動畫,即使view區域發生了改變,但是事件點選區域仍然不變,而屬性動畫在3.0以下系統上根本不支援,就算採用相容包,但是屬性動畫在3.0以下系統的點選事件區域仍然不會隨著動畫而改變,這更加證實了一個結論:動畫是對view的顯示發生作用,而不是view這個物件,也即是說動畫並不影響view的區域(4個頂點)。說了這麼多,好像還挺晦澀的,直白來說,採用動畫來實現的問題是:在3.0以下系統,雖然view已經看起來跑到新位置了,但是你在新位置點選是不會觸發點選事件的,而老位置還是可以觸發點選事件,這就意味著,content移動後,content無法點選了,基於此,動畫不可行;

 

3.第三種方案,也就是本文所採用的方案:通過手勢監聽結合header高度的改變來實現整個動畫效果,具體點就是,當手指滑動的時候,動態去調整header的高度並重繪,這個時候由於header的高度發生了改變,所以content中的內容就會擠上去,就實現了本文中的效果了;

有了這個StickyLayout,想實現類似的效果,這要把可以收縮的內容放到header裡,其他內容放到content裡即可。下面看程式碼:

/**The MIT License (MIT)Copyright (c) 2014 singwhatiwannahttps://github.com/singwhatiwannahttp://blog.csdn.net/singwhatiwannaPermission is hereby granted, free of charge, to any person obtaining a copyof this software and associated documentation files (the "Software"), to dealin the Software without restriction, including without limitation the rightsto use, copy, modify, merge, publish, distribute, sublicense, and/or sellcopies of the Software, and to permit persons to whom the Software isfurnished to do so, subject to the following conditions:The above copyright notice and this permission notice shall be included in allcopies or substantial portions of the Software.THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS ORIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THEAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHERLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THESOFTWARE.*/package com.ryg.expandable.ui;import java.util.NoSuchElementException;import android.annotation.TargetApi;import android.content.Context;import android.os.Build;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.view.ViewConfiguration;import android.widget.LinearLayout;public class StickyLayout extends LinearLayout {    private static final String TAG = "StickyLayout";    public interface OnGiveUpTouchEventListener {        public boolean giveUpTouchEvent(MotionEvent event);    }    private View mHeader;    private View mContent;    private OnGiveUpTouchEventListener mGiveUpTouchEventListener;    // header的高度  單位:px    private int mOriginalHeaderHeight;    private int mHeaderHeight;    private int mStatus = STATUS_EXPANDED;    public static final int STATUS_EXPANDED = 1;    public static final int STATUS_COLLAPSED = 2;    private int mTouchSlop;    // 分別記錄上次滑動的座標    private int mLastX = 0;    private int mLastY = 0;    // 分別記錄上次滑動的座標(onInterceptTouchEvent)    private int mLastXIntercept = 0;    private int mLastYIntercept = 0;    // 用來控制滑動角度,僅當角度a滿足如下條件才進行滑動:tan a = deltaX / deltaY > 2    private static final int TAN = 2;    public StickyLayout(Context context) {        super(context);    }    public StickyLayout(Context context, AttributeSet attrs) {        super(context, attrs);    }    @TargetApi(Build.VERSION_CODES.HONEYCOMB)    public StickyLayout(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);    }    @Override    public void onWindowFocusChanged(boolean hasWindowFocus) {        super.onWindowFocusChanged(hasWindowFocus);        if (hasWindowFocus && (mHeader == null || mContent == null)) {            initData();        }    }    private void initData() {        int headerId= getResources().getIdentifier("header", "id", getContext().getPackageName());        int contentId = getResources().getIdentifier("content", "id", getContext().getPackageName());        if (headerId != 0 && contentId != 0) {            mHeader = findViewById(headerId);            mContent = findViewById(contentId);            mOriginalHeaderHeight = mHeader.getMeasuredHeight();            mHeaderHeight = mOriginalHeaderHeight;            mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();            Log.d(TAG, "mTouchSlop = " + mTouchSlop);        } else {            throw new NoSuchElementException("Did your view with \"header\" or \"content\" exist?");        }    }    public void setOnGiveUpTouchEventListener(OnGiveUpTouchEventListener l) {        mGiveUpTouchEventListener = l;    }    @Override    public boolean onInterceptTouchEvent(MotionEvent event) {        int intercepted = 0;        int x = (int) event.getX();        int y = (int) event.getY();        switch (event.getAction()) {        case MotionEvent.ACTION_DOWN: {            mLastXIntercept = x;            mLastYIntercept = y;            mLastX = x;            mLastY = y;            intercepted = 0;            break;        }        case MotionEvent.ACTION_MOVE: {            int deltaX = x - mLastXIntercept;            int deltaY = y - mLastYIntercept;            if (mStatus == STATUS_EXPANDED && deltaY <= -mTouchSlop) {                intercepted = 1;            } else if (mGiveUpTouchEventListener != null) {                if (mGiveUpTouchEventListener.giveUpTouchEvent(event) && deltaY >= mTouchSlop) {                    intercepted = 1;                }            }            break;        }        case MotionEvent.ACTION_UP: {            intercepted = 0;            mLastXIntercept = mLastYIntercept = 0;            break;        }        default:            break;        }        Log.d(TAG, "intercepted=" + intercepted);        return intercepted != 0;    }    @Override    public boolean onTouchEvent(MotionEvent event) {        int x = (int) event.getX();        int y = (int) event.getY();        Log.d(TAG, "x=" + x + "  y=" + y + "  mlastY=" + mLastY);        switch (event.getAction()) {        case MotionEvent.ACTION_DOWN: {            break;        }        case MotionEvent.ACTION_MOVE: {            int deltaX = x - mLastX;            int deltaY = y - mLastY;            Log.d(TAG, "mHeaderHeight=" + mHeaderHeight + "  deltaY=" + deltaY + "  mlastY=" + mLastY);            mHeaderHeight += deltaY;            setHeaderHeight(mHeaderHeight);            break;        }        case MotionEvent.ACTION_UP: {            // 這裡做了下判斷,當鬆開手的時候,會自動向兩邊滑動,具體向哪邊滑,要看當前所處的位置            int destHeight = 0;            if (mHeaderHeight <= mOriginalHeaderHeight * 0.5) {                destHeight = 0;                mStatus = STATUS_COLLAPSED;            } else {                destHeight = mOriginalHeaderHeight;                mStatus = STATUS_EXPANDED;            }            // 慢慢滑向終點            this.smoothSetHeaderHeight(mHeaderHeight, destHeight, 500);            break;        }        default:            break;        }        mLastX = x;        mLastY = y;        return true;    }        public void smoothSetHeaderHeight(final int from, final int to, long duration) {        final int frameCount = (int) (duration / 1000f * 30) + 1;        final float partation = (to - from) / (float) frameCount;        new Thread("Thread#smoothSetHeaderHeight") {            @Override            public void run() {                for (int i = 0; i < frameCount; i++) {                    final int height;                    if (i == frameCount - 1) {                        height = to;                    } else {                        height = (int) (from + partation * i);                    }                    post(new Runnable() {                        public void run() {                            setHeaderHeight(height);                        }                    });                    try {                        sleep(10);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            };        }.start();    }    private void setHeaderHeight(int height) {        Log.d(TAG, "setHeaderHeight height=" + height);        if (height < 0) {            height = 0;        } else if (height > mOriginalHeaderHeight) {            height = mOriginalHeaderHeight;        }        if (mHeaderHeight != height || true) {            mHeaderHeight = height;            mHeader.getLayoutParams().height = mHeaderHeight;            mHeader.requestLayout();        }    }}

關於這個view還需要說明的是滑動衝突,如果content裡是個listview,由於兩者都能豎向滑動,這就會有衝突,如何解決滑動衝突一直是一個難點,我的解決思路是這樣的:首先StickyLayout預設不攔截事件,如果子元素不處理事件,它就會上下滑動,如果子元素處理了事件,它就不會滑動,所以在最外層我們需要知道子元素何時處理事件、何時不處理事件,為了解決這個問題,提供了一個介面OnGiveUpTouchEventListener,當子元素不處理事件的時候,StickyLayout就可以處理滑動事件,具體請參看程式碼中的onInterceptTouchEvent和onTouchEvent。下面看一下activity對這2個介面的實現。

Activity的實現

由於Activity中大部分程式碼都是圍繞ExpandableListAdapter,是比較普通的程式碼,這裡要介紹的是activity對上述2個view中介面的實現,分別為PinnedHeaderExpandableListView中如何繪製和更新固定的頭部以及StickyLayout中content何時放棄事件處理。

    @Override    public View getPinnedHeader() {        if (mHeaderView == null) {            mHeaderView = (ViewGroup) getLayoutInflater().inflate(R.layout.group, null);            mHeaderView.setLayoutParams(new LayoutParams(                    LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));        }        return mHeaderView;    }    @Override    public void updatePinnedHeader(int firstVisibleGroupPos) {        Group firstVisibleGroup = (Group) adapter.getGroup(firstVisibleGroupPos);        TextView textView = (TextView) getPinnedHeader().findViewById(R.id.group);        textView.setText(firstVisibleGroup.getTitle());    }    @Override    public boolean giveUpTouchEvent(MotionEvent event) {        if (expandableListView.getFirstVisiblePosition() == 0) {            View view = expandableListView.getChildAt(0);            if (view != null && view.getTop() >= 0) {                return true;            }        }        return false;    }

總結

demo效果上還是不錯的,在4.x和2.x上都經過測試,完美執行,市面上不少android應用有類似的效果,歡迎大家fork程式碼,歡迎大家交流。

程式碼地址

https://github.com/singwhatiwanna/PinnedHeaderExpandableListView

需要注意的是:該專案採用MIT共享協議釋出,意味著如果你要使用或修改它,必須在原始碼中保留頭部的版權宣告,這個要求夠不夠低啊,哈哈!

           

給我老師的人工智慧教程打call!http://blog.csdn.net/jiangjunshow

這裡寫圖片描述