相信會碰到很多類似的需求,一個列表控制項,然後控制項上方的一個頭部需要自定義,這樣就不好有時候也不能加在列表控制項的頭部了,那必須得嵌套一層scrollview了,沒毛病,那麼一般的列表控制項都是有上拉下拉的操作,而且一般也是在 github 上找尋一個收藏量高的 來做為一個全局通用的上下拉控制項,這裡問題就 ...
相信會碰到很多類似的需求,一個列表控制項,然後控制項上方的一個頭部需要自定義,這樣就不好有時候也不能加在列表控制項的頭部了,那必須得嵌套一層scrollview了,沒毛病,那麼一般的列表控制項都是有上拉下拉的操作,而且一般也是在 github 上找尋一個收藏量高的
來做為一個全局通用的上下拉控制項,這裡問題就來了,一般的 scrollview 嵌套 recyclerview 或者 listview 都畢竟容易解決,可是在加上一層上下拉控制項呢?上下拉控制項肯定會有它自己的觸摸處理機制,這樣你改起來也很麻煩,這種滑動觸摸的問題最是難搞,也有的是scrollview
嵌套 viewpage 嵌套 recyclerview 的,不過這種效果網上還是有些方案的,搜索material design效果也有一些現成的,不過 scrollview 嵌套 SwipeRefreshLayout 嵌套 recyclerview 或者 scrollview 嵌套 PtrClassicFrameLayout 嵌套 recyclerview 很少,我搜了好久沒有滿足我需求的,而且很多是自定義了 recyclerview 和下拉控制項才解決的,這樣的話它不是用的 github 原控制項,可以說是自己寫的,可項目里很多都是有固定的一套第三方庫,你不能改,除非你是項目負責人,一開始就這樣規定了,不然就得想辦法去解決
還有一種網上的方案是退而求其次,用 NestedScrollView,不過這也有一些問題,就是下滑的時候對於侵入式的下拉方式有效果,可是對於非侵入刷新就不好使了,而且我個人也更喜歡非侵入刷新,所以花了些空閑的時間搗鼓了一個demo,解決了一個心頭之患,下次遇到就能輕鬆的對待了,不用在去糾結用這個庫是否會有問題了
一開始做的走了個誤區,沒有思考清楚,大致思路就是首先把下拉的控制項禁止 setEnabled(false) ,然後scrollview攔截事件,判斷滑動,超出了頭部的距離就讓子控制項滑動,這是一個大致的思路,然後就開幹了,可想法是美好的,現實是殘酷的,並沒有取得成功,而且還把自己的思路攪渾了
先看失敗的效果圖
咋一看好像沒啥問題,而且簡單,這還說這麼多做什麼,其實只是效果沒有顯示出來(顯示不出來),這就尷尬了⊙﹏⊙‖∣
如果直接把上下拉控制項包住頭部其實沒有問題,但是這樣你下拉的時候在頂端顯示效果,這樣頭部和直接加入到列表控制項里有什麼區別,所以重新創建了個類,從頭再來,好好在理理思路,先從簡單的開始,SwipeRefreshLayout 是android自帶的下拉刷新庫,而且是侵入式的下拉刷新,這樣處理起來更簡單,柿子挑軟的捏了,這樣都搞不定那就猝死在電腦邊吧,結果肯定是成功了,先看效果圖
this is scrollView其實就是一個固定的頭部,測試用的,可以換成自己的,而且還能根據滑動添加動畫,基本的需求都滿足了,直接看代碼,代碼粗糙了點
1 package com.example.demo.test2; 2 3 import android.os.Handler; 4 import android.support.v4.widget.SwipeRefreshLayout; 5 import android.support.v7.widget.LinearLayoutManager; 6 import android.support.v7.widget.RecyclerView; 7 import android.view.LayoutInflater; 8 import android.view.View; 9 import android.view.ViewGroup; 10 import android.view.ViewTreeObserver; 11 import android.widget.TextView; 12 import android.widget.Toast; 13 14 import com.example.demo.BaseActivity; 15 import com.example.demo.R; 16 17 import butterknife.BindView; 18 19 public class Test3Activity extends BaseActivity { 20 21 @BindView(R.id.scrollView) 22 MyNestedScroll scrollView; 23 @BindView(R.id.tv_head) 24 TextView tv_head; 25 @BindView(R.id.layout) 26 SwipeRefreshLayout layout; 27 @BindView(R.id.recyclerView) 28 RecyclerView recyclerView; 29 private boolean hasMeasured; 30 31 @Override 32 protected int getLayoutId() { 33 return R.layout.activity_test3; 34 } 35 36 @Override 37 protected void initView() { 38 recyclerView.setLayoutManager(new LinearLayoutManager(Test3Activity.this)); 39 recyclerView.setAdapter(new ContentAdapter()); 40 ViewTreeObserver vto = tv_head.getViewTreeObserver(); 41 vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 42 public boolean onPreDraw() { 43 if (hasMeasured == false) { 44 int height = tv_head.getMeasuredHeight(); 45 scrollView.setHeadHeight(height); 46 hasMeasured = true; 47 } 48 return true; 49 } 50 }); 51 scrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() { 52 @Override 53 public void onScrollChanged() { 54 boolean b = scrollView.getScrollY()==0; 55 layout.setEnabled(b); 56 } 57 }); 58 //設置刷新時動畫的顏色,可以設置4個 59 layout.setColorSchemeResources(android.R.color.holo_blue_light, android.R.color.holo_red_light, 60 android.R.color.holo_orange_light, android.R.color.holo_green_light); 61 layout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { 62 63 @Override 64 public void onRefresh() { 65 //正在刷新 66 // TODO Auto-generated method stub 67 new Handler().postDelayed(new Runnable() { 68 69 @Override 70 public void run() { 71 // TODO Auto-generated method stub 72 //刷新完成 73 layout.setRefreshing(false); 74 } 75 }, 500); 76 } 77 }); 78 79 } 80 81 class ContentAdapter extends RecyclerView.Adapter { 82 @Override 83 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 84 return new ContentViewHolder(LayoutInflater.from(Test3Activity.this).inflate(R.layout.item_test, parent, false)); 85 } 86 87 @Override 88 public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) { 89 final ContentViewHolder viewHolder = (ContentViewHolder) holder; 90 viewHolder.itemView.setOnClickListener(new View.OnClickListener() { 91 @Override 92 public void onClick(View view) { 93 Toast.makeText(Test3Activity.this, ""+position, Toast.LENGTH_SHORT).show(); 94 } 95 }); 96 } 97 98 @Override 99 public int getItemCount() { 100 return 15; 101 } 102 } 103 104 class ContentViewHolder extends RecyclerView.ViewHolder { 105 106 public ContentViewHolder(View itemView) { 107 super(itemView); 108 } 109 } 110 }MainActivity
1 package com.example.demo.test2; 2 3 import android.content.Context; 4 import android.util.AttributeSet; 5 import android.view.MotionEvent; 6 import android.widget.ScrollView; 7 8 /** 9 * Created by LiuZhen on 2017/6/8. 10 */ 11 12 public class MyNestedScroll extends ScrollView { 13 14 private int headHeight = 300;//預設高度,一般動態設置 15 private int downY; 16 17 public MyNestedScroll(Context context) { 18 super(context); 19 } 20 21 public MyNestedScroll(Context context, AttributeSet attrs) { 22 super(context, attrs); 23 } 24 25 public MyNestedScroll(Context context, AttributeSet attrs, int defStyleAttr) { 26 super(context, attrs, defStyleAttr); 27 } 28 29 @Override 30 public boolean onInterceptTouchEvent(MotionEvent e) { 31 int action = e.getAction(); 32 int dis = getScrollY(); 33 switch (action) { 34 case MotionEvent.ACTION_DOWN: 35 downY = (int) e.getRawY(); 36 break; 37 case MotionEvent.ACTION_MOVE: 38 int moveY = (int) e.getRawY(); 39 /**判斷是向下滑動**/ 40 if(downY-moveY>0){ 41 if (dis < headHeight && dis >= 0){ 42 return true;//滑動距離小於頭部並且沒有超出頭部攔截事件,讓本身去滑動 43 }else{ 44 return false;//不攔截事件 45 } 46 }else{ 47 } 48 } 49 50 return super.onInterceptTouchEvent(e); 51 } 52 53 public void setHeadHeight(int headHeight) { 54 this.headHeight = headHeight; 55 } 56 57 }MyNestedScroll
這個很簡單,主要的邏輯也就一個方法里
然後在github上又找了個下拉庫,收藏畢竟多
結果不行了,上滑一小段距離,然後在下拉,結果拉不動了,直接就顯示了上拉效果,這顯然不正常,問題一個一個來解決,肯定是有其它問題的,不過先把眼前的解決了才能下一步,要解決這個問題就得禁止 PtrClassicFrameLayout 的下拉了,問題的根源很有可能是我們用的上下拉控制項的滑動刷新效果起衝突了,最好的辦法肯定是直接禁止了,所以就給scrollview來個監聽 onScrollChanged ,獲取scrollview的scrolly,如果scrolly是0,就不禁止,因為在頂部了,不是0就禁止下拉操作,結果好了,可惜新的問題來了,上拉沒反應了,因為被禁止了,所以需要判斷滑動到了底部讓下拉效果出現
於是有了這麼兩個判斷,然後肯定還是有問題的,上拉效果沒事了,下拉效果出事了,滑動到頂端的時候下拉效果直接出現了,這樣頭部都顯示不出來了,而此時事件處於子列表控制項,所以scrollview不好去監聽了,只得在 recyclerView 上面下手了,需要監聽滑動到頂部,然後禁止上下拉效果,交給scrollview滑動,讓頭部顯示出來
可是這樣一來發現還是那樣,而調試發現的確觸發了事件,可是為什麼上下拉還是沒有禁止呢,這樣滑動不出頭部,然後調試發現被下麵的 scrollview 的 onScrollChanged 事件里覆蓋了,又設置回去了,所以還得控制一下這裡,讓scrollview的監聽不要覆蓋上面的設置,這裡只是下拉的時候才會用到,所以就有了dy這個全局參數,這樣一來就ok了
1 package com.example.demo.test2; 2 3 import android.os.Handler; 4 import android.support.v4.widget.NestedScrollView; 5 import android.support.v4.widget.SwipeRefreshLayout; 6 import android.support.v7.widget.LinearLayoutManager; 7 import android.support.v7.widget.RecyclerView; 8 import android.util.Log; 9 import android.view.LayoutInflater; 10 import android.view.MotionEvent; 11 import android.view.View; 12 import android.view.ViewGroup; 13 import android.view.ViewTreeObserver; 14 import android.widget.ScrollView; 15 import android.widget.TextView; 16 import android.widget.Toast; 17 18 import com.example.demo.BaseActivity; 19 import com.example.demo.R; 20 import com.example.demo.test2.MyRecyclerView; 21 22 import butterknife.BindView; 23 import in.srain.cube.views.ptr.PtrClassicFrameLayout; 24 import in.srain.cube.views.ptr.PtrDefaultHandler2; 25 import in.srain.cube.views.ptr.PtrFrameLayout; 26 27 public class Test2Activity extends BaseActivity { 28 29 @BindView(R.id.scrollView) 30 MyNestedScroll scrollView; 31 @BindView(R.id.tv_head) 32 TextView tv_head; 33 @BindView(R.id.layout) 34 PtrClassicFrameLayout layout; 35 @BindView(R.id.recyclerView) 36 RecyclerView recyclerView; 37 private boolean hasMeasured; 38 int height,dy; 39 40 @Override 41 protected int getLayoutId() { 42 return R.layout.activity_test2; 43 } 44 45 @Override 46 protected void initView() { 47 final LinearLayoutManager manager = new LinearLayoutManager(Test2Activity.this); 48 recyclerView.setLayoutManager(manager); 49 recyclerView.setAdapter(new ContentAdapter()); 50 ViewTreeObserver vto = tv_head.getViewTreeObserver(); 51 vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 52 public boolean onPreDraw() { 53 if (hasMeasured == false) { 54 height = tv_head.getMeasuredHeight(); 55 scrollView.setHeadHeight(height);//設置頭部的高度 56 hasMeasured = true; 57 } 58 return true; 59 } 60 }); 61 62 recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 63 @Override 64 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 65 66 // int visibleItemCount = manager.getChildCount(); 67 // int totalItemCount = manager.getItemCount(); 68 int pastVisiblesItems = manager.findFirstVisibleItemPosition(); 69 Test2Activity.this.dy = dy; 70 //小於0表示下拉滑動,下拉到頂端則禁止子控制項刷新,讓scrollview滑動,頭部完全顯示後才能下拉,這裡只做下滑到頂部時禁止立刻刷新,讓scrollview滑動 71 if ( pastVisiblesItems == 0 && dy < 0) { 72 Log.e("onScrolled","dy "+dy); 73 layout.setEnabled(false); 74 } 75 } 76 }); 77 scrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() { 78 @Override 79 public void onScrollChanged() { 80 //滑動的距離超出頭部,讓列表控制項自己滑動,dy大於0是上滑操作,這裡只做滑動超出頭部後交給子列表滑動 81 boolean b = scrollView.getScrollY()==0;//上拉一段距離後下拉,結果直接下拉效果出現了,滑動失效 82 //解決上拉滑動到底部後讓上拉效果有用 83 if (scrollView != null && scrollView.getMeasuredHeight() <= scrollView.getScrollY()+height && dy >= 0) { 84 b = true; 85 // Log.e("Changed","true "); 86 } 87 layout.setEnabled(b); 88 Log.e("Changed",""+b+" - scrollView.getScrollY "+scrollView.getScrollY()); 89 } 90 }); 91 layout.setLastUpdateTimeRelateObject(this); 92 layout.setPtrHandler(new PtrDefaultHandler2() { 93 94 @Override 95 public void onLoadMoreBegin(PtrFrameLayout frame) { 96 updateData(); 97 } 98 99 @Override 100 public void onRefreshBegin(PtrFrameLayout frame) { 101 updateData(); 102 } 103 104 @Override 105 public boolean checkCanDoLoadMore(PtrFrameLayout frame, View content, View footer) { 106 return super.checkCanDoLoadMore(frame, recyclerView, footer); 107 } 108 109 @Override 110 public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) { 111 return super.checkCanDoRefresh(frame, recyclerView, header); 112 } 113 }); 114 115 layout.postDelayed(new Runnable() { 116 @Override 117 public void run() { 118 // mPtrFrame.autoRefresh(); 119 } 120 }, 100); 121 122 123 } 124 125 protected void updateData() { 126 127 layout.postDelayed(new Runnable() { 128 @Override 129 public void run() { 130 layout.refreshComplete(); 131 } 132 }, 1000); 133 } 134 135 class ContentAdapter extends RecyclerView.Adapter { 136 @Override 137 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 138 return new ContentViewHolder(LayoutInflater.from(Test2Activity.this).inflate(R.layout.item_test, parent, false)); 139 } 140 141 @Override 142 public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) { 143 final ContentViewHolder viewHolder = (ContentViewHolder) holder; 144 viewHolder.itemView.setOnClickListener(new View.OnClickListener() { 145 @Override 146 public void onClick(View view) { 147 Toast.makeText(Test2Activity.this, ""+position, Toast.LENGTH_SHORT).show(); 148 } 149 }); 150 } 151 152 @Override 153 public int getItemCount() { 154 return 15; 155 } 156 } 157 158 class ContentViewHolder extends RecyclerView.ViewHolder { 159 160 public ContentViewHolder(View itemView) { 161 super(itemView); 162 } 163 } 164 }MainActivity
侵入式的下拉刷新的確麻煩很多,但是好在自己也都解決了,而有了這兩種方案不敢說每個第三方庫都能通用,但是大部分都可以通用,只要懂得了其中的原理,自己稍加修改也行,畢竟第三方庫太多了,每個庫都有它自己的自定義,所以還是要根據獨特的情況去修改