可下拉的PinnedHeaderExpandableListView的實現

轉載請註明出處:http://blog.csdn.net/singwhatiwanna/article/details/25546871(來自singwhatiwanna的csdn博客)java

前言

Android中,你們都用過ListView,ExpandableListView等,也許你還用過PinnedHeaderListView,可是若是我說PinnedHeaderExpandableListView,你聽過嗎?還有可下拉的PinnedHeaderExpandableListView呢?沒聽過也沒關係,本文就是介紹這個東西的,爲了讓你們有更直觀的瞭解,先上效果圖。經過效果圖能夠看出,首先它是一個ExpandableListView,可是它的頭部能夠固定,其次,在它的上面還有一個頭部能夠來回伸縮,恩,這就是本文要介紹的自定義view。爲了提升複用性,這個效果我分紅來了2個view來實現,第一個是PinnedHeaderExpandableListView來實現頭部固定的ExpandableListView,第二個view是StickyLayout,這個view具備一個能夠上下滑動的頭部,最後將這2個view組合在一塊兒,就達到了以下的效果。android

PinnedHeaderExpandableListView的實現

關於ExpandableListView的使用方法請本身瞭解下,網上不少。關於這個view,它的實現方式是這樣的:git

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

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

 

2. 如何在頂部繪製group,這個咱們能夠重寫dispatchDraw這個方法,在這個方法裏drawChild便可,dispatchDraw是被draw方法用來繪製子元素的,和onDraw不一樣,onDraw是用來繪製本身的,咱們要知道,view繪圖的過程是先背景再本身最後在繪製子元素;api

 

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

 

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

 

同時,我還提供了一個接口,OnHeaderUpdateListener,經過實現這個接口,PinnedHeaderExpandableListView就知道如何繪製和更新header了。下面看代碼:佈局

/**
The MIT License (MIT)

Copyright (c) 2014 singwhatiwanna
https://github.com/singwhatiwanna
http://blog.csdn.net/singwhatiwanna

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, 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 THE
SOFTWARE.
*/

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上下收縮,有幾個看似可行的方案,咱們分析下:post

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 singwhatiwanna
https://github.com/singwhatiwanna
http://blog.csdn.net/singwhatiwanna

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, 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 THE
SOFTWARE.
*/

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共享協議發佈,意味着若是你要使用或修改它,必須在源代碼中保留頭部的版權聲明,這個要求夠不夠低啊,哈哈!

相關文章
相關標籤/搜索