1. 程式人生 > >Android學習筆記之自定義View

Android學習筆記之自定義View

一、自定義View的分類

1.1.繼承 View

這種方法主要用於實現一些不規則的效果(不方便通過佈局的組合方式來實現),比如靜態或動態地顯示一些不規則的圖形(因此需要重寫onDraw方法)。值得注意的是,繼承View的自定義View需要自己制定 wrap_content 的尺寸,並且需要自己處理padding屬性。

1.2.繼承 ViewGroup

這種方法主要用於實現自定義佈局,當某種效果看起來很像幾種View組合在一起的時候,可以採用這種方法來實現。值得注意的是,繼承ViewGroup的自定義佈局需要妥善處理measure、layout過程,同時處理好子元素的measure、layout過程。

1.3.繼承特定的 View(比如TextView)

這種方法比較常見,通常用於擴充套件某個已知View的功能。這種方法的自定義View不需要設定 wrap_content 的尺寸以及處理padding屬性。

1.4.繼承特定的 ViewGroup(比如LinearLayout)

其實第四種方法和第二種方法都是用來實現自定義佈局的,而且實現的效果也很相近。通常而言,方法2能實現的效果方法4也都能實現,兩者的主要區別在於方法2更接近於View的底層。方法4使用起來更簡單,因為不需要自己處理ViewGroup的measure、layout過程。

二、使用自定義View的注意事項

2.1.讓View支援 wrap_content 屬性

其實也就是制定自定義控制元件 wrap_content 時的預設尺寸。這是因為直接繼承View或者ViewGroup的控制元件,如果不在onMeasure中指定 wrap_content 時的預設尺寸,那麼在使用該屬性的時候會和match_parent效果一樣。具體原因可以參考我之前的博文《measure過程分析》

2.2.如果使用到了padding屬性,需要自己處理它以使屬性生效

這是因為直接繼承 View 的控制元件,如果不在 draw 方法中處理 padding,那麼 padding 屬性是無法起作用的。另外直接繼承自ViewGroup 的控制元件需要在 onMeasure 和 onLayout 中考慮 padding 和子元素的 margin 對其造成的影響,不然將導致自己的 padding 屬性和子元素的 margin 屬性失效。

2.3.儘量不要在View中使用Handler

這是因為 View 內部本身就提供了 post 系列的方法, 完全可以替代 Handler 的作用,當然除非我們很明確的要使用 Handler 來發送訊息。

2.4.View中如果有執行緒或者動畫,需要及時停止,參考View#onDetachedFromWindow

如果有執行緒或者動畫需要停止時,那麼 onDetachedFromWindow 是一個很好的時機。當包含此View的Activity退出或者當前View被remove時,View的 onDetachedFromWindow 方法會被呼叫,和此方法對應的是 onAttachedToWindow,當包含此 View 的Activity 啟動時,View的 onAttachedToWindow方法會被呼叫。同時,當View變得不可見時我們也需要停止執行緒和動畫,以免記憶體洩漏。

2.5.當自定義控制元件帶有滑動巢狀時,需要處理滑動衝突

如果自定義控制元件中存在滑動巢狀的情況,如果不加以處理,那麼就會嚴重影響View的效果。

三、繼承View的自定義控制元件

3.1.自定義View的初步實現:

public class CustomView extends View {
    private int mColor = Color.BLUE;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    public CustomView(Context context) {
        super(context);
        init();
    }

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs, 0);
        init();
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(){
        mPaint.setColor(mColor);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        int radius = Math.min(width,height)/2;
        canvas.drawCircle(width/2,height/2,radius,mPaint);
    }
}

上述程式碼設定好畫筆顏色之後,會根據View的寬高,取較小值為直徑畫一個圓。然而這樣繪製的自定義View,padding屬性以及wrap_content是不會生效的。如下:

<com.xxxx.xxxxx.customviewtest.CustomView
    android:layout_width="wrap_content"
    android:layout_height="100dp"
    android:layout_marginTop="8dp"
    android:background="#302c2c"
    android:padding="20dp"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

3.2.使自定義View支援padding屬性以及wrap_content:

不難看出,margin屬性雖然生效了,但是padding屬性沒有效果,而且wrap_content設定的效果和match_parent效果一樣。因此為了讓自定義View更加完整我們應該對之前的程式碼進行修改,如下:

public class CustomView extends View {
    private int mColor = Color.BLUE;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private int mWidth = 200;
    private int mHeight = 200;
   
    ......
    //記得要處理好padding屬性
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        int radius = Math.min(width,height)/2;
        canvas.drawCircle(width/2 + paddingLeft,height/2 + paddingTop,radius,mPaint);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,mHeight);
        } else if (widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize,mHeight);
        }
    }
}

這裡給自定義View設定了wrap_content的預設寬高,這麼一來就不會把可用空間全部佔用了。

3.3.為自定義View新增自定義屬性:

1.在values目錄下建立自定義屬性的XML檔案,比如attr.xml。檔案內容如下:

<?xml version="1.0" encoding="utf-8" ?>
<resources>
    <declare-styleable name="CustomView">
        <attr name="circle_color" format="color"/>
    </declare-styleable>

</resources>

在上面的XML中聲明瞭一個自定義屬性集合“CustomView”,在這個集合裡面可以有很多自定義屬性,這裡只定義了一個格式為“color”的屬性“circle_color”。除了顏色格式,自定義屬性還有其他格式,比如 reference 是指資源id;dimension 是指尺寸;而像string、integer、boolean是指基本資料型別,等等。

2.在View的構造方法中解析自定義屬性的值並做相應處理。

public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.CustomView);
    mColor = array.getColor(R.styleable.CustomView_circle_color,mColor);
    array.recycle();
    init();
}

首先載入自定義屬性集合CustomView,接著解析CustomView屬性集合中的circle_color屬性,它的id為R.styleable.CustomView_circle_color。後面那個引數是預設顏色值。然後別忘了讀取xml檔案呼叫的是兩個引數的那個構造器,因此要做如下修改:

public CustomView(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
    init();
}

3.最後在xml檔案中使用自定義屬性

<com.whut.ein3614.customviewtest.CustomView
    android:layout_width="wrap_content"
    android:layout_height="100dp"
    android:layout_marginStart="8dp"
    android:layout_marginLeft="8dp"
    android:layout_marginTop="8dp"
    android:layout_marginEnd="8dp"
    android:layout_marginRight="8dp"
    android:background="#302c2c"
    android:padding="20dp"
    app:circle_color="#53d45d"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

四、繼承ViewGroup的自定義佈局

4.1.自定義佈局的場景:

該自定義佈局是一個可橫向滑動的佈局,測試的時候向其內部添加了三個縱向滑動的ListView,以模擬滑動衝突的場景。除了要解決滑動衝突的問題之外,和自定義View一樣還要處理自身的padding,以及子元素的margin。下面是測試用的Activity程式碼:

public class MainActivity extends AppCompatActivity {

    private HorizontalScrollView mListContainer;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.demo_1);
        initView();
    }

    private void initView(){
        mListContainer = findViewById(R.id.container);
        LayoutInflater inflater = getLayoutInflater();
        for(int i=0;i<3;i++){
            ViewGroup layout = (ViewGroup) inflater.inflate(R.layout.content_layout,mListContainer,false);
            TextView textView  = layout.findViewById(R.id.title);
            textView.setText("page "+(i+1));
            layout.setBackgroundColor(Color.rgb(255/(i+1),255/(i+1),0));
            createList(layout);
            mListContainer.addView(layout);
        }
    }
    private void createList(ViewGroup layout){
        ListView listView = layout.findViewById(R.id.list);
        ArrayList<String> datas = new ArrayList<>();
        for(int i=0;i<50;i++){
            datas.add("name "+i);
        }
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,R.layout.content_list_item,R.id.name,datas);
        listView.setAdapter(adapter);
    }
}

4.2.先要重寫onMeasure方法,以得出該自定義佈局的尺寸

具體的處理思路為:先遍歷所有子元素並呼叫它們的measure方法,然後再結合子元素的寬高測量出自己(自定義佈局)的寬高值。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    final int childCount = getChildCount();
    int measureWidth = 0;
    int measureHeight = 0;
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    final int paddingLeft = getPaddingLeft();
    final int paddingRight = getPaddingRight();
    final int paddingTop = getPaddingTop();
    final int paddingBottom = getPaddingBottom();
    ViewGroup.LayoutParams lp = getLayoutParams();
    final View childView = getChildAt(0);
    MarginLayoutParams clp = (MarginLayoutParams) childView.getLayoutParams();
    mChildLp = clp;

    measureChildren(MeasureSpec.makeMeasureSpec(widthSpecSize - clp.leftMargin - clp.rightMargin, widthSpecMode),
            MeasureSpec.makeMeasureSpec(heightSpecSize - clp.topMargin - clp.bottomMargin, heightSpecMode));

    if (childCount == 0) {
        if (lp.width >= 0) {
            measureWidth = lp.width;
        } else if (lp.width == LayoutParams.MATCH_PARENT) {
            measureWidth = widthSpecSize;
        } else if (lp.width == LayoutParams.WRAP_CONTENT) {
            measureWidth = 0;
        }
        if (lp.height >= 0) {
            measureHeight = lp.height;
        } else if (lp.height == LayoutParams.MATCH_PARENT) {
            measureHeight = heightSpecSize;
        } else if (lp.height == LayoutParams.WRAP_CONTENT) {
            measureHeight = 0;
        }
        setMeasuredDimension(measureWidth, measureHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
        measureWidth = (childView.getMeasuredWidth() + clp.leftMargin + clp.rightMargin) * childCount + paddingLeft + paddingRight;
        measureHeight = childView.getMeasuredHeight() + clp.topMargin + clp.bottomMargin + paddingTop + paddingBottom;
        setMeasuredDimension(measureWidth, measureHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        measureWidth = (childView.getMeasuredWidth() + clp.leftMargin + clp.rightMargin) * childCount + paddingLeft + paddingRight;
        setMeasuredDimension(measureWidth, heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        measureHeight = childView.getMeasuredHeight() + clp.topMargin + clp.bottomMargin + paddingTop + paddingBottom;
        setMeasuredDimension(widthSpecSize, measureHeight);
    }
}

在上述程式碼中值得注意的是,通過measureChildren方法就可以遍歷所有子元素並呼叫它們的measure方法,但是這種方法並沒有將子元素的margin屬性考慮進去(父容器自身的padding屬性有考慮進去),因此需要加以處理。但是直接將子元素的LayoutParams 轉換為 MarginLayoutParams 是會報錯的(型別轉換異常)。因此還需要在自定義佈局中,新增如下程式碼:

// 繼承自margin,支援子檢視android:layout_margin屬性
public static class LayoutParams extends MarginLayoutParams {


    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
    }


    public LayoutParams(int width, int height) {
        super(width, height);
    }


    public LayoutParams(ViewGroup.LayoutParams source) {
        super(source);
    }


    public LayoutParams(ViewGroup.MarginLayoutParams source) {
        super(source);
    }
}

@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    return new LayoutParams(p);
}

@Override
protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}

4.3.然後重寫onLayout方法

具體的處理思路為:先遍歷所有子元素,如果這個子元素不是處於GONE這個狀態,那麼就通過layout方法將其放置在合適的位置上。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childLeft = 0;
    final int childCount = getChildCount();
    mChildrenSize = childCount;

    final int paddingLeft = getPaddingLeft();
    final int paddingTop = getPaddingTop();
    childLeft += paddingLeft;
    for (int i = 0; i < childCount; i++) {
        final View childView = getChildAt(i);
        if (childView.getVisibility() != View.GONE) {
            MarginLayoutParams clp = (MarginLayoutParams) childView.getLayoutParams();
            childLeft += clp.leftMargin;
            final int childWidth = childView.getMeasuredWidth();
            mChildWidth = childWidth;
            final int childHeight = childView.getMeasuredHeight();
            childView.layout(childLeft, paddingTop + clp.topMargin, childLeft + childWidth, paddingTop + clp.topMargin + childHeight);
            childLeft += childWidth + clp.rightMargin;
        }
    }
}

4.4.妥善處理滑動衝突

首先分析當前的滑動衝突,外層橫向滑動,內層豎向滑動。因此我這裡採用的解決辦法是根據滑動距離判斷當前滑動屬於哪一種滑動的外部攔截法。如果是橫向滑動,則父容器攔截當前事件並加以處理(呼叫onTouchEvent方法);如果是豎向滑動,則父容器不予攔截,讓事件繼續向下傳遞。程式碼如下:

public class HorizontalScrollView extends ViewGroup {
    private static final String TAG = "HorizontalScrollView";
    private int mChildIndex;
    private int mChildWidth;
    private int mChildrenSize;
    private MarginLayoutParams mChildLp;
    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;
    //記錄上次滑動座標(onInterceptTouchEvent)
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;
    //記錄上次滑動的座標
    private int mLastX = 0;
    private int mLastY = 0;

    public HorizontalScrollView(Context context) {
        super(context);
        init();
    }

    public HorizontalScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        if (mScroller == null) {
            mScroller = new Scroller(getContext());
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                if (!mScroller.isFinished()) {//如果上次滑動沒有完成
                    mScroller.abortAnimation();//優化滑動體驗
                    intercepted = true;//全交由父容器處理
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    intercepted = true;//如果是水平滑動,則父容器攔截
                } else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
            default:
                break;
        }
        Log.d(TAG, "intercepted: " + intercepted);
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                scrollBy(-deltaX, 0);//只水平滑動,注意右滑mScrollX為負,即傳參為負內容向右滑動
                break;
            case MotionEvent.ACTION_UP://鬆手後的滑動動畫處理
                int scrollX = getScrollX();//注意這裡拿到的mScrollX(scrollX),內容向右偏移的時候,mScrollX值為負
                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity();
                if (Math.abs(xVelocity) >= 50) {//快滑
                    mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
                } else {//慢滑
                    mChildIndex = (scrollX + mChildWidth/2)/mChildWidth;
                }
                mChildIndex = Math.max(0,Math.min(mChildIndex,mChildrenSize-1));//不超邊界
                int dx = mChildIndex*(mChildWidth + mChildLp.leftMargin + mChildLp.rightMargin) - scrollX ;
                smoothScrollBy(dx,0);//注意這裡的滑動:傳參為負時,內容向右滑動
                mVelocityTracker.clear();
                break;
            default:
                break;
        }
        mLastX = x;
        mLastY = y;
        return true;
    }

    ......    
    private void smoothScrollBy(int dx,int dy){
        mScroller.startScroll(getScrollX(),0,dx,0,500);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if(mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            postInvalidate();
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        mVelocityTracker.recycle();
        super.onDetachedFromWindow();
    }
}

上述程式碼中值得注意的是,為了避免在上一次水平滑動(父容器處理)過程結束前,使用者快速的進行豎直滑動,導致介面在水平方向上無法滑動到終點從而處於一種中間狀態的情況。在上一次滑動結束前,下一個點選事件序列仍然交由父容器處理:

case MotionEvent.ACTION_DOWN:
    intercepted = false;
    if (!mScroller.isFinished()) {//如果上次滑動沒有完成
        mScroller.abortAnimation();//優化滑動體驗
        intercepted = true;//全交由父容器處理
    }
    break;

五、繼承特定ViewGroup的自定義佈局(LinearLayout)

其實繼承特定的 ViewGroup 和繼承 ViewGroup 實現的自定義佈局思路都是一樣的,相比之下繼承現有的ViewGroup(比如LinearLayout)比直接繼承ViewGroup 還要簡單一些(不用再自己處理那些擾人的margin、padding)。我之所以還要再舉個例子主要是想再現另外一種滑動衝突,並加以處理。

5.1.自定義佈局的場景:

該自定義佈局是一個可縱向滑動的佈局,其內部只是簡單的放置了一個TextView(Header),然後在它下面再放一個ListView(Content)。這麼一來就是滑動衝突的第二個場景:外層縱向滑動,內層也是縱向滑動。

PS:此時的自定義佈局繼承LinearLayout,因此不用再自己處理margin、padding,也就不用再重寫onMeasure、onLayout。

5.2.妥善處理滑動衝突

這裡我同樣採用外部攔截法來處理滑動衝突。只是處理邏輯不能再依照之前的滑動方向來判定了,具體的判定邏輯如下:

  1. 當事件落在Header(TextView)上面時父容器不會攔截事件
  2. 如果屬於橫向滑動,父容器不會攔截事件
  3. 當Header施展開狀態並向上滑動時,父容器攔截該事件
  4. 當ListView滑動到頂部了並且向下滑動時,父容器攔截該事件

因此,重寫 onInterceptTouchEvent 和 onTouchEvent 如下:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted = false;
    int x = (int) ev.getX();
    int y = (int) ev.getY();

    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            mLastXIntercept = x;
            mLastYIntercept = y;
            mLastX = x;
            mLastY = y;
            intercepted = false;
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastXIntercept;
            int deltaY = y - mLastYIntercept;
            if (y <= getHeaderHeight()) {//當事件落在Header上面時,父容器不攔截該事件
                intercepted = false;
            } else if (Math.abs(deltaY) <= Math.abs(deltaX)) {//視為水平滑動時,不攔截該事件
                intercepted = false;
            } else if (mStatus == STATUS_EXPANDED && deltaY <= -mTouchSlop) {//Header是展開狀態時並且向上滑動,攔截該事件
                intercepted = true;
            } else if (mGiveUpTouchEventListener != null) {//當ListView滑動到頂部了並且向下滑動時,父容器攔截該事件
                if (mGiveUpTouchEventListener.giveUpTouchEvent(ev) && deltaY >= mTouchSlop) {
                    intercepted = true;
                }
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            intercepted = false;
            mLastXIntercept = mLastYIntercept = 0;
            break;
        }
        default:
            break;
    }
    return intercepted;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastX;
            int deltaY = y - 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;
}

其中,getHeaderHeight()方法獲取的是 mHeaderHeight 的值,它是Header在螢幕上顯示出來的高度:

public int getHeaderHeight() {
    return mHeaderHeight;
}

一開始是等於TextView的完整高度的:

@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
    super.onWindowFocusChanged(hasWindowFocus);
    if (hasWindowFocus ) {
        mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
        mHeader = findViewById(R.id.tv_title);
        mOriginalHeaderHeight = mHeader.getMeasuredHeight();
        mHeaderHeight = mOriginalHeaderHeight;
    }
}

但是隨著內容的滑動,其顯示出來的高度會發生變化:

public void setHeaderHeight(int height) {
    if (height <= 0) {//顯示在螢幕上的header高度範圍
        height = 0;
    } else if (height > mOriginalHeaderHeight) {
        height = mOriginalHeaderHeight;
    }

    if (height == 0) {
        mStatus = STATUS_COLLAPSED;
    } else {
        mStatus = STATUS_EXPANDED;
    }

    if (mHeader != null && mHeader.getLayoutParams() != null) {
        mHeader.getLayoutParams().height = height;
        mHeader.requestLayout();
        mHeaderHeight = height;
    }
}

最後值得一提的是,我這裡是通過設定監聽器來判斷ListView是否滑動到頂部的:

public interface OnGiveUpTouchEventListener {
    boolean giveUpTouchEvent(MotionEvent event);
}
public void setOnGiveUpTouchEventListener(OnGiveUpTouchEventListener l) {
    mGiveUpTouchEventListener = l;
}
@Override
public boolean giveUpTouchEvent(MotionEvent event) {
    if (listView.getFirstVisiblePosition() == 0) {
        View view = listView.getChildAt(0);
        if (view != null && view.getTop() >= 0) {
            Log.d(TAG, "it is top");
            return true;
        }
    }
    return false;
}