Android開發筆記——ListView模塊、緩存及性能


ListView是Android開發中最常用的組件之一。本文將重點說明如何正確使用ListView,以及使用過程中可能遇到的問題。

  1. ListView開發模塊
  2. 圖片緩存
  3. 可能遇到的問題

 

一、ListView開發模塊

  從項目實踐的角度來看,ListView適合“自底向上”的開發模式,即從每個條目的顯示組件,到對其進行控制的數據結構,最后通過Activity等進行使用。主要包括以下模塊:

1、首先是item組件,即用於每項布局輸出的xml文件。Android SDK中有simple_list_item_1、simple_list_item_2可用,當需要比較豐富的顯示效果時,一般通過自定義xml實現。本文以論壇的格式進行說明,主要包括發帖人頭像、用戶名,帖子的標題、內容、最后回復時間、編輯、收藏、回復等內容,布局文件比較簡單,這里截取其中一項顯示,用以說明:

這里寫圖片描述

2、其次是父對象layout文件,即用於Activity或者Fragment的布局輸出文件,一般在此輸出文件中包含ListView。當然,如果采用ListFragment或ListActivity,並不需要再顯示的定義ListView組件。本文中采用Fragment默認的輸出文件,當然,也可以采用自定義的布局文件。

1 <ListView
2     android:id="@+id/topic_list"
3     android:layout_width="fill_parent"
4     android:layout_height="fill_parent"
5     android:cacheColorHint="@android:color/transparent"
6     android:divider="@color/topic_divider_color"
7     android:dividerHeight="1px"
8     android:listSelector="@android:color/transparent" >
9 </ListView>

3、定義數據結構(容器),即用於持有單個Item的數據,可以是簡單的String,也可以通過抽象Items所需字段組成一個類,抽象的原則是與Item中的組件對應。本文中上圖涉及多個字段,因此通過抽象組件形成BBSTopicItem類。

4、列表適配器。決定每行Item中具體顯示什么內容,以怎樣的樣式顯示等,通常通過繼承ArrayAdapter、SimpleAdapter等實現。本文定義BBSTopicAdapter,繼承於ArrayAdapter<BBSTopicItem>。

5、最后,需要定義一個Activity或Fragment來使用上述模塊。需要說明的是,ListView可以直接被ListActivity或者ListFragment使用。

以上五個模塊就是使用ListView的基本邏輯框架,開發過程中,需要時刻理清它們之間的關系。


 

二、圖片緩存及相關

  在ListView中顯示圖片是比較常見的應用場景,但加載圖片一般需要通過緩存來進行處理。由於虛擬機的heapsize默認為16M:

AndroidRuntime.cpp

int AndroidRuntime::startVM(JavaVM** pJavaVM, JNIEnv** pEnv) {
    ……
    property_get("dalvik.vm.heapsize", heapsizeOptsBuf+4, "16M");
    ……
}

  (廠商一般會修改為32M,后面會說到這個數值)

  在操作大尺寸圖片時無法分配所需內存,就會引起OOM。因此,使用LruCache來緩存圖片是常見的做法。但在其使用過程中,也需要注意一些問題,比如使用線程池下載圖片,使用SD卡緩存,ListView滑動流暢性,圖片顯示錯亂等,下面對這些問題進行說明。

1、首先是LruCache,該類在android-support-v4的包中提供

  主要算法原理是把最近使用的對象用強引用存儲在 LinkedHashMap 中,並且把最近最少使用的對象在緩存值達到預設定值之前從內存中移除。

  以前經常會使用一種非常流行的內存緩存技術的實現,即軟引用或弱引用 (SoftReference or WeakReference)。但是現在已經不再推薦使用這種方式了,因為從 Android 2.3 (API Level 9)開始,垃圾回收器會更傾向於回收持有軟引用或弱引用的對象,這讓軟引用和弱引用變得不再可靠。另外,Android 3.0 (API Level 11)中,圖片的數據會存儲在本地的內存當中,因而無法用一種可預見的方式將其釋放,這就有潛在的風險造成應用程序的內存溢出並崩潰。

  那么,怎樣確定一個合適的緩存大小給LruCache呢?有以下多個因素應該放入考慮范圍內,例如:

  • 設備可以為每個應用程序分配多大的內存?
  • 設備屏幕上一次最多能顯示多少張圖片?有多少圖片需要進行預加載?
  • 設備的屏幕大小和分辨率分別是多少?一個超高分辨率的設備(例如 Galaxy Nexus) 比起一個較低分辨率的設備(例如 Nexus S),在持有相同數量圖片的時候,需要更大的緩存空間。
  • 圖片的尺寸和大小,還有每張圖片會占據多少內存空間?
  • 圖片被訪問的頻率有多高?是否有一些圖片的訪問頻率比其它圖片要高?如果有的話,應該讓一些圖片常駐在內存當中,或者使用多個LruCache 對象來區分不同組的圖片。
  • 是否能維持好數量和質量之間的平衡嗎?有些時候,存儲多個低像素的圖片,而在后台去開線程加載高像素的圖片會更加的有效。

  並沒有一個指定的緩存大小可以滿足所有的應用程序,通常需要分析程序內存的使用情況,然后制定出一個合適的解決方案。緩存太小,有可能造成圖片頻繁地被釋放和重新加載;而緩存太大,則有可能還是會引起 java.lang.OutOfMemory 異常。

  不過,讀者可能會在不同的地方遇到類似下面這段定義LruCache的代碼:

 1 public ImageDownLoader(Context context){
 2     int maxMemory = (int) Runtime.getRuntime().maxMemory();  
 3     int mCacheSize = maxMemory / 8;
 4     mMemoryCache = new LruCache<String, Bitmap>(mCacheSize){
 5 
 6         @Override
 7         protected int sizeOf(String key, Bitmap value) {
 8             return value.getRowBytes() * value.getHeight();
 9         }
10     };
11 }

  其實,這里的maxMemory / 8是一個經驗值,上述提到廠商一般會設置虛擬機的heapsize為32M,那么1/8就是4M,這也是綜合前面所提到的影響因素得出的一個常用值吧。如果在你的應用中出現問題,那么還是回到上述影響因素中逐一分析,得到適合自己應用的值才是最佳的做法。

2、使用線程池管理圖片下載任務

  線程池自然是為了限制系統中執行線程的數量,通常的一種比較低效的做法是為每一張圖片下載開啟一個新線程(new thread),線程的創建和銷毀將造成極大的性能損耗,而對服務器來講,維護過多的線程將造成內存消耗過大。總的來講,使用線程池是執行此類任務的一個基本做法。Java中線程池的頂級接口是Executor,但是嚴格意義上講Executor並不是一個線程池,而只是一個執行線程的工具,真正的線程池接口是ExecutorService。配置線程池是略顯復雜,尤其是對於線程池的原理不是很清楚的情況下,很有可能配置的線程池不是最優的。在Executors類里提供了一些靜態工廠,生成一些常用的線程池。

  • newSingleThreadExecutor 創建一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因為異常結束,那么會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。
  • newFixedThreadPool 創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因為執行異常而結束,那么線程池會補充一個新線程。
  • newCachedThreadPool 創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程, 
    那么就會回收部分空閑(60秒不執行任務)的線程,當任務數增加時,此線程池又可以智能的添加新線程來處理任務。此線程池不會對線程池大小做限制,線程池大小完全依賴於操作系統(或者說JVM)能夠創建的最大線程大小。
  • newScheduledThreadPool創建一個大小無限的線程池。此線程池支持定時以及周期性執行任務的需求。

  通過下面的代碼創建固定大小的線程池:

 1 private ExecutorService getThreadPools(){
 2     if(mImageThreadPool == null){
 3         synchronized(ExecutorService.class){
 4             if(mImageThreadPool == null){
 5                 mImageThreadPool = Executors.newFixedThreadPool(4);
 6             }
 7         }
 8     }
 9     return mImageThreadPool;
10 }
11 
12 (本段代碼來自互聯網)

  對於固定大小的線程池,關鍵是需要根據實際應用場景設置線程數量,既快速有效的執行下載任務,又不造成資源浪費。

3、SD卡存儲配合LruCache

  使用SD卡存儲下載的圖片有多方面的好處,提高圖片加載速度(從本地加載肯定比網絡要快)、節約用戶流量等,因此,除了LruCache,一般還會將圖片存儲在本地SD卡。因此,加載圖片的順序應該是

  a. 首先從LruCache中獲取圖片; 
  b. 如果a的返回值為null,則檢查SD卡是否存在圖片; 
  c. 如果a、b的返回值都為null,則通過網絡進行下載。

  用代碼表述上述邏輯為:

1 Bitmap bitmap;
2 if (getBitmapFromMemCache(url) != null) {
3     bitmap = getBitmapFromMemCache(url);
4 } else if (fileUtils.isFileExists(url) && fileUtils.getFileSize(url) != 0) {
5     bitmap = fileUtils.getBitmapFromSD(url);
6 } else {
7     bitmap = getBitmapFormUrl(url);
8 }
9 return bitmap;

  Tip:如果通過HttpURLConnection下載圖片,需要注意一個小問題,如果設置HttpURLConnection對象的DoOutput屬性為true(con.setDoOutput(true)),在Android4.0以后,會解析為post請求,導致filenotfound異常(獲取圖片應該是get請求)。

4、ListView滑動停止下載

  滑動時停止下載也是提高用戶體驗的方式之一,因為如果在ListView滑動過程中執行下載任務,將會使得ListView出現卡頓。監聽滑動狀態改變的方法是onScrollStateChanged(AbsListView view, int scrollState),該方法在OnScrollListener接口中定義的,而OnScrollListener是AbsListView中為了在列表或網格滾動時執行回調函數而定義的接口。(強烈建議做ListView相關應用的讀者熟悉一下AbsListView的源碼)

  為了實現下載任務與滑動狀態的關聯,在自定義列表適配器中實現了OnScrollListener接口,在onScrollStateChanged方法中根據scrollState執行相應的下載任務操作。

 1 @Override
 2 public void onScrollStateChanged(AbsListView view, int scrollState) {
 3     this.scrollState = scrollState;
 4     if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
 5         showImage(mFirstVisibleItem, mVisibleItemCount);
 6     } else {
 7         cancelTask();
 8     }
 9 }
10 
11 (本段代碼來自互聯網)

5、圖片顯示錯亂

  這是一個比較老生常談的問題,在百度搜索一下“listview 圖片錯位”會見到一大片帖子在討論這個問題,這里不再贅述,推薦幾個比較靠譜鏈接:

http://www.cnblogs.com/lesliefang/p/3619223.html 
http://www.trinea.cn/android/android-listview-display-error-image-when-scroll/ 
http://blog.csdn.net/shineflowers/article/details/41744477


  總的來講,圖片緩存是Android開發中比較有意思的一個話題,常用的圖片緩存開源庫有ImageLoader、Picasso、Glide等,最近由Facebook開源了Fresco(http://www.fresco-cn.org/),根據介紹,它能夠從網絡、本地存儲和本地資源中加載圖片。同時,為了節省數據和CPU,它擁有三級緩存。此外,Fresco在顯示方面是用了Drawees,可以顯示占位符,直到圖片加載完成。而當圖片從屏幕上消失時,會自動釋放圖片所占的內存。這里推薦一個關於Android三大圖片緩存原理、特性對比的鏈接:

http://www.csdn.net/article/2015-10-21/2825984#rd


 

三、可能遇到的問題

1、notifyDataSetChanged與局部更新

  首先舉一個栗子:QQ空間或者朋友圈的點贊功能,點贊之后頁面會馬上刷新,但不會影響本條目以外的其他條目的顯示。換句話說,它使用了局部更新,而非notifyDataSetChanged。在了解notifyDataSetChanged與局部更新區別時,需要先對以下問題作出解釋:

  • notifyDataSetChanged如何刷新界面?
  • 什么場景需要使用notifyDataSetChanged?什么場景需要使用局部更新?
  • 局部更新如何實現?

  回到代碼中,notifyDataSetChanged是在BaseAdapter中定義的,首先初始了一個DataSetObservable類的final實例mDataSetObservable:

private final DataSetObservable mDataSetObservable = new DataSetObservable();

  notifyDataSetChanged就是通過操作mDataSetObservable實現的,DataSetObservable是觀察者模式的一個實現(Android源碼中有很多類似設計模式的實現)。

 1 public void registerDataSetObserver(DataSetObserver observer) {
 2     mDataSetObservable.registerObserver(observer);
 3 }
 4 
 5 public void unregisterDataSetObserver(DataSetObserver observer) {
 6     mDataSetObservable.unregisterObserver(observer);
 7 }
 8 
 9 /**
10  * Notifies the attached observers that the underlying data has been changed
11  * and any View reflecting the data set should refresh itself.
12  */
13 public void notifyDataSetChanged() {
14     mDataSetObservable.notifyChanged();
15 }
16 
17 /**
18  * Notifies the attached observers that the underlying data is no longer valid
19  * or available. Once invoked this adapter is no longer valid and should
20  * not report further data set changes.
21  */
22 public void notifyDataSetInvalidated() {
23     mDataSetObservable.notifyInvalidated();
24 }

  notifyDataSetChanged調用了notifyChanged方法,回到DataSetObservable中:

 1 /**
 2  * Invokes {@link DataSetObserver#onChanged} on each observer.
 3  * Called when the contents of the data set have changed.  The recipient
 4  * will obtain the new contents the next time it queries the data set.
 5  */
 6 public void notifyChanged() {
 7     synchronized(mObservers) {
 8         // since onChanged() is implemented by the app, it could do anything, including
 9         // removing itself from {@link mObservers} - and that could cause problems if
10         // an iterator is used on the ArrayList {@link mObservers}.
11         // to avoid such problems, just march thru the list in the reverse order.
12         for (int i = mObservers.size() - 1; i >= 0; i--) {
13             mObservers.get(i).onChanged();
14         }
15     }
16 }
17 
18 /**
19  * Invokes {@link DataSetObserver#onInvalidated} on each observer.
20  * Called when the data set is no longer valid and cannot be queried again,
21  * such as when the data set has been closed.
22  */
23 public void notifyInvalidated() {
24      synchronized (mObservers) {
25          for (int i = mObservers.size() - 1; i >= 0; i--) {
26              mObservers.get(i).onInvalidated();
27          }
28      }
29  }

  notifyChanged也只是調用了其綁定的接口,並沒有具體的實現,那么這個接口是什么時候綁定的呢?回憶ListView與adapter的關聯是何時開始的呢?setAdapter!是的,從setAdapter的代碼中可以看到這種關聯。

 1 /**
 2  * Sets the data behind this ListView.
 3  *
 4  * The adapter passed to this method may be wrapped by a {@link WrapperListAdapter},
 5  * depending on the ListView features currently in use. For instance, adding
 6  * headers and/or footers will cause the adapter to be wrapped.
 7  *
 8  * @param adapter The ListAdapter which is responsible for maintaining the
 9  *        data backing this list and for producing a view to represent an
10  *        item in that data set.
11  *
12  * @see #getAdapter() 
13  */
14 @Override
15 public void setAdapter(ListAdapter adapter) {
16     if (mAdapter != null && mDataSetObserver != null) {
17         mAdapter.unregisterDataSetObserver(mDataSetObserver);
18     }
19 
20     resetList();
21     mRecycler.clear();
22 
23     if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
24         mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
25     } else {
26         mAdapter = adapter;
27     }
28 
29     mOldSelectedPosition = INVALID_POSITION;
30     mOldSelectedRowId = INVALID_ROW_ID;
31 
32     // AbsListView#setAdapter will update choice mode states.
33     super.setAdapter(adapter);
34 
35     if (mAdapter != null) {
36         mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
37         mOldItemCount = mItemCount;
38         mItemCount = mAdapter.getCount();
39         checkFocus();
40         // 原來是在這里綁定了數據改變的觀察者對象
41         mDataSetObserver = new AdapterDataSetObserver();
42         mAdapter.registerDataSetObserver(mDataSetObserver);
43 
44         mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
45 
46         int position;
47         if (mStackFromBottom) {
48             position = lookForSelectablePosition(mItemCount - 1, false);
49         } else {
50             position = lookForSelectablePosition(0, true);
51         }
52         setSelectedPositionInt(position);
53         setNextSelectedPositionInt(position);
54 
55         if (mItemCount == 0) {
56             // Nothing selected
57             checkSelectionChanged();
58         }
59     } else {
60         mAreAllItemsSelectable = true;
61         checkFocus();
62         // Nothing selected
63         checkSelectionChanged();
64     }
65 
66     requestLayout();
67 }

  前面提到,觀察者對象調用的onChanged方法,可以確定,上述綁定的AdapterDataSetObserver中必然有onChanged方法的實現。

 1 public void onChanged() { 
 2     mDataChanged = true;
 3     mOldItemCount = mItemCount;
 4     mItemCount = getAdapter().getCount();
 5 
 6     if ((getAdapter().hasStableIds()) && 
 7         (mInstanceState != null) && 
 8         (mOldItemCount == 0) && 
 9         (mItemCount > 0)) {
10         onRestoreInstanceState(mInstanceState);
11         mInstanceState = null;
12     } else {
13         rememberSyncState();
14     }
15     checkFocus();
16     requestLayout();
17 }

  很明顯,在onChanged的末尾調用了requestLayout方法,而requestLayout方法是用來繪制界面的,定義在View中。

 1 /**
 2  * Call this when something has changed which has invalidated the
 3  * layout of this view. This will schedule a layout pass of the view
 4  * tree. This should not be called while the view hierarchy is currently in a layout
 5  * pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the
 6  * end of the current layout pass (and then layout will run again) or after the current
 7  * frame is drawn and the next layout occurs.
 8  *
 9  * <p>Subclasses which override this method should call the superclass method to
10  * handle possible request-during-layout errors correctly.</p>
11  */
12 public void requestLayout() {
13     if (mMeasureCache != null) mMeasureCache.clear();
14 
15     if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
16         // Only trigger request-during-layout logic if this is the view requesting it,
17         // not the views in its parent hierarchy
18         ViewRootImpl viewRoot = getViewRootImpl();
19         if (viewRoot != null && viewRoot.isInLayout()) {
20             if (!viewRoot.requestLayoutDuringLayout(this)) {
21                 return;
22             }
23         }
24         mAttachInfo.mViewRequestingLayout = this;
25     }
26 
27     mPrivateFlags |= PFLAG_FORCE_LAYOUT;
28     mPrivateFlags |= PFLAG_INVALIDATED;
29 
30     if (mParent != null && !mParent.isLayoutRequested()) {
31         mParent.requestLayout();
32     }
33     if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
34         mAttachInfo.mViewRequestingLayout = null;
35     }
36 }

  根據上述解釋,會發現notifyDataSetChanged會通知View刷新所有與其綁定的數據列表,而某些局部操作明顯不需要全部刷新,全局刷新會造成極大的資源浪費。在這種情況下,就需要進行局部更新。

  局部更新的實現定義在是適配器(adapter)中,根據指定的index(即item在listview中的位置),實現指定條目內容的更新:

 1 /** 
 2  * 局部刷新
 3  * @param index item在listview中的位置 
 4  */  
 5 public void updateItem(int index) {  
 6     if (listView == null) {  
 7         return;  
 8     }
 9     // 停止滑動時才更新界面
10     if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
11         // 指定更新的位置在可見范圍之內
12         if (index >= listView.getFirstVisiblePosition() && 
13             index <= listView.getLastVisiblePosition()) {
14             // 獲取當前可以看到的item位置  
15             int visiblePosition = listView.getFirstVisiblePosition();
16             View view = listView.getChildAt(index - visiblePosition); 
17             //在這里對view中的組件進行設置,數據可以通過getItem(index)獲取//
18         }
19     }
20 }

(一個小問題:ListView的getCount()與getChildCount()有什么差別呢?)

 

  使用notifyDataSetChanged時,一個常見的問題就是調用了notifyDataSetChanged,但界面並沒有刷新。很常見的原因是list的指向改變了,換句話說,list指向了與初始化時不同的堆地址。這種情況比較常見,給一個說明的鏈接:http://www.tuicool.com/articles/aiiYzeR

一般的經驗是在聲明變量時對list進行初始化,當涉及數據改變時,通過add或者remove實現。

 

2、listview的item內部組件的事件響應

在具體的工程中,item組件的響應會根據對其使用的Activity(Fragment)的不同而變化,因此,不宜在其內部設定響應事件的具體實現。推薦在adapter中定義接口,將接口暴露給具體的Activity(Fragment),Activity(Fragment)根據具體的業務邏輯進行配置。以前面提到的論壇帖子為例,其包含以下操作:

1 /**
2  * 處理Item中控件的點擊事件接口
3  */
4 public interface ITopicItemOperation {
5     public void topicItemEdit(BBSTopicItem item);
6     public void topicItemCollect(BBSTopicItem item);
7     public void topicItemReply(BBSTopicItem item);
8 }

  然后, 在Activity(Fragment)中實現上述接口:

 1 /**
 2  * 編輯主題貼
 3  */
 4 @Override
 5 public void topicItemEdit(BBSTopicItem item) {
 6     // 業務邏輯
 7 }
 8 
 9 /**
10  * 收藏主題貼
11  */
12 @Override
13 public void topicItemCollect(BBSTopicItem item) {
14     // 業務邏輯
15 }
16 
17 /**
18  * 回復主題帖
19  */
20 @Override
21 public void topicItemReply(BBSTopicItem item) {
22     // 業務邏輯
23 }

  將該實現通過adapter的構造器進行傳遞,以響應點擊事件為例:

 1 /**
 2  * 處理ListView中控件的點擊事件
 3  */
 4 private class TopicItemOnClickListener implements OnClickListener {
 5 
 6     private BBSTopicItem item;
 7         
 8     public TopicItemOnClickListener(BBSTopicItem item) {
 9         this.item = item;
10     }
11         
12     @Override
13     public void onClick(View v) {
14         switch (v.getId()) {
15         case R.id.bbs_topic_edit:
16             topicItemOperation.topicItemEdit(item);
17             break;
18         case R.id.bbs_topic_collect:
19             topicItemOperation.topicItemCollect(item);
20             break;
21         case R.id.bbs_topic_reply:
22             topicItemOperation.topicItemReply(item);
23             break;
24         }
25     }
26 }            

  在點擊按鈕時,添加TopicItemOnClickListener對象,即可實現不同的Activity(Fragment)對該項功能的復用。

 

3、圖文混合顯示

  在涉及論壇帖子的時候,圖文混合顯示一種很常見的場景。Android中沒有原生支持圖文混合顯示的控件,github上有一些自定義控件能實現這種需求,百度一下也能發現很多。但此類個性化的需求需要很據項目實際來靈活運用,這里描述一種通過正則來處理的方法。比如,服務端返回的帖子內容如下:

“全新寶馬7系上市了,是不是很有氣勢?http://img2.tuohuangzu.com/THZ/UserBlog/0/15/2015061810510550085.jpg不過相比於7系,我還是更喜歡3系的操控,轉向非常精確,而且過彎姿勢的建立也是非常恰到好處,過彎姿勢建立的過早過晚都不好。過早會導致操控措手不及,無法感覺方向打多少,在匝道,有時打多了要在回,回多了又要打。過晚會導致路感缺失,側傾明顯,虛的慌http://res3.auto.ifeng.com/s/6606/0/3/13309355849880_3.jpghttp://img1.cheshi-img.com/product/1_1024/887/4b25a8df6e8ec.jpg明天天氣不錯,去自駕游如何?

 帖子內容為純文本格式,顯示時需要從中提取出圖片的鏈接。這時正則就派上用場了:

 1 Pattern p = Pattern.compile("http://[^\\u4e00-\\u9fa5]*?[.]jpg");
 2 Matcher m = p.matcher(text);
 3             
 4 int lastTextIndex = 0;
 5 while (m.find()) {
 6     // 設置文本顯示
 7     String textFrag = text.substring(lastTextIndex, m.start());
 8     if (!textFrag.isEmpty()) {
 9         layout.addView(getTextView(context, textFrag));
10     }
11     // 更新最后文本下標
12     lastTextIndex = m.end();
13                 
14     // 設置圖片顯示
15     String imageUrl = m.group();
16     ImageView imageView = getImageView(context);
17     setImageViewDisplay(imageView, imageUrl);
18     layout.addView(imageView);
19 }
20 if (lastTextIndex < text.length()) {
21     String textFrag = text.substring(lastTextIndex, text.length());
22     layout.addView(getTextView(context, textFrag));
23 }    

  這段代碼比較簡單,只有一處需要說明。在上述帖子內容匯總,后面兩張圖片的鏈接是連續的,當正則表達式中包含能接受重復的限定符時,通常的行為是(在使整個表達式能得到匹配的前提下)匹配盡可能多的字符。以這個表達式為例:a.*b,它將會匹配最長的以a開始,以b結束的字符串。如果用它來搜索aabab的話,它會匹配整個字符串aabab。這被稱為貪婪匹配。有時,我們更需要懶惰匹配,也就是匹配盡可能少的字符。前面給出的限定符都可以被轉化為懶惰匹配模式,只要在它后面加上一個問號?。這樣.*?就意味着匹配任意數量的重復,但是在能使整個匹配成功的前提下使用最少的重復。因此,在上述場景中,想要將連續的兩個URL匹配成功,則需要進行懶惰匹配。

4、ScrollView與ListView的沖突

  如果在ScrollView中嵌套了ListView(原則上應盡量避免這種情況),那么很不幸,可能會遇到以下問題:

  • ListView只顯示一行
  • 頁面默認不從頂端開始顯示
  • ListView滑動事件無法監聽

  這幾個問題都是比較常見的問題了,這里不再贅述其原理,給出比較通用的解決方案:

  1、listview需要手動設置高度,這里給出一個鏈接:http://www.cnblogs.com/zhwl/p/3333585.html

  2、listview需要設置listview.setFocusable(false);

  3、重載listview的onInterceptTouchEvent方法,在ACTION_DOWN時通過ScrollView的requestDisallowInterceptTouchEvent方法設置交出ontouch權限,ACTION_CANCEL時再恢復ontouch權限。

  再次強調,應盡量避免ScrollView中嵌套了ListView。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM