双层布局与事件传递
需求是这样的:
双层布局,上层布局类似于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,比较简单就不细说了.