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,比較簡單就不細說了.