android問題及其解決-優化listView卡頓和怎樣禁用ListView的fling
問題解決-優化listView卡頓和怎樣禁用ListView的fling
前戲非常長,轉載請保留出處:http://blog.csdn.net/u012123160/article/details/47720257
問題產生
這算是剛到實習公司接觸到的第一個任務。公司某一產品中某個界面的listView高速滑動會有卡頓的現象發生,我的任務就是解決它。
產生原因分析
我一開始的想法比較簡單。可能是listview的優化沒有做到位,比如convertView的復用、viewHolder的使用等等基礎的優化措施。然並卵。好長時間後最終找到了問題發生的相關代碼...經過在可疑語句上(onTouchEvent方法中的幾個case、onScrollStateChanged方法中的SCROLLSTATE
- 在每天固定的一個時間段內,listView展示的數據室實時刷新的,也就是server會定時向client發送數據刷新的信息,client再向server發出數據請求,client主要屬於被動刷新;其它時間段僅僅是單由client想服務區主動發數據請求。
- listView展示的數據是分頁請求的。依據滾動位置來動態請求某一段數據。
- 依據打印的log。在運行到onScrollStateChanged方法中,也就是說當前listView的狀態時滾動狀態(包含fling)時,中間可能會插入數據請求的log。
- 經過請教導師,問題原因縮小到了數據請求、數據解析更新界面導致的卡頓。
於是再看代碼。發現原因在於接受數據、解析數據、更新listView的Adapter內容的代碼。沒有和listView的滑動狀態相關聯。即滑動過程中可能就接收到了新數據並刷新了界面,造成卡頓。
問題初解思路
主要是滑動和數據接收更新界面之間的沖突,二者通過一個boolean變量關聯一下就可以。即僅僅有在scrollState為IDLE時,才同意進行數據接收和界面刷新(調用notifyAdapterDataChanged方法),其它狀態(FLING,TOUCH_SCROLL)都不同意。 而在server活躍的時間段之外,原本的代碼中也是在滑動結束後才主動發起數據請求的,所以這方面不用考慮。
問題再發生——數據丟失
可是組長提出,可能會發生這種問題,在滑動過程中數據不接收,會發生數據丟失的問題,這個應用本身對數據的實時性非常高,所以這個也須要解決。
問題再解
測試了一下發現,卡頓的原因主要在於界面的刷新而不是數據接收,於是將控制範圍縮小到了:僅僅有在不滑動的狀態下才同意更新界面,而數據接收在滑動過程中也可運行。
保留最新接收到的數據就可以。在滑動結束後及時推斷有無最新的數據緩存。
問題再再提出——disable fling
組長的老板感覺還是卡頓...這次問清楚了。居然認為界面上滑塊的移動有一頓一頓非常難受...我和還有一個前輩對此真是相當無語了。最後沒辦法了,組長說那就把這個fling的功能給禁止了吧...
fling問題研究
百度搜索關鍵詞“android listView fling”。出現的第一條就是以下這個,【Andorid X 項目筆記】禁用ListView的Fling功能(1),但是也是然並卵...
/** 手勢識別類 */
private class TouchGesture extends SimpleOnGestureListener {
/** 高速滾動 */
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return true;
}
}
private OnTouchListener mOnListViewTouchListener = new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (mTouchGesture.onTouchEvent(event))
return true;
return false;
}
};
代碼中mTouchGesture
根本沒有onTouchEvent(event)
這種方法啊啊啊啊啊啊...一開始我是非常開心的....
然後就是各種百度,各種google,各種“靜態流”(stackOverFlow的中文備案名稱,我也是醉了),都沒有個能用的解決方式。 當中能夠使用可是不能達到要求的一段代碼我認為不錯,留下鏈接-Android
listview垂直滑動指定距離,利用反射的原理。從AbsListView中找到一個相對來說比較通用的方法boolean
trackMotionScroll(int deltaY, int incrementalDeltaY)
來達到控制滑動距離的目的。
以後可能用得上。
禁止fling思路之中的一個——切斷事件鏈
整整困擾了我三天,既然不能從正面剛,那從側面試試看吧。於是開始查詢事件分發機制、fling的實現方式、fling的觸發條件、scroll的相關內容...當中比較實用的是
- fling的觸發是一系列的ACTIONXXX事件結合的結果,用戶觸摸屏幕後高速滑動,出現一個ACTIONDOWN,多個ACTIONMOVE和一個ACTIONUP相結合。鏈接-Android: 觸屏fling/scroll/drag的差別及其具體過程
- 觸摸事件分發就比較復雜了,主要涉及到三個重要的方法,各自是
dispatchTouchEvent
涉及事件分發,onInterceptTouchEvent
涉及事件攔截和onTouchEvent
涉及事件處理。遇到幾次和這個相關的問題,每次都得查看相關的知識點...總是記不住。
鏈接-Android 觸摸事件分發ViewGroup&View多看幾遍就看懂了。
VelocityTracker
這個類是用來測量滑動速度的。鏈接-手勢事件:滑動動速度跟蹤類VelocityTracker介紹
- 在
AbsListView
中,滑動達到了一定的速度,就會觸發fling。而在AbsListView
的源代碼中是由一個runnable
的自己定義類來完畢的。太復雜所以沒怎麽看。可是有空了是一定要細致研究的。
那麽從上面這幾點能夠總結出一個結論,就是fling是由touch事件來控制的。而touch事件是由view&viewGroup來進行分發、攔截、對應的。今天早上起床的一個念頭就是解決這個問題的方法:
攔截掉touch事件來從源頭上打破觸發fling的事件鏈,從而達到禁止fling的目的
說幹就幹,到了公司。找到問題發生的listView。找到容納該listView的viewGroup(是一個LinearLayout的子類)。將那三個touch函數寫下了,打印log分析,從哪裏攔截,攔截哪個事件比較好。
終於決定在dispatchTouchEvent
中攔截。在ACTION_UP
中推斷當前滑動在y(縱軸)方向上的速度,當速度達到某一個閥值後,return
false
,也就是將能夠出發fling的事件鏈中最後一個事件給消滅掉。不傳給子view。
試了一下。果然好使!
總結
對公司項目代碼還是不熟,大部分事件都花在定位問題代碼上面了。再加上解決這個問題的積累還是不夠。不能從根本上考慮,處處碰壁之後才想到從源頭上來解決。
事實上我感覺解決這個問題的根本還是看一個人的知識積累和知識整合的能力。“熟讀唐詩三百首”就是這個道理。
額外補充
- 鏈接- Android View的onTouchEvent和OnTouch差別
onTouch
方法和onTouchEvent
方法還是不同的,前者是OnTouchListener
接口中的方法。而後者是view中的方法,並且前者比後者的優先級要高。
8月18日補充
提交代碼後。發現效果還是不太理想。
因為在事件分發的時候截斷了事件鏈,導致在手指滑動屏幕過程中。速度一旦過快,屏幕就會停止響應,卡頓效果甚至比之前還要嚴重。而原本預期的效果應該是這樣:
低速滑動能夠觸發fling。
快速滑動,能夠滑動可是fling效果不明顯,即有一種手指離開馬上停止的效果。
無奈之下又返回頭來看源代碼,有之前查找的資料做鋪墊,在AbsListView
這個類中尋找和fling相關的代碼塊,細致搜索之下,有所發現。
改進過程——利用反射來從源代碼的角度解決fling
在AbsListView
類中有例如以下成員變量,和fling有關。
private FlingRunnable mFlingRunnable;
這是一個Runnable
類型的對象,其內是用於對ListView運行fling的運動。包含其開始、運動、遇到邊界、結束等等操作。private int mMinimumVelocity;
觸發fling的最低速度,其賦值語句為mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
,而在configuration
這個類中,相關的值被定義為50,即每秒在屏幕上運動50dip單位。private int mMaximumVelocity;
和fling的最快速度、運動距離相關,賦值和上句類似,值為8000。velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity)
,前一個參數表示速率的基本時間單位。單位為毫秒。在此即為1秒。第二個參數表示速率超過mMaximumVelocity
的都依照mMaximumVelocity
的值來計算。private float mVelocityScale = 1.0f;
與fling的最遠距離相關,最遠距離的計算公式為velocityTracker.getYVelocity(mActivePointerId) * mVelocityScale
,就是實際速度與mVelocityScale
的乘積。
那麽知道了上面的一些屬性之後,就能夠在代碼中動態的改動這些值,來實際的控制fling的觸發速度、fling的最遠移動距離等效果了。
可是註意一點,這些熟悉都是私有屬性,所以還須要通過反射來獲取AbsListView
這個抽象父類進行改動。
以下上相關代碼。
public void initFlingArgument(ListView listView,int maxVelocity,int minVelocity,double scale){
if(listView == null){
return null;
}
/*獲取父類。假設是所要改動的類是ListView的子類的話,再獲取一次父類就可以*/
Class cls = listView.getSuperclass();
if(cls == AbsListView.class){
try{
//fling的最小響應速度
Field f_min = AbsListView.class.getDeclaredField("mMinimumVelocity");
//fling最大響應速度
Field f_max = AbsListView.class.getDeclaredField("mMaximumVelocity");
//fling scale
Field f_scale = AbsListView.class.getDeclaredField("mVelocityScale");
//設置屬性可達
f_min.setAccessible(true);
f_max.setAccessible(true);
f_scale.setAccessible(true);
//設置詳細值
f_max.set(listView,maxVelocity);
f_min.set(listView,minVelocity);
f_scale.set(listView,scale);
}catch(NoSuchFieldException e){
e.printStackTrace();
}catch(IllegalArgumentException e){
e.printStackTrace();
}catch(IllegalAccessException e){
e.printStackTrace();
}
}//end if
}//end method
總結
這下應該幾乎相同了吧...
8月20日補充
仍然不符合要求。在真機上測試,流暢程度仍然不盡人意。昨天做完導師分配的還有一個任務後。接著研究這個。
新的要求
有可能是前期沒有把需求搞明確。之後提出的要求變成了這樣:低速滑動,有fling的效果。快速滑動手指。listView仍然能夠滾動一段距離。然後停止,註意是停止。不是逐漸停止(也就是說停止fling)。
我的思考
相比於之前的實現,新的要求在於滾動一段距離後馬上停止。不同意有減速停止的效果。那麽解決問題的關鍵還是關於fling的實現上,或者還有一個辦法就是直接不使用fling,在高速滑動。手指離開屏幕後(觸發ACTION_UP)。用代碼操作ListView進行滾動。
- 思路1:在源代碼中找找有沒有控制fling時間的參數或者方法。
- 思路2:在豎直速度超過預定的閥值後。斷掉fling的事件鏈(為什麽不用上一次的方法來讓
mVelocityScale
這個屬性值為0呢,我的考慮是滑動是一個不斷觸發的過程,被調用的頻率非常高。而反射的效率非常低,所以會影響界面的響應,或者其它的副作用,不如直接斷開事件鏈效率高),然後調用ListView的smoothScrollXXXX()
系列的方法來進行自主控制的滾動。 - 思路3:之前在網上看過一篇關於Scroller的介紹和基本運用,就想著能不能用Scroller的fling或者startScroll方法,鏈接- android scroller overscroller使用方法。
解決途徑
實際動手開始寫代碼,發現和想象的根本不是一回事。
之前想的被一一排除。
- 排除思路1:源代碼中控制fling的是一個
Runnable
類,並沒有fling時間的相關設置或者參數,是由start(int)
方法和endFling()
方法來分別啟動和結束fling的。 - 排除思路2:並沒有什麽卵用。可能是用的方法不正確,可是沒有試出結果來...只是有個方法叫
setSelection(int)
能夠直接把可視範圍確定在給定序號的item上,可是這沒有滾動效果... - 排除思路3:沒有細致看博客說明...scroller控制的是view的內容,並非view本身,導致我參數設置。一調用方法,listView的內容就不見了...和滾動是兩個概念。
最後解決:既然之前能用反射把fling的參數設置了,那麽也能夠調用AbsListView
裏面的FlingRunnable
對象的endFling()
方法。再延時個幾百毫秒的調用一下,停止掉fling。
相關代碼例如以下:
private Field mFlingEndField = null;
private Method mFlingEndMethod = null;
private Handler mStopFlingHandler = new Handler();
//放於初始化方法中被調用
public void stopFlingInit(){
try{
mFlingEndField = AbsListView.class.getDeclaredField("mFlingRunnable");
mFlingEndField.setAccessible(true);
mFlingEndMethod = mFlingEndField.getType().getDeclaredMethod("endFling");
mFlingEndMethod.setAccessible(true);
}catch(NoSuchFieldException e){
e.printStackTrace();
}catch(NoSuchMethodException e){
e.printStackTrace();
}catch(IllegalArgumentException e){
e.printStackTrace();
}catch(IllegalAccessException e){
e.printStackTrace();
}
}
//在須要停止fling的地方調用
public void stopFling(ListView listView){
if(mFlingEndMethod != null){
try{
mFlingEndMethod.invoke(mFlingEndField.get(listView));
}cache(InvocationTargetException e){
e..printStackTrace();
}
}
}
ListView listView = this;/*這些我都是寫在自己定義的ListView內部的*/
Runnable mStopFilingRunnable = new Runnable(){
public void run(){
stopFling(listView);
}
}
//用法,300毫秒後停止fling
mStopFlingHandler.postDelayed(mStopFilingRunnable,300);
結束
虛擬機上達到要求,可是真機上是不是還是然並卵...這就不知道了。
但我有種預感,這事沒完。
android問題及其解決-優化listView卡頓和怎樣禁用ListView的fling