LayerPagerDemo - 雙層可拖拽式佈局界面

LayerPagerDemo

雙層佈局與事件傳遞

需求是這樣的:

雙層佈局,上層佈局類似於ScrollView可以滑動。

當滑動頂部,拖拽下半部分佈局,可以讓佈局進行分離,實現可拖拽效果。

當拖拽隱藏,顯示出裏層佈局,事件可以傳遞到裏層佈局。


做出的效果如下:


這種佈局設計考驗開發者對事件分發,自定義控件,Scroller界面滾動原理等知識的掌握程度。

下面我來分析我自己是怎麼實現的,先從佈局開始:

最外層根佈局可以用幀佈局或者相對佈局。

<?xml version="1.0" encoding="utf-8"?>
    <FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <include layout="@layout/layout_in"/>
    <include layout="@layout/layout_out"/>
    </FrameLayout>

1,裏層佈局。

裏層佈局爲RecycleView,有一個特殊的條目作爲他的Header佈局,這個很簡單。

<?xml version="1.0" encoding="utf-8"?>
    <FrameLayout  xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <android.support.v7.widget.RecyclerView
        android:id="@+id/recycle_in"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <TextView
        android:id="@+id/tv_open"
        android:background="#8841A7E1"
        android:layout_gravity="bottom"
        android:textColor="@android:color/white"
        android:textSize="16sp"
        android:text="open"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="40dp"/>

    </FrameLayout>

2,外層佈局。

外層佈局,一個自定義的ScrollView,自定義的拖拽ViewGroup,和自定義HeaderViewGroup

<?xml version="1.0" encoding="utf-8"?>
    <com.ruzhan.layerpagerdemo.view.LayerScrollView 
    xmlns:android="http://schemas.android.com/apk/res/android"
      android:id="@+id/scroll_root"
      android:layout_width="match_parent"
      android:layout_height="wrap_content">

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

      <include layout="@layout/layout_header"/>

      <com.ruzhan.layerpagerdemo.view.LayerLinearLayout
          android:id="@+id/ll_body"
          android:orientation="vertical"
          android:layout_width="match_parent"
          android:layout_height="wrap_content">

            <FrameLayout
                android:background="#E18441"
                android:layout_width="match_parent"
                android:layout_height="166dp">

              <TextView
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:layout_gravity="center"
                  android:text="Content01"
                  android:textColor="@android:color/white"
                  android:textSize="36sp"/>
            </FrameLayout>

            <FrameLayout
                android:background="#E1C741"
                android:layout_width="match_parent"
                android:layout_height="166dp">

              <TextView
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:layout_gravity="center"
                  android:text="Content02"
                  android:textColor="@android:color/white"
                  android:textSize="36sp"/>
            </FrameLayout>

            <FrameLayout
                android:background="#B7E141"
                android:layout_width="match_parent"
                android:layout_height="166dp">

              <TextView
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:layout_gravity="center"
                  android:text="Content03"
                  android:textColor="@android:color/white"
                  android:textSize="36sp"/>
            </FrameLayout>

            <FrameLayout
                android:background="#41E194"
                android:layout_width="match_parent"
                android:layout_height="166dp">

              <TextView
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:layout_gravity="center"
                  android:text="Content04"
                  android:textColor="@android:color/white"
                  android:textSize="36sp"/>
            </FrameLayout>

            <FrameLayout
                android:background="#4164E1"
                android:layout_width="match_parent"
                android:layout_height="166dp">

              <TextView
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:layout_gravity="center"
                  android:text="Content05"
                  android:textColor="@android:color/white"
                  android:textSize="36sp"/>

        </FrameLayout>
          </com.ruzhan.layerpagerdemo.view.LayerLinearLayout>
        </LinearLayout>
      </com.ruzhan.layerpagerdemo.view.LayerScrollView>

Header,抽取出來,RecycleView也需要Header

<?xml version="1.0" encoding="utf-8"?>
    <com.ruzhan.layerpagerdemo.view.LayerHeaderFrameLayout 
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/fl_header"
        android:layout_width="match_parent"
        android:layout_height="266dp">

          <TextView
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:layout_gravity="center"
              android:background="#E14141"
              android:gravity="center"
              android:text="Header"
              android:textColor="@android:color/white"
              android:textSize="46sp"/>

    </com.ruzhan.layerpagerdemo.view.LayerHeaderFrameLayout>

佈局的設計是醬紫。現在我來說動態圖的效果是如何實現的

1,向上滑動,讓ScrollView自然滾動就好。

2,向下滑動,當ScrollView到頂部,觸摸Body佈局繼續拖拽時,使佈局分離。

下面看代碼:

public class LayerScrollView extends ScrollView {

  private int mDownY;
  private int mMoveY;
  private LayerHeaderFrameLayout mHeader;
  private LayerLinearLayout mBodyLayout;

  public LayerScrollView(Context context) {
    this(context, null);
  }

  public LayerScrollView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }

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

  @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) {
    super.onScrollChanged(l, t, oldl, oldt);

    //如果滑動大於Header 0.7高度,可以做open動畫
    if (t >= mHeader.getMeasuredHeight() * 0.7
    && mBodyLayout.getCurrentState() == mBodyLayout.STATE_DOWN) {
      mHeader.setIsHide(true);
    } else {
      mHeader.setIsHide(false);
    }
  }

  @Override public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
      case MotionEvent.ACTION_DOWN:
        mDownY = (int) ev.getRawY();
        break;

      case MotionEvent.ACTION_MOVE:
        mMoveY = (int) ev.getRawY();
        break;

      case MotionEvent.ACTION_CANCEL:
      case MotionEvent.ACTION_UP:
        mDownY = 0;
        mMoveY = 0;
        break;
}
        int diffY = mDownY - mMoveY;

        if (diffY > 0) {//向上滑動,ScrollView處理事件
          return super.onInterceptTouchEvent(ev);
        }

        //ScrollView處於頂部並向下滑動,body佈局爲顯示狀態,事件需要給body佈局
        if (getScrollY() == 0 && mBodyLayout.getCurrentState() == LayerLinearLayout.STATE_UP) {
          return false;
        }

        //ScrollView處於頂部並向下滑動,body佈局爲移動狀態,事件需要給body佈局
        if (getScrollY() == 0 && mBodyLayout.getCurrentState() == LayerLinearLayout.STATE_MOVE) {
          return false;
        }

        return super.onInterceptTouchEvent(ev);
   }

      @Override public boolean onTouchEvent(MotionEvent ev) {
    //body佈局隱藏不處理
        if (mBodyLayout.getCurrentState() == LayerLinearLayout.STATE_DOWN) {
          return false;
        }

        return super.onTouchEvent(ev);
      }

    public void setBodyLayout(LayerLinearLayout bodyLayout) {
        mBodyLayout = bodyLayout;
     }
    public void setHeader(LayerHeaderFrameLayout header) {
         mHeader = header;
     }
}

ScrollView主要在事件攔截和處理我們需要做一些判斷:

1,向上滑,ScrollView自然滾動,事件由ScrollView處理。

 switch (ev.getAction()) {
      case MotionEvent.ACTION_DOWN:
            mDownY = (int) ev.getRawY();
            break;

      case MotionEvent.ACTION_MOVE:
            mMoveY = (int) ev.getRawY();
            break;

      case MotionEvent.ACTION_CANCEL:
      case MotionEvent.ACTION_UP:
            mDownY = 0;
            mMoveY = 0;
            break;
    }
        int diffY = mDownY - mMoveY;
        if (diffY > 0) {//向上滑動,ScrollView處理事件
          return super.onInterceptTouchEvent(ev);
}

2,到頂部後,如果向下滑,事件傳遞給Body佈局,ScrollView必須不攔截,不處理事件。

//ScrollView處於頂部並向下滑動,body佈局爲顯示狀態,事件需要給body佈局
    if (getScrollY() == 0 && mBodyLayout.getCurrentState() == LayerLinearLayout.STATE_UP) {
      return false;
    }

    //ScrollView處於頂部並向下滑動,body佈局爲移動狀態,事件需要給body佈局
    if (getScrollY() == 0 && mBodyLayout.getCurrentState() == LayerLinearLayout.STATE_MOVE) {
      return false;
    }

   @Override public boolean onTouchEvent(MotionEvent ev) {

    //body佈局隱藏不處理
    if (mBodyLayout.getCurrentState() == LayerLinearLayout.STATE_DOWN) {
      return false;
    }
    return super.onTouchEvent(ev);
  }

接着看Body佈局:

public class LayerLinearLayout extends LinearLayout {

  private Scroller mScroller;
  private float mLastY;
  private int mMoveY;
  private static int DRAG_Y_MAX = 230;
  public static final int STATE_UP = 1;
  public static final int STATE_DOWN = 2;
  public static final int STATE_MOVE = 3;
  public int mCurrentState = STATE_UP;
  private FrameLayout mScrollRootHeader;
  private RecyclerView mInRecycleView;

public LayerLinearLayout(Context context) {
    this(context, null);
}

public LayerLinearLayout(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

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

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

public int getCurrentState() {
    return mCurrentState;
}

public void setCurrentState(int currentState) {
    mCurrentState = currentState;
}

@Override public boolean onTouchEvent(MotionEvent event) {
    if (mCurrentState == STATE_DOWN) {//如果當前狀態爲隱藏,不處理
      return false;
    }

    float y = event.getY();
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
    mLastY = y;
    break;

      case MotionEvent.ACTION_MOVE:
    int moveY = (int) (mLastY - y);
    if (getScrollY() <= 0) {
      mCurrentState = STATE_MOVE;
      scrollBy(0, moveY / 2);//距離減半,產生拉力效果
    }
    mLastY = y;
    break;

      case MotionEvent.ACTION_CANCEL:
      case MotionEvent.ACTION_UP:
    process();//佈局還原或者隱藏
    break;

}
return true;
}

private void process() {
    if (-getScrollY() > DRAG_Y_MAX) {//隱藏
      mCurrentState = STATE_DOWN;
      mScrollRootHeader.setVisibility(INVISIBLE);
      mMoveY = Math.abs(-getMeasuredHeight() - getScrollY());//隱藏,佈局移動的高度
      mScroller.startScroll(0, getScrollY(), 0, -mMoveY, 1000);

    } else {//打開
      mCurrentState = STATE_UP;
      mScroller.startScroll(0, getScrollY(), 0, -getScrollY(), Math.abs(getScrollY()) / 2);
    }

    invalidate();
  }


  public void open() {

    mCurrentState = STATE_UP;
    mScrollRootHeader.setVisibility(VISIBLE);
    mScroller.startScroll(0, -mMoveY, 0, mMoveY, 1000);

    postDelayed(new Runnable() {
      @Override public void run() {
    LinearLayoutManager manager = (LinearLayoutManager) mInRecycleView.getLayoutManager();
    manager.scrollToPosition(0);
      }
    },1000);

    invalidate();

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

 }
   public void setScrollRootHeader(FrameLayout scrollRootHeader) {
        mScrollRootHeader = scrollRootHeader;
    }

   public void setInRecycleView(RecyclerView inRecycleView) {
        mInRecycleView = inRecycleView;
   }
}

Body佈局需要做這幾件事情:

1,在OnTouchEvent方法中,判斷是否爲豎直向下滑動,

如果是,Move事件使用Scroller控制佈局移動。

@Override public boolean onTouchEvent(MotionEvent event) {
    if (mCurrentState == STATE_DOWN) {//如果當前狀態爲隱藏,不處理
      return false;
    }

    float y = event.getY();
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
    mLastY = y;
    break;

      case MotionEvent.ACTION_MOVE:
    int moveY = (int) (mLastY - y);
    if (getScrollY() <= 0) {

      mCurrentState = STATE_MOVE;
      scrollBy(0, moveY / 2);//距離減半,產生拉力效果
    }
    mLastY = y;
    break;

2,在Up事件對當前Body佈局處於的狀態進行處理:

滑動距離過小,回到原來的位置。

滑動距離超出設置的數值,自動下拉隱藏。

 case MotionEvent.ACTION_CANCEL:
 case MotionEvent.ACTION_UP:
    process();//佈局還原或者隱藏
    break;

  private void process() {
    if (-getScrollY() > DRAG_Y_MAX) {//隱藏
      mCurrentState = STATE_DOWN;
      mScrollRootHeader.setVisibility(INVISIBLE);
      mMoveY = Math.abs(-getMeasuredHeight() - getScrollY());//隱藏,佈局移動的高度
      mScroller.startScroll(0, getScrollY(), 0, -mMoveY, 1000);

    } else {//打開
      mCurrentState = STATE_UP;
      mScroller.startScroll(0, getScrollY(), 0, -getScrollY(), Math.abs(getScrollY()) / 2);
    }

    invalidate();
  }

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

3,設置一個重新打開Body佈局的方法。

public void open() {
    mCurrentState = STATE_UP;
    mScrollRootHeader.setVisibility(VISIBLE);
    mScroller.startScroll(0, -mMoveY, 0, mMoveY, 1000);

    postDelayed(new Runnable() {
      @Override public void run() {
    LinearLayoutManager manager = (LinearLayoutManager) mInRecycleView.getLayoutManager();
    manager.scrollToPosition(0);
      }
    },1000);

    invalidate();
  }

接下來是Header佈局,Header只需要滾動,簡單的使用Scroller就好了

public class LayerHeaderFrameLayout extends FrameLayout {

  private Scroller mScroller;
  private boolean mHide;
  private LayerScrollView mScrollRoot;

public LayerHeaderFrameLayout(Context context) {
    this(context, null);
}

public LayerHeaderFrameLayout(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

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

public boolean isHide() {
    return mHide;
}

public void setIsHide(boolean isHide) {
    mHide = isHide;
}

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

public void open() {
if (!mHide) {
  return;
}

    //如果ScrollView跟隨RecycleView移動大於Header 0.7距離,讓ScrollView回到頂部
    mScrollRoot.scrollTo(0, 0);

    //做Header Open動畫
    scrollTo(0, getMeasuredHeight());
    mScroller.startScroll(0, getMeasuredHeight(), 0, -getMeasuredHeight(), 1000);
    invalidate();
  }

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

public void setScrollRoot(LayerScrollView scrollRoot) {
    mScrollRoot = scrollRoot;
}
}

Scroller不清楚的自行百度了,佈局移動什麼鬼全靠它了。

然後是MainActivity

public class MainActivity extends AppCompatActivity
implements BaseRecyclerAdapter.OnItemClickListener {

      @Bind(R.id.recycle_in) RecyclerView recycleIn;
      @Bind(R.id.tv_open) TextView tvOpen;
      @Bind(R.id.fl_header) LayerHeaderFrameLayout flHeader;
      @Bind(R.id.ll_body) LayerLinearLayout llBody;
      @Bind(R.id.scroll_root) LayerScrollView scrollRoot;
      private List<String> mList = new ArrayList<>();
      private InAdapter mAdapter;

    @OnClick(R.id.tv_open) void tvOpen() {
        flHeader.open();
        llBody.open();
    }

  @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        initData();
        intListener();
  }

  private void intListener() {
    mAdapter.setOnItemClickListener(this);
    recycleIn.addOnScrollListener(new RecyclerView.OnScrollListener() {
      @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
    super.onScrolled(recyclerView, dx, dy);
    scrollRoot.scrollBy(dx,dy);
      }
    });
  }

 private void initData() {
    for (int x = 0; x < 20; x++) {
     mList.add("Item " + x);
}

    recycleIn.setLayoutManager(new LinearLayoutManager(this));
    recycleIn.addItemDecoration(new VerticalSpaceItemDecoration(3));
    mAdapter = new InAdapter(mList);
    recycleIn.setAdapter(mAdapter);
    View header = LayoutInflater.from(this).inflate(R.layout.layout_header, recycleIn, false);
    mAdapter.setHeaderView(header);

    scrollRoot.setBodyLayout(llBody);
    scrollRoot.setHeader(flHeader);
    flHeader.setScrollRoot(scrollRoot);
    llBody.setScrollRootHeader(flHeader);
    llBody.setInRecycleView(recycleIn);
}

@Override public void onItemClick(View itemView, int position, Object data) {
    Toast.makeText(this, ""+data, Toast.LENGTH_SHORT).show();
}
}

因爲界面動畫需要獲取到其他佈局的狀態,所以需要把相應的佈局傳遞過去,比較麻煩一點,傳來傳去,射來射去的= -

裏面的佈局是一個帶Header的RecycleView,比較簡單就不細說了.

項目地址: https://github.com/RuZhan/LayerPagerDemo