【Android自定义控件】不用ScrollView实现上下两屏滑动

2022-12-13 18:54:27 浏览数 (1)

  • 前言
  • 思路
  • 代码
  • 使用方法
  • 补充

前言

近期项目原因需要一个上下两屏滑动的效果。可以想象成viewpager左右滑动变成上下滑动。本来想用Srcollview实现,但是由于一些原因,比如按键冲突,和listview布局冲突等等,最后决定自己写一个自定义控件。

思路

由于之前实现过SlidingMenu,所以就考虑参考那个模式,左右滑动变成上下滑动就可以。 其实就是两个大小一样的布局,一个显示在屏幕上,另一个隐藏在屏幕外,等到滑动的时候就显示出来。

代码

我这边继承的是ViewGroup 。再重写onLayout方法,放置一个布局占满屏幕,另一个在屏幕外。

代码语言:javascript复制
public class MySlidingMenu extends ViewGroup {

    private static final String TAG = MySlidingMenu.class.getName();

    private enum Scroll_State {
        Scroll_to_Open, Scroll_to_Close;
    }

    private Scroll_State state;
    private int mMostRecentY;
    private int downY;
    private boolean isOpen = false;

    private View menu;
    private View mainView;

    private Scroller mScroller;

    private OnSlidingMenuListener onSlidingMenuListener;

    public MySlidingMenu(Context context, View main, View menu) {
        super(context);
        setMainView(main);
        setMenu(menu);
        init(context);
    }

    private void init(Context context) {
        mScroller = new Scroller(context);
    }

    @Override
    protected void onLayout(boolean arg0, int l, int t, int r, int b) {
        mainView.layout(l,t,r,b);
        //设置坐标再屏幕下方
        menu.layout(l, menu.getMeasuredHeight(), r, menu.getMeasuredHeight()*2);
    }

    public void setMainView(View view) {
        mainView = view;
        addView(mainView);
    }

    public void setMenu(View view) {
        menu = view;
        addView(menu);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mainView.measure(widthMeasureSpec, heightMeasureSpec);
        menu.measure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mMostRecentY = (int) event.getY();
                downY = (int) event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                int moveY = (int) event.getY();
                int deltaY = mMostRecentY - moveY;
                // 如果在菜单打开时向上滑动及菜单关闭时向下滑动不会触发Scroll事件
                if ((!isOpen && (downY - moveY) > 0)
                        || (isOpen && (downY - moveY) < 0)) {
                    scrollBy(0, deltaY);
                }
                mMostRecentY = moveY;
                break;
            case MotionEvent.ACTION_UP:
                int upY = (int) event.getY();
                int dy = upY - downY;
                if (!isOpen) {
                // 菜单关闭时向右滑动超过menu三分之一宽度才会打开菜单
                    if (dy < - menu.getMeasuredHeight() / 3) {
                        state = Scroll_State.Scroll_to_Open;
                    } else {
                        state = Scroll_State.Scroll_to_Close;
                    }
                } else {
                // 菜单打开时当按下时的触摸点在menu区域时,只有向左滑动超过menu的三分之一,才会关闭
                    if (dy > menu.getMeasuredHeight() / 3) {
                        state = Scroll_State.Scroll_to_Close;
                    } else {
                        state = Scroll_State.Scroll_to_Open;
                    }
                }
                smoothScrollto();
                break;
            default:
                break;
        }
        return true;
    }

    private void smoothScrollto() {
        int scrolly = getScrollY();
        switch (state) {
            case Scroll_to_Close:
                mScroller.startScroll(0, scrolly, 0, - scrolly, 500);
                isOpen = false;
                break;
            case Scroll_to_Open:
                mScroller.startScroll(0, scrolly, 0, menu.getMeasuredHeight()- scrolly, 500);
                isOpen = true;
                break;
            default:
                break;
        }
        //需要主动出发一次invalidate,之后再移动的时候自动走computeScroll方法
        invalidate();
    }

    @Override
    public void computeScroll() {
        //滑动时候computeScrollOffset会一直返回false,只有在startScroll结束时候才会为true
        if (mScroller.computeScrollOffset()) {
            scrollTo(0, mScroller.getCurrY());
            postInvalidate();
        }
        super.computeScroll();
    }
}

整段代码不算复杂,最主要的是坐标的计算。startScroll(int startX, int startY, int dx, int dy, int duration)这个方法对于坐标滑动比较难算。开始我一直在纠结dx,dy正的往什么方向,负的往什么方向。后来看了一下源码。

代码语言:javascript复制
public void startScroll(int startX, int startY, int dx, int dy, int duration) {  
    mMode = SCROLL_MODE;  
    mFinished = false;  
    mDuration = duration;  
    mStartTime = AnimationUtils.currentAnimationTimeMillis();  
    mStartX = startX;  
    mStartY = startY;  //开始滑动坐标
    mFinalX = startX   dx;  
    mFinalY = startY   dy;  //滑动终点坐标
    mDeltaX = dx;  
    mDeltaY = dy;  
    mDurationReciprocal = 1.0f / (float) mDuration;  
    // This controls the viscous fluid effect (how much of it)  
    mViscousFluidScale = 8.0f;  
    // must be set to 1.0 (used in viscousFluid())  
    mViscousFluidNormalize = 1.0f;  
    mViscousFluidNormalize = 1.0f / viscousFluid(1.0f);  
}  

所以如果从A(a1, a2)滑动到B(b1,b2), 只需要startScroll(a1,a2,b1-a1,b2-a2)就行。主要就求个差值。

使用方法

代码语言:javascript复制
    private MySlidingMenu mSlidingMenu;
    ......
    mSlidingMenu = new MySlidingMenu(this, LayoutInflater
         .from(this).inflate(R.layout.fragment1, null), LayoutInflater
         .from(this).inflate(R.layout.fragment2, null));

补充

这个会导致listview 滑动冲突,如果列数固定可以一屏显示直接重写listviewe的onTouchEvent 返回false就行。如果比较多的话,那就通过在onTouchEvent () 中getParent().requestDisallowInterceptTouchEvent(bool)来设置哪个控件响应事件。

一般的思路是list滑动到头和尾的时候,才将相应方向的滑动事件传给父组件。

0 人点赞