1、android:clipToPadding
意思是控制元件的繪製區域是否在padding裡面。預設為true。如果你設定了此屬性值為false,就能實現一個在佈局上事半功陪的效果。先看一個效果圖。
上圖中的ListView頂部預設有一個間距,向上滑動後,間距消失,如下圖所示。
如果使用margin或padding,都不能實現這個效果。加一個headerView又顯得大材小用,而且過於麻煩。此處的clipToPadding配合paddingTop效果就剛剛好。
同樣,還有另外一個屬性也很神奇:android:clipChildren,具體請參考:【Android】神奇的android:clipChildren屬性
2、match_parent和wrap_content
按理說這兩個屬性一目瞭然,一個是填充佈局空間適應父控制元件,一個是適應自身內容大小。但如果在列表如ListView中,用錯了問題就大了。ListView中的getView方法需要計算列表條目,那就必然需要確定ListView的高度,onMesure才能做測量。如果指定了wrap_content,就等於告訴系統,如果我有一萬個條目,你都幫我計算顯示出來,然後系統按照你的要求就new了一萬個物件出來。那你不悲劇了?先看一個圖。
假設現在ListView有8條資料,match_parent需要new出7個物件,而wrap_content則需要8個。這裡涉及到View的重用,就不多探討了。所以這兩個屬性的設定將決定getView的呼叫次數。
由此再延伸出另外一個問題:getView被多次呼叫。
什麼叫多次呼叫?比如position=0它可能呼叫了幾次。看似很詭異吧。GridView和ListView都有可能出現,說不定這個禍首就是wrap_content。說到底是View的佈局出現了問題。如果巢狀的View過於複雜,解決方案可以是通過程式碼測量列表所需要的高度,或者在getView中使用一個小技巧:parent.getChildCount == position
1 2 3 4 5 6 7 8 |
@Override public View getView(int position, View convertView, ViewGroup parent) { if (parent.getChildCount() == position) { // does things here } return convertView; } |
3、IllegalArgumentException: pointerIndex out of range
出現這個Bug的場景還是很無語的。一開始我用ViewPager + PhotoView(一個開源控制元件)顯示圖片,在多點觸控放大縮小時就出現了這個問題。一開始我懷疑是PhotoView的bug,找了半天無果。要命的是不知如何try,老是crash。後來才知道是android遺留下來的bug,原始碼裡沒對pointer index做檢查。改原始碼重新編譯不太可能吧。明知有exception,又不能從根本上解決,如果不讓它crash,那就只能try-catch了。解決辦法是:自定義一個ViewPager並繼承ViewPager。請看以下程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/** * 自定義封裝android.support.v4.view.ViewPager,重寫onInterceptTouchEvent事件,捕獲系統級別異常 */ public class CustomViewPager extends ViewPager { public CustomViewPager(Context context) { this(context, null); } public CustomViewPager(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { try { return super.onInterceptTouchEvent(ev); } catch (IllegalArgumentException e) { LogUtil.e(e); } catch (ArrayIndexOutOfBoundsException e) { LogUtil.e(e); } return false; } } |
把用到ViewPager的佈局檔案,替換成CustomViewPager就OK了。
4、ListView中item點選事件無響應
listView的Item點選事件突然無響應,問題一般是在listView中加入了button、checkbox等控制元件後出現的。這個問題是聚焦衝突造成的。在android裡面,點選螢幕之後,點選事件會根據你的佈局來進行分配的,當你的listView裡面增加了button之後,點選事件第一優先分配給你listView裡面的button。所以你的點選Item就失效了,這個時候你就要根據你的需求,是給你的item的最外層layout設定點選事件,還是給你的某個佈局元素新增點選事件了。
解決辦法:在ListView的根控制元件中設定(若根控制元件是LinearLayout, 則在LinearLayout中加入以下屬性設定)descendantFocusability屬性。
1 |
android:descendantFocusability="blocksDescendants" |
官方文件也是這樣說明。
5、getSupportFragmentManager()和getChildFragmentManager()
有一個需求,Fragment需要巢狀3個Fragment。基本上可以想到用ViewPager實現。開始程式碼是這樣寫的:
1 |
mViewPager.setAdapter(new CustomizeFragmentPagerAdapter(getActivity().getSupportFragmentManager(), subFragmentList)); |
導致的問題是巢狀的Fragment有時會莫名其妙不顯示。開始根本不知道問題出現在哪,當你不知道問題的原因時,去解決這個問題顯然比較麻煩。經過一次又一次的尋尋覓覓,終於在stackoverflow上看到了同樣的提問。說是用getChildFragmentManager()就可以了。真是這麼神奇!
1 |
mViewPager.setAdapter(new CustomizeFragmentPagerAdapter(getChildFragmentManager, subFragmentList)); |
讓我們看一下這兩個有什麼區別。首先是getSupportFragmentManager(或者getFragmentManager)的說明:
1 |
Return the FragmentManager for interacting with fragments associated with this fragment's activity. |
然後是getChildFragmentManager:
1 |
Return a private FragmentManager for placing and managing Fragments inside of this Fragment. |
Basically, the difference is that Fragment’s now have their own internal FragmentManager that can handle Fragments. The child FragmentManager is the one that handles Fragments contained within only the Fragment that it was added to. The other FragmentManager is contained within the entire Activity.
已經說得比較明白了。
6、ScrollView巢狀ListView
這樣的設計是不是很奇怪?兩個同樣會滾動的View居然放到了一起,而且還是巢狀的關係。曾經有一個這樣的需求:介面一共有4個區域部分,分別是公司基本資訊(logo、名稱、法人、地址)、公司簡介、公司榮譽、公司口碑列表。每部分內容都需要根據內容自適應高度,不能寫死。鄙人首先想到的也是外部用一個ScrollView包圍起來。然後把這4部分分別用4個自定義控制元件封裝起來。基本資訊和公司簡介比較簡單,榮譽需要用到RecyclerView和TextView的組合,RecyclerView(當然,用GridView也可以,3列多行的顯示)存放榮譽圖片,TextView顯示榮譽名稱。最後一部分口碑列表當然是ListView了。這時候,問題就出來了。需要解決ListView放到ScrollView中的滑動問題和RecyclerView的顯示問題(如果RecyclerView的高度沒法計算,你是看不到內容的)。
當然,網上已經有類似的提問和解決方案了。
給一個網址:
ListView的情況還比較好解決,優雅的做法無非寫一個類繼承ListView,然後重寫onMeasure方法。
1 2 3 4 |
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); super.onMeasure(widthMeasureSpec, expandSpec); } |
ListView可以重寫onMeasure解決,RecyclerView重寫這個方法是行不通的。
說到底其實計算高度嘛。有兩種方式,一種是動態計算RecycleView,然後設定setLayoutParams;另外一種跟ListView的解決方式類似,定義一個類繼承LinearLayoutManager或GridLayoutManager(注意:可不是繼承RecyclerView),重寫onMeasure方法(此方法比較麻煩,此處不表,下次寫一篇文章再作介紹)。
動態計算高度如下:
1 2 3 4 5 |
int heightPx = DensityUtil.dip2px(getActivity(), (imageHeight + imageRowHeight) * lines); MarginLayoutParams mParams = new MarginLayoutParams(LayoutParams.MATCH_PARENT, heightPx); mParams.setMargins(0, 0, 0, 0); LinearLayout.LayoutParams lParams = new LinearLayout.LayoutParams(mParams); honorImageRecyclerView.setLayoutParams(lParams); |
思路是這樣的:服務端返回榮譽圖片後,由於是3列顯示的方式,只需要計算需要顯示幾行,然後給定行間距和圖片的高度,再設定setLayoutParams就行了。
1 |
int lines = (int) Math.ceil(totalImages / 3d); |
至此,這個奇怪的需求得到了解決。
可是在滑動的時候,感覺出現卡頓的現象。聰明的你肯定想到是滑動衝突了。應該是ScrollView的滑動干擾到了ListView的滑動。怎麼辦呢?能不能禁掉ScrollView的滑動?
百度一下,你肯定能搜尋到答案的。先上程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
/** * @author Leo * * Created in 2015-9-12 * 攔截ScrollView滑動事件 */ public class CustomScrollView extends ScrollView { private int downY; private int touchSlop; public CustomScrollView(Context context) { this(context, null); } public CustomScrollView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CustomScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); } @Override public boolean onInterceptTouchEvent(MotionEvent e) { int action = e.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: downY = (int) e.getRawY(); break; case MotionEvent.ACTION_MOVE: int moveY = (int) e.getRawY(); if (Math.abs(moveY - downY) > touchSlop) { return true; } } return super.onInterceptTouchEvent(e); } } |
只要理解了getScaledTouchSlop()這個方法就好辦了。這個方法的註釋是:Distance in pixels a touch can wander before we think the user is scrolling。說這是一個距離,表示滑動的時候,手的移動要大於這個距離才開始移動控制元件,如果小於此距離就不觸發移動。
看似很完美了。
但是還有另外一個問題:我每次載入這個介面花的時間太長了,每次由其它介面啟動這個介面時,都要卡上1~2秒,而且因手機效能時間不等。並不是由於網路請求,取資料由子執行緒做,跟UI執行緒毫無關係。這樣的體驗自己看了都很不爽。
幾天過去了,還是那樣。馬上要給老闆演示了。這樣的體驗要被罵十次呀。
難道跟ScrollView的巢狀有關?
好吧,那我重構程式碼。不用ScrollView了。直接用一個ListView,然後add一個headerView存放其它內容。因為控制元件封裝得還算好,沒改多少佈局就OK了,一執行,流暢順滑,一切迎刃而解!
本來就是這麼簡單的問題,為什麼非得用ScrollView巢狀呢?
stackoverflow早就告訴你了,不要這樣巢狀!不要這樣巢狀!不要這樣巢狀!重要的事情說三遍。
ListView inside ScrollView is not scrolling on Android
當然,從android 5.0 Lollipop開始提供了一種新的API支援嵌入滑動,此時,讓像這樣的需求也能很好實現。
此處給一個網址,大家有興趣自行了解,此處不再討論。
7、EmojiconTextView的setText(null)
這是開源表情庫com.rockerhieu.emojicon中的TextView加強版。相信很多人用到過這個開源工具包。TextView用setText(null)完全沒問題。但EmojiconTextView setText(null)後就悲劇了,直接crash,顯示的是null pointer。開始我懷疑時這個view沒初始化,但並不是。那就除錯一下唄。
1 2 3 4 5 6 |
@Override public void setText(CharSequence text, BufferType type) { SpannableStringBuilder builder = new SpannableStringBuilder(text); EmojiconHandler.addEmojis(getContext(), builder, mEmojiconSize); super.setText(builder, type); } |
EmojiconTextView中的setText看來沒什麼問題。點SpannableStringBuilder進去看看,原始碼原來是這樣的:
1 2 3 4 5 6 7 |
/** * Create a new SpannableStringBuilder containing a copy of the * specified text, including its spans if any. */ public SpannableStringBuilder(CharSequence text) { this(text, 0, text.length()); } |
好吧。問題已經找到了,text.length(),不空指標才怪。
1 2 |
text = text == null ? "" : text; SpannableStringBuilder builder = new SpannableStringBuilder(text); |
加一行判斷就行了。
8、cursor.close()
一般來說,database的開和關不太會忘記,但遊標的使用可能並不會引起太多重視,尤其是遊標的隨意使用。比如用ContentResolver結合Cursor查詢SD卡中圖片,很容易寫出以下的程式碼:
1 2 3 4 5 6 |
Cursor cursor = contentResolver.query(uri, null, MediaStore.Images.Media.MIME_TYPE + "=? or " + MediaStore.Images.Media.MIME_TYPE + "=?", new String[] { "image/jpeg", "image/png" }, MediaStore.Images.Media.DATE_MODIFIED); while (cursor.moveToNext()) { // TODO } |
cursor都不做非空判斷,而且往往在關閉遊標的時候不注意有可能異常丟擲。
以前在專案中,經常出現由於遊標沒及時關閉或關閉出異常沒處理好導致其它的問題產生,而且問題看起來非常的詭異,不好解決。後來,我把整個專案中有關遊標的使用重構一遍,後來就再沒發生過類似的問題。
原則很簡單,所有Cursor的宣告為:
1 |
Cursor cursor = null; |
且放在try-catch外面;需要用到cursor,先做非空判斷。然後在方法的最後用一個工具類處理遊標的關閉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
/** * @author Leo * * Created in 2015-9-15 */ public class IOUtil { private IOUtil() { } public static void closeQuietly(Closeable closeable) { if (closeable != null) { try { closeable.close(); } catch (Throwable e) { } } } public static void closeQuietly(Cursor cursor) { if (cursor != null) { try { cursor.close(); } catch (Throwable e) { } } } } |
我想,這樣就沒必要在每個地方都try-catch-finally了。
9、java.lang.String cannot be converted to JSONObject
解析服務端返回的JSON字串時,居然丟擲了這個異常。除錯沒發現任何問題,看起來是正常的JSON格式。後來發現居然是JSON串多了BOM(Byte Order Mark)。服務端的程式碼由PHP實現,有時開發為了修改方便,直接用windows記事本開啟儲存,引入了人眼看不到的問題。其實就是多了”ufeff”這個玩意,客戶端程式碼過濾一下就行了。
1 2 3 4 5 6 7 8 |
// in case: Value of type java.lang.String cannot be converted to JSONObject // Remove the BOM header if (jsonStr != null) { jsonStr = jsonStr.trim(); if (jsonStr.startsWith("ufeff")) { jsonStr = jsonStr.substring(1); } } |
10、Shape round rect too large to be rendered into a texture
圓形矩形太大?
一開始我發現一個acitivity中的scrollView滑動一頓一頓的,而實際上沒有巢狀任何的列表控制元件如ListView、GridView,包含的無非是一些TextView、ImagView等。看了下Eclipse中log輸出,發現出現了這個warn級別的提示。難道是我在外層巢狀了這個圓形矩形?我在很多地方都用了呀,為何就這個介面出現問題了?
後來才發現,這個圓形矩形包含的內容太多了,已經超出了手機的高度,而且可以滑好幾頁。
StackOverFlow上有人說:The easiest solution is to get rid of the rounded corners. If you remove the rounded corners and use a simple rectangle, the hardware renderer will no longer create a single large texture for the background layer, and won’t run into the texture size limit any more.
也有建議:to draw onto the canvas.
具體連結:How Do Solve Shape round rect too large to be rendered into a texture
我試了下自定義控制元件LinearLayout,通過canvas進行draw,沒能解決。去掉radius屬性確實可行,可我想保留怎麼辦?
還有一個解決辦法,通過在androidManifest.xml中禁用硬體加速,為了控制粒度,我只在此activity中禁用此功能。
1 |
<activity android:hardwareaccelerated="false"/> |
先想到這麼多,以後再補充。
參考:
android:clipToPadding和android:clipChildren
HowTo: ListView, Adapter, getView and different list items’ layouts in one ListView
android ListView 在初始化時多次呼叫getView()原因分析
java.lang.IllegalArgumentException: pointerIndex out of range Exception – dispatchTouchEvent
What is difference between getSupportFragmentManager() and getChildFragmentManager()?