AndroidUI系列--在DecorView層解決RecyclerView和ScrollView的滑動衝突
滑動衝突,這個是作安卓的必經之坑。最開始的ListView和ScollView衝突,或者ListView巢狀ListView滑動衝突,再或者ListView和ViewPager的滑動衝突,再或者是GraidView等可滑動控制元件互相巢狀的衝突。解決方案呢,有很多。比如在onTouchEvent中攔截事件。又或者自定義ListView,修改onMesure測量,使它在測量時獲得最大的寬高,這樣可以讓它不滑動。全部展示,當然作為在android摸爬滾打了這麼久的程式猿,這些坑都應該踩過了,而且網上一大堆解決方案,不得不說,這就是開源的好處啊,想著谷歌巴巴把kotlin扶上位了,我們這些苦逼的程式猿,那就只有跟著大部隊走了。沒辦法呀~夾縫裡生存。
View的繪製流程,Activity–phonewindow–decorview–contentview,如下圖
我們平時在Activity的setContentView就是在ContentViews作文章。那麼我們的衝突就是在這裡,在ContentView裡設定了一個activity_main.xml,為什麼會有滑動衝突呢,那是因為recyclerview和scollview都設定在了activity_main.xml。那麼換個角度,如果把recyclerview加在contentviews和activity_main.xml佈局平級。那麼是不是就不存在滑動衝突了呢,想到就來試試。
首先自定義一個view,用來彈窗。
package com.example.administrator.bounceview;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
/**
* Created by ShuWen on 2017/5/23.
*/
public class BounceView extends View {
private int mArcMaxHeight;//彈窗最高距離
private int mArcHeight;//記錄變換過程的距離
private Paint mPaint;//畫筆
private Path mPath = new Path();//繪製動畫弧度
private BounceAnimatorListener animatorListener;//動畫開始的監聽回撥
private Status status = Status.NONE;//記錄動畫的狀態
public enum Status{
//沒動,上升,下降
NONE,STATUS_UP,STATUS_DOWN
}
public BounceView(Context context) {
super(context);
init();
}
public BounceView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public BounceView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
//初始化
private void init() {
mPaint = new Paint();
mPaint.setColor(Color.WHITE);
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL);
mArcMaxHeight = getResources().getDimensionPixelOffset(R.dimen.m_maxarcheight);
}
//上升的動畫
public void show(){
status = Status.STATUS_UP;
if (animatorListener != null){
this.postDelayed(new Runnable() {
@Override
public void run() {
animatorListener.showContent();
}
},600);
}
ValueAnimator animator = ValueAnimator.ofInt(0,mArcMaxHeight);
animator.setDuration(700);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mArcHeight = (int) valueAnimator.getAnimatedValue();
if (mArcHeight == mArcMaxHeight){
bounce();
}
invalidate();
}
});
animator.start();
}
//下降的動畫
private void bounce() {
status = Status.STATUS_DOWN;
ValueAnimator valueAnimator = ValueAnimator.ofInt(mArcMaxHeight,0);
valueAnimator.setDuration(600);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mArcHeight = (int) valueAnimator.getAnimatedValue();
invalidate();
}
});
valueAnimator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int currentY = 0;
switch (status){
case NONE:
currentY = 0;
break;
case STATUS_UP:
currentY = (int) (getHeight()*(1 - (float)(mArcHeight/mArcMaxHeight))+mArcMaxHeight);
break;
case STATUS_DOWN:
currentY = mArcMaxHeight;
break;
}
mPath.reset();
mPath.moveTo(0,currentY);
mPath.quadTo(getWidth()/2,currentY - mArcHeight,getWidth(),currentY);
mPath.lineTo(getWidth(),getHeight());
mPath.lineTo(0,getHeight());
mPath.close();
canvas.drawPath(mPath,mPaint);
}
public void setAnimatorListener(BounceAnimatorListener animatorListener){
this.animatorListener = animatorListener;
}
public interface BounceAnimatorListener{
void showContent();
}
}
上升過程中,繪製動畫,使用ValueAnimator在回撥裡進行更新介面,呼叫invalidate()。中間使用到了二階貝塞爾曲線,關於貝塞爾其實很簡單的,在網上一搜,當然就有了。
那麼在建立一個類,用來載入BounceVeiw。
package com.example.administrator.bounceview;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.FrameLayout;
/**
* Created by ShuWen on 2017/5/23.
*/
public class BounceMenu {
private RecyclerView recyclerView;
private BounceView bounceView;
private ViewGroup parentVG;
private View rootView;
private BounceMenu(View view, int resId, final MyAdapter myAdapter) {
parentVG = findParentVG(view);
rootView = LayoutInflater.from(view.getContext()).inflate(resId,null,false);
recyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerview);
bounceView = (BounceView) rootView.findViewById(R.id.bounceview);
recyclerView.setLayoutManager(new LinearLayoutManager(view.getContext()));
bounceView.setAnimatorListener(new BounceView.BounceAnimatorListener() {
@Override
public void showContent() {
recyclerView.setVisibility(View.VISIBLE);
recyclerView.setAdapter(myAdapter);
recyclerView.scheduleLayoutAnimation();
}
});
}
public static BounceMenu makeBounce(View view, int resId, final MyAdapter myAdapter){
return new BounceMenu(view, resId, myAdapter);
}
public void show(){
if (rootView != null){
parentVG.removeView(rootView);
}
ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
parentVG.addView(rootView,layoutParams);
bounceView.show();
}
private ViewGroup findParentVG(View view) {
do {
if (view instanceof FrameLayout){
//找到decorview的根佈局
if (view.getId() == android.R.id.content){
return (ViewGroup) view;
}
}
if (view != null){
ViewParent viewParent = view.getParent();
view = viewParent instanceof View? (View) viewParent :null;
}
}while (view!= null);
return null;
}
}
在這裡面,傳入需要新增recylerview的跟佈局,通過這個根佈局獲得decorview的contentviews這個佈局,然後在這個佈局上新增recyclerview。這樣就是與activity_main同級,不會有滑動衝突。在bounceview的監聽裡,新增recyclerview的動畫。
那麼再來看看MianAcicity的程式碼:
package com.example.administrator.bounceview;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private MyAdapter myAdapter;
private List<String> stringList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
stringList = new ArrayList<>();
for (int i = 0; i < 20; i++) {
stringList.add("阿西吧"+i);
}
myAdapter = new MyAdapter(this,stringList) {
@Override
protected int ItemLayoutId() {
return R.layout.item;
}
@Override
protected void onBindHolder(MyViewHolder myViewHolder, int position) {
TextView textView = myViewHolder.getTextView(R.id.text);
textView.setText(stringList.get(position));
}
};
}
public void click(View view){
BounceMenu bounceMenu = BounceMenu.makeBounce(findViewById(R.id.activity_main),R.layout.bounce_view_layout,myAdapter);
bounceMenu.show();
}
}
呼叫就是相當的簡單了。同時還有炫酷的動畫,何樂而不為呢。接下來所有程式碼都貼出來。
activity_main的xml佈局。使用ScrollView,展示主要資料。在每一項的點選事件,觸發彈窗,recyclerview。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.administrator.bounceview.MainActivity">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#465">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:textSize="20sp"
android:onClick="click"
android:text="你滑動啊"
/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#fff">
</View>
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:textSize="20sp"
android:onClick="click"
android:text="你滑動啊"
/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#fff">
</View>
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:onClick="click"
android:textSize="20sp"
android:text="你滑動啊"
/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#fff">
</View>
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:textSize="20sp"
android:onClick="click"
android:text="你滑動啊"
/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#fff">
</View>
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:textSize="20sp"
android:onClick="click"
android:text="你滑動啊"
/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#fff">
</View>
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:textSize="20sp"
android:onClick="click"
android:text="你滑動啊"
/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#fff">
</View>
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:textSize="20sp"
android:onClick="click"
android:text="你滑動啊"
/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#fff">
</View> <TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:textSize="20sp"
android:onClick="click"
android:text="你滑動啊"
/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#fff">
</View>
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:textSize="20sp"
android:onClick="click"
android:text="你滑動啊"
/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#fff">
</View>
</LinearLayout>
</ScrollView>
</RelativeLayout>
那麼再看看彈窗的xml。
<?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">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="290dp"
android:layout_gravity="bottom">
<com.example.administrator.bounceview.BounceView
android:id="@+id/bounceview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:overScrollMode="never"
android:layout_alignParentBottom="true"
android:layoutAnimation="@anim/bounce_layout"
android:layout_marginTop="70dp">
</android.support.v7.widget.RecyclerView>
</RelativeLayout>
</FrameLayout>
這裡就是彈窗了,recyclerview和scrollview同級,不會產生滑動衝突。
這個是解決滑動衝突的一個可行方案,相當不錯。如果覺得動畫不必要,直接去掉動畫,只需要BounceMenu中的一些邏輯就ok了。我會把程式碼放在git上,有興趣的朋友可以自己研究研究。
效果圖: