可滑动Header控件的实现

本文介绍了一种解决Android中内外层视图滑动冲突的方法,通过自定义SlideHeaderView控件来实现平滑的滑动效果,并保持良好的用户体验。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在项目开发中经常遇到这样的需求:在一个Activity中,上半部分是一个高度比较大(屏幕高度的1/3或者1/2)的Header,下半部分是ListView或者是ViewPager(里面依然包含ListView)。如果Header不能向上活动,则在浏览ListView中的内容时,只能在很小的高度里面滑动,不能发挥大屏幕的优势。如果在滑动过程中,使Header也滑动则会造成外层ViewGroup与内层ListView滑动冲突。

本文从滑动冲突产生的原因、解决方案以及代码实现过程三个方面,介绍了可滑动Header控件的实现过程。
1.滑动冲突产生的原因
滑动冲突的原因很简单,就是一个原因:在界面中只要内外两层同时可以滑动,就会产生滑动冲突。常见的滑动冲突场景有两种:
a.外部View滑动方向与内部View滑动方向一致;
b.外部View滑动方向与内部View滑动方向不一致。
Android系统提供的可滑动控件有的已经解决了滑动冲突问题,例如:ViewPager;而大部分可滑动控件并没有处理滑动冲突,例如:ScrollView、ListView。
2.解决方案
要想解决滑动冲突,首先要对Android的事件传递机制有一个深入的理解,本文的重点是讲述解决滑动冲突,对Android的事件传递机制就不展开了。Android的MotionEvent是从外层View向内层View传递的,一旦MotionEvent被拦截,MotionEvent就交给拦截它的View处理。针对滑动冲突问题,外层滑动View在onInterceptTouchEvent()方法中根据业务逻辑的需要判断是否拦截MotionEvent,在onTouchEvent()方法中实现滑动效果;在滑动的过程中,使内外层View只有一个能拦截MotionEvent,这样子就解决了View的滑动冲突问题。

3.实现过程
外层滑动View的实现`

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.LinearLayout;

import java.util.NoSuchElementException;


public class SlideHeaderView extends LinearLayout{

    private static final String TAG = "SlideHeaderView";
    private static final boolean DEBUG = true;

    /**
     * 滑动的风格类型,非覆盖
     */
    private StyleType mStyleType = StyleType.SlideOut;

    /**
     * 覆盖式、非覆盖式枚举
     */
    public enum StyleType {
        SlideOut, SlideIn // SlideOut:整个View滑动,整个View的位置发生改变  SlideIn:整个View的位置没有改变,改变View内HeaderView的高度
    }

    public interface OnInterceptScrollListener {
        boolean isInterceptScrollUpward(MotionEvent event);
        boolean isInterceptScrollDownward(MotionEvent event);
    }

    public interface OnChangeHeaderStateListener {
        void onExpandHeader();
        void onCollapseHeader();
    }

    private View mHeader;
    private View mContent;
    private OnInterceptScrollListener mInterceptScrollListener;

    private OnChangeHeaderStateListener mChangeHeaderStateListener;

    // header的高度  单位:px
    private int mOriginalHeaderHeight = -1;
    private int mHeaderHeight;
    private int minHeaderHeight;

    public int mStatus = STATUS_EXPANDED;
    public static final int STATUS_EXPANDED = 1;
    public static final int STATUS_COLLAPSED = 2;

    public int mAutoScrollOrientation;
    private final int mSlideDownward = 1; //向下滑动
    private final int mSlideUpward = 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;

    private boolean mIsSticky = true;
    private boolean mInitDataSucceed = false;
    private boolean mDisallowInterceptTouchEventOnHeader = true;

    private MarginLayoutParams mTopMarginLayoutParams;
    private int mTopMargin;
    private int maxTopMargin = -1;

    private int mAnimationSpeed = 20;
    private int mAnimationInterval = 10;

    public SlideHeaderView(Context context) {
        super(context);
    }

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

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public SlideHeaderView(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("sticky_header", "id", getContext().getPackageName());
        int contentId = getResources().getIdentifier("sticky_content", "id", getContext().getPackageName());
        if (headerId != 0 && contentId != 0) {
            mHeader = findViewById(headerId);
            mContent = findViewById(contentId);

            if (mOriginalHeaderHeight < 0) {
                mOriginalHeaderHeight = mHeader.getMeasuredHeight();
            }
            mHeaderHeight = mOriginalHeaderHeight;
            if (maxTopMargin < 0) {
                maxTopMargin = mOriginalHeaderHeight;
            }
            mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
            if (mHeaderHeight > 0) {
                mInitDataSucceed = true;
            }
            if (DEBUG) {
                Log.d(TAG, "mTouchSlop = " + mTouchSlop + "mHeaderHeight = " + mHeaderHeight);
            }
        } else {
            throw new NoSuchElementException("Did your view with id \"sticky_header\" or \"sticky_content\" exists?");
        }
    }

    public void setOnInterceptScrollListener(OnInterceptScrollListener l) {
        mInterceptScrollListener = l;
    }

    public void setOnChangeHeaderStateListener(OnChangeHeaderStateListener l) {
        mChangeHeaderStateListener = l;
    }

    public void setAnimationSpeed(int speed) {
        mAnimationSpeed = speed;
    }

    public void setAnimationInterval(int interval) {
        mAnimationInterval = interval;
    }

    public void setMaxTopMargin(int margin) {
        maxTopMargin = margin;
    }

    public void setMinHeaderHeight(int height) {
        minHeaderHeight = height;
    }

    public void setStyleType (StyleType type) {
        mStyleType = type;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        int intercepted = 0;
        int x = getX(event);
        int y = getY(event);
        Log.d(TAG, "onInterceptTouchEvent-->y = " + y);

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                mLastXIntercept = x;
                mLastYIntercept = y;
                mLastX = x;
                mLastY = y;
                Log.d(TAG, "onInterceptTouchEvent-->mLastY = " + mLastY);
                intercepted = 0;
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;

                if (Math.abs(deltaY) <= Math.abs(deltaX)) {
                    intercepted = 0;
                } else if (deltaY <= -mTouchSlop) {
                    if (mInterceptScrollListener != null && mInterceptScrollListener.isInterceptScrollUpward(event)) {
                        intercepted = 1;
                    }
                } else if (deltaY >= mTouchSlop) {
                    if (mInterceptScrollListener != null && mInterceptScrollListener.isInterceptScrollDownward(event)) {
                        intercepted = 1;
                    }
                }

//                Log.d(TAG, "onInterceptTouchEvent-->ACTION_MOVE-->intercepted = " + intercepted);
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = 0;
                mLastXIntercept = mLastYIntercept = 0;
                break;
            }
            default:
                break;
        }

        if (DEBUG) {
            Log.d(TAG, "intercepted=" + intercepted);
        }
        return intercepted != 0 && mIsSticky;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!mIsSticky) {
            return true;
        }
        int x = getX(event);
        int y = getY(event);
        Log.d(TAG, "onTouchEvent-->y = " + y);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                mTopMargin = ((MarginLayoutParams) getLayoutParams()).topMargin;
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                Log.d(TAG, "onTouchEvent-->deltaY = " + deltaY);

                if (DEBUG) {
//                    Log.d(TAG, "mHeaderHeight=" + mHeaderHeight + "  deltaY=" + deltaY + "  mlastY=" + mLastY);
                }

                if (mStyleType == StyleType.SlideIn) {
                    mHeaderHeight += deltaY;
                    setHeaderHeight(mHeaderHeight);
                } else if (mStyleType == StyleType.SlideOut) {
                    mTopMargin += deltaY;
                    setTopMargin();
                    Log.d(TAG, "onTouchEvent-->ACTION_MOVE-->mTopMargin = " + mTopMargin);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                // 这里做了下判断,当松开手的时候,会自动向两边滑动,具体向哪边滑,要看当前所处的位置
                if (mStyleType == StyleType.SlideIn) {
                    if (mHeaderHeight <= mOriginalHeaderHeight * 0.5) {
//                        mStatus = STATUS_COLLAPSED;
                        mAutoScrollOrientation = mSlideUpward;//向上滑动
                    } else {
//                        mStatus = STATUS_EXPANDED;
                        mAutoScrollOrientation = mSlideDownward;//向下滑动
                    }

                    post(new AutoSlidingRunnable());
                } else if (mStyleType == StyleType.SlideOut) {
                    if (Math.abs(mTopMargin) <= mOriginalHeaderHeight * 0.5) {
//                        mStatus = STATUS_EXPANDED;
                        mAutoScrollOrientation = mSlideDownward;//向下滑动
                    } else {
//                        mStatus = STATUS_COLLAPSED;
                        mAutoScrollOrientation = mSlideUpward;//向上滑动
                    }

                    post(new AutoSlidingRunnable());
                }

                break;
            }
            default:
                break;
        }
        mLastX = x;
        mLastY = y;
        return true;
    }

    private int getX(MotionEvent event) {
        int x = 0;
        if (mStyleType == StyleType.SlideIn) {
            x = (int) event.getX();
        } else if (mStyleType == StyleType.SlideOut) {
            x = (int) event.getRawX();
        }
        return x;
    }

    private int getY(MotionEvent event) {
        int y = 0;
        if (mStyleType == StyleType.SlideIn) {
            y = (int) event.getY();
        } else if (mStyleType == StyleType.SlideOut) {
            y = (int) event.getRawY();
        }
        return y;
    }

    public void setOriginalHeaderHeight(int originalHeaderHeight) {
        mOriginalHeaderHeight = originalHeaderHeight;
    }

    public void setHeaderHeight(int height, boolean modifyOriginalHeaderHeight) {
        if (modifyOriginalHeaderHeight) {
            setOriginalHeaderHeight(height);
        }
        setHeaderHeight(height);
    }

    public void setHeaderHeight(int height) {
        if (!mInitDataSucceed) {
            initData();
        }

        if (DEBUG) {
//            Log.d(TAG, "setHeaderHeight height=" + height);
        }
        if (height <= minHeaderHeight) {
            height = minHeaderHeight;
        } else if (height > mOriginalHeaderHeight) {
            height = mOriginalHeaderHeight;
        }

        if (height == minHeaderHeight) {
            mStatus = STATUS_COLLAPSED;
            if (mChangeHeaderStateListener != null) {
                mChangeHeaderStateListener.onCollapseHeader();
            }
        } else {
            mStatus = STATUS_EXPANDED;
            if (mChangeHeaderStateListener != null) {
                mChangeHeaderStateListener.onExpandHeader();
            }
        }

        if (mHeader != null && mHeader.getLayoutParams() != null) {
            mHeader.getLayoutParams().height = height;
            mHeader.requestLayout();
            mHeaderHeight = height;
        } else {
            if (DEBUG) {
                Log.e(TAG, "null LayoutParams when setHeaderHeight");
            }
        }
    }

    public void setTopMargin() {
        if (mTopMargin < -maxTopMargin) {
            mTopMargin = -maxTopMargin;
        } else if (mTopMargin > 0) {
            mTopMargin = 0;
        }

        if (mTopMargin == -maxTopMargin) {
            mStatus = STATUS_COLLAPSED;
            if (mChangeHeaderStateListener != null) {
                mChangeHeaderStateListener.onCollapseHeader();
            }
        } else {
            mStatus = STATUS_EXPANDED;
            if (mChangeHeaderStateListener != null) {
                mChangeHeaderStateListener.onExpandHeader();
            }
        }

//        Log.d(TAG, "setTopMargin: topMargin = " + mTopMargin);
        mTopMarginLayoutParams = (MarginLayoutParams) getLayoutParams();
        mTopMarginLayoutParams.topMargin = mTopMargin;
        setLayoutParams(mTopMarginLayoutParams);
    }

    public int getHeaderHeight() {
        return mHeaderHeight;
    }

    public void setSticky(boolean isSticky) {
        mIsSticky = isSticky;
    }

    public void requestDisallowInterceptTouchEventOnHeader(boolean disallowIntercept) {
        mDisallowInterceptTouchEventOnHeader = disallowIntercept;
    }

    public int getHeaderStatus() {
        return mStatus;
    }

    private class AutoSlidingRunnable implements Runnable {

        public AutoSlidingRunnable() {}


        @Override
        public void run() {
            switch (mStyleType) {
                case SlideOut:
                    if (mAutoScrollOrientation == mSlideDownward) {
                        mTopMargin = mTopMargin + mAnimationSpeed;
                        Log.d(TAG, "向下滑动: topMargin = " + mTopMargin);
                        if (mTopMargin >= 0 ) {
                            mTopMargin = 0;
                            mHandler.sendEmptyMessage(100);

                            mStatus = STATUS_EXPANDED;
                            if (mChangeHeaderStateListener != null) {
                                mChangeHeaderStateListener.onExpandHeader();
                            }
                            return;
                        }
                        mHandler.sendEmptyMessage(100);
                    } else if (mAutoScrollOrientation == mSlideUpward) {
                        mTopMargin = mTopMargin - mAnimationSpeed;
                        Log.d(TAG, "向上滑动: topMargin = " + mTopMargin);
                        if (mTopMargin <= -maxTopMargin ) {
                            mTopMargin = -maxTopMargin;
                            mHandler.sendEmptyMessage(100);

                            mStatus = STATUS_COLLAPSED;
                            if (mChangeHeaderStateListener != null) {
                                mChangeHeaderStateListener.onCollapseHeader();
                            }
                            return;
                        }
                        mHandler.sendEmptyMessage(100);
                    }

                    postDelayed(this, mAnimationInterval);

                    break;
                case SlideIn:
                    if (mAutoScrollOrientation == mSlideDownward) {
                        mHeaderHeight = mHeaderHeight + mAnimationSpeed;
                        Log.d(TAG, "向下滑动: mHeaderHeight = " + mHeaderHeight);
                        if (mHeaderHeight >= mOriginalHeaderHeight) {
                            mHeaderHeight = mOriginalHeaderHeight;
                            mHandler.sendEmptyMessage(101);

                            mStatus = STATUS_EXPANDED;
                            if (mChangeHeaderStateListener != null) {
                                mChangeHeaderStateListener.onExpandHeader();
                            }
                            return;
                        }
                        mHandler.sendEmptyMessage(101);
                    } else if (mAutoScrollOrientation == mSlideUpward) {
                        mHeaderHeight = mHeaderHeight - mAnimationSpeed;
                        Log.d(TAG, "向上滑动: mHeaderHeight = " + mHeaderHeight);
                        if (mHeaderHeight <= minHeaderHeight) {
                            mHeaderHeight = minHeaderHeight;
                            mHandler.sendEmptyMessage(101);

                            mStatus = STATUS_COLLAPSED;
                            if (mChangeHeaderStateListener != null) {
                                mChangeHeaderStateListener.onCollapseHeader();
                            }
                            return;
                        }
                        mHandler.sendEmptyMessage(101);
                    }

                    postDelayed(this, mAnimationInterval);

                    break;
                default:
                    break;
            }

        }
    }

    /**
     * Handler处理者
     */
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 100:
                    mTopMarginLayoutParams = (MarginLayoutParams) getLayoutParams();
                    mTopMarginLayoutParams.topMargin = mTopMargin;
                    setLayoutParams(mTopMarginLayoutParams);
                    break;
                case 101:
                    if (mHeader != null && mHeader.getLayoutParams() != null) {
                        mHeader.getLayoutParams().height = mHeaderHeight;
                        mHeader.requestLayout();
                    } else {
                        if (DEBUG) {
                            Log.e(TAG, "null LayoutParams when setHeaderHeight");
                        }
                    }
                    break;
                default:
                    break;
            }
        }
    };

    public void expandHeader() {
        mAutoScrollOrientation = mSlideDownward;
        post(new AutoSlidingRunnable());
    }

}

使用这种方式解决滑动冲突问题,只需重新外层滑动View,内层滑动View不用做任何处理。

在使用SlideHeaderView的Activity中需要实现SlideHeaderView.OnInterceptScrollListener接口

@Override
    public boolean isInterceptScrollUpward(MotionEvent event) {
        if (isNeedIntercept) {
            return true;
        }
        return false;
    }

    @Override
    public boolean isInterceptScrollDownward(MotionEvent event) {
        if (isNeedIntercept) {
            return true;
        }
        return false;
    }

xml文件的实现

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FFEEEFEF">

    <SlideHeaderView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <LinearLayout
            android:id="@+id/sticky_header"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <ImageView
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>

        </LinearLayout>

        <LinearLayout
            android:id="@+id/sticky_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <android.support.v4.view.ViewPager
                android:id="@+id/viewpager"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="#FFEEEFEF"></android.support.v4.view.ViewPager>

        </LinearLayout>

    </SlideHeaderView>

</RelativeLayout>

过一段时间,写一个demo上传上来。

reference:Android开发艺术探索

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值