標籤:android emptyview
大家都對ListView非常熟悉,目測也會經常使用ListView的一個方法setEmptyView,來設定當資料載入中或者資料載入失敗的一個提醒的效果,這個方法雖然使用起來簡單,但是如果你提供一個複雜的布局,例如:
在資料載入失敗後,添加一個Button讓使用者可以選擇重新載入資料。
那麼,你可能會這麼做,find這個button,然後給button設定點擊事件,好吧。。。一個兩個的還可以忍受,那多了呢?比如我遇到的這個情況,在測試階段,老闆讓加一個重新整理的功能,要是按照這種方法,估計現在現在我還在加班(2015/7/27 23:00),那有沒有一種更加方便的方式,幾行代碼就可以搞定?而且不需要寫那些煩人的setOnClickListener?能不能提供一個不僅僅局限於ListView的EmptyView,因為我不僅僅在ListView上使用。
答案是肯定的,這篇部落格,我們就去實現一個這樣的組件,在實現之間,我們來看看ListView和他的EmptyView是怎麼一個關係,首先定位到ListView.setEmptyView方法:
@android.view.RemotableViewMethod public void setEmptyView(View emptyView) { mEmptyView = emptyView; // If not explicitly specified this view is important for accessibility. if (emptyView != null && emptyView.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { emptyView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } final T adapter = getAdapter(); final boolean empty = ((adapter == null) || adapter.isEmpty()); updateEmptyStatus(empty); }
繼續跟進代碼updateEmptyStatus
private void updateEmptyStatus(boolean empty) { if (isInFilterMode()) { empty = false; } if (empty) { if (mEmptyView != null) { mEmptyView.setVisibility(View.VISIBLE); setVisibility(View.GONE); } else { // If the caller just removed our empty view, make sure the list view is visible setVisibility(View.VISIBLE); } // We are now GONE, so pending layouts will not be dispatched. // Force one here to make sure that the state of the list matches // the state of the adapter. if (mDataChanged) { this.onLayout(false, mLeft, mTop, mRight, mBottom); } } else { if (mEmptyView != null) mEmptyView.setVisibility(View.GONE); setVisibility(View.VISIBLE); }}
唉,原來也沒啥,看代碼31~37行,就是根據資料是否為空白,來控制顯示mEmptyView和ListView本身。
既然原理簡單,那麼我們完全可以自己實現一個。但是,我們的原理正好和ListView的這個相反:
ListView是通過綁定一個emptyView實現的
而我們,是通過EmptyView綁定ListView(其他view也ok)實現的。
我們的EmptyView提供一個通用的方式,載入中時提醒載入中,載入失敗提醒載入失敗,並提供一個Button供使用者重新整理使用。
分析完了,接下來就是編碼了,首先我們繼承一個RelativeLayout來實現這麼一個布局:
public class EmptyView extends RelativeLayout { private String mText; private String mLoadingText; private TextView mTextView; private Button mButton; private View mBindView;...}
簡單說一下4個變數的作用。
mText表示資料為空白時提醒的文本。
mLoadingText表示載入中提醒的文本。
mTextView顯示提醒文本。
mButton提供給使用者重新整理的按鈕。
mBindView我們要綁定的view。
ok,繼續代碼:
public class EmptyView extends RelativeLayout { ... public EmptyView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.EmptyView, 0, 0); String text = ta.getString(R.styleable.EmptyView_android_text); String buttonText = ta.getString(R.styleable.EmptyView_buttonText); mLoadingText = ta.getString(R.styleable.EmptyView_loadingText); ta.recycle(); init(text, buttonText); }...}
為了靈活性,這些常值內容我們定義成可以在xml中配置使用,哎?怎麼還有一個buttonText,這個當然是按鈕上的文字了。
繼續代碼,可以看到調用了init方法。
來看看:
public class EmptyView extends RelativeLayout { ... private void init(String text, String buttonText) { if(TextUtils.isEmpty(text)) text = "暫無資料"; if(TextUtils.isEmpty(buttonText)) buttonText = "重試"; if(TextUtils.isEmpty(mLoadingText)) mLoadingText = "載入中..."; mText = text; mTextView = new TextView(getContext()); mTextView.setText(text); LayoutParams textViewParams = new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); textViewParams.addRule(RelativeLayout.CENTER_IN_PARENT); mTextView.setId(R.id.id_empty_text); addView(mTextView, textViewParams); mButton = new Button(getContext()); mButton.setText(buttonText); LayoutParams buttonParams = new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); buttonParams.addRule(RelativeLayout.CENTER_HORIZONTAL); buttonParams.addRule(RelativeLayout.BELOW, R.id.id_empty_text); addView(mButton, buttonParams); } ...}
在init方法中,上來,我們去判斷這些文本是否為空白,如果為空白,提供預設的文本。接下來new了一個TextView和Button並添加到該控制項中,TextView和Button是上下排列的。至此,布局已經完成了,那怎麼控制呢?我們想要的是什麼效果呢?
在資料載入的時候調用loading方法,顯示正在載入中的文本。
在資料載入成,隱藏該view。
在資料載入失敗,顯示載入失敗的文本,並提供一個按鈕去重新整理資料。
ok,我們按照這個條目一個個的來實現,首先是loading。
public class EmptyView extends RelativeLayout { ... public void loading() { if(mBindView != null) mBindView.setVisibility(View.GONE); setVisibility(View.VISIBLE); mButton.setVisibility(View.INVISIBLE); mTextView.setText(mLoadingText); } ...}
loading方法很簡單,首先判斷mBindView是否為空白,不為空白則隱藏它,然後讓該控制項可見,繼續讓Button不可見,因為在載入中的時候,我們不允許點擊的發生。最後就是讓TextView顯示正在載入中的文本。
繼續看看載入成功的方法,這個更簡單啦。
public class EmptyView extends RelativeLayout { ... public void success() { setVisibility(View.GONE); if(mBindView != null) mBindView.setVisibility(View.VISIBLE); } ...}
只有兩行代碼,就是讓該控制項隱藏,讓綁定的view顯示。
那麼載入失敗呢?同樣簡單!
public class EmptyView extends RelativeLayout { ... public void empty() { if(mBindView != null) mBindView.setVisibility(View.GONE); setVisibility(View.VISIBLE); mButton.setVisibility(View.VISIBLE); mTextView.setText(mText); } ...}
不多說了,唯一注意的就是我們讓Button顯示了。
至此,我們整個效果就完成了,在載入資料的時候調用loading方法來顯示載入中的文本,載入失敗後,調用empty來顯示載入失敗的文本和重新整理的按鈕,在載入成功後直接隱藏控制項!
控制項倒是完成了,我們還不知道mBindView怎麼來的,其實也很簡單。我們在代碼中需要調用bindView(View view)方法來指定。
public class EmptyView extends RelativeLayout { ... public void bindView(View view) { mBindView = view; } ...}
哈哈,剩下最後一個問題了,按鈕的點擊事件怎麼做?難道要在使用的時候添加onClick事件?哎,那樣太麻煩了,要知道,我有很多檔案要改的,我希望一行代碼就可以搞定!
亮點來了:
public class EmptyView extends RelativeLayout { ... public void buttonClick(final Object base, final String method, final Object... parameters) { mButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { int length = parameters.length; Class<?>[] paramsTypes = new Class<?>[length]; for (int i = 0; i < length; i++) { paramsTypes[i] = parameters[i].getClass(); } try { Method m = base.getClass().getDeclaredMethod(method, paramsTypes); m.setAccessible(true); m.invoke(base, parameters); } catch (Exception e) { e.printStackTrace(); } } }); } ...}
利用反射去做,我們只需要指定調用哪個對象上的哪個方法,需要參數的話就傳入參數即可。
這段代碼簡單說一下,首先我們給button設定了一個點擊事件,在事件響應的時候,首先遍曆參數,擷取參數的類型。然後根據方法名反射出方法,最後直接invoke去執行。這樣我們使用起來就非常方便了,完成了我們一行代碼搞定的目標。
好激動,來測試一下吧:
先看xml布局。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <loader.org.emptyview.EmptyView android:id="@+id/empty_view" android:layout_width="match_parent" android:layout_height="match_parent"/> <TextView android:id="@+id/name" android:layout_width="match_parent" android:layout_height="wrap_content"/></RelativeLayout>
我們沒有使用ListView,而是使用了一個TextView,再來看看在Activity中怎麼調用:
public class MainActivity extends AppCompatActivity { private EmptyView mEmptyView; private TextView mTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mEmptyView = (EmptyView) findViewById(R.id.empty_view); mTextView = (TextView) findViewById(R.id.name); mEmptyView.bindView(mTextView); // 設定bindView mEmptyView.buttonClick(this, "loadData"); // 當button點擊時調用哪個方法 loadData(); } /** * 載入資料 */ private void loadData() { mEmptyView.loading(); // 載入中 // 2s後出結果 new Handler().postDelayed(new Runnable() { @Override public void run() { Random r = new Random(); int res = r.nextInt(2); // 失敗 if(res == 0) { mEmptyView.empty(); // 顯示失敗 }else { // 成功 mEmptyView.success(); mTextView.setText("success"); } } }, 2000); }}
首先,我們通過mEmptyView.bindView(mTextView)來設定要綁定的view,這裡當然是TextView了。
接下來,通過mEmptyView.buttonClick(this, "loadData")設定按鈕點擊後執行哪個方法,這裡是當前對象上的loadData方法,並且沒有參數。
在getData中類比延遲2s後擷取資料,資料的成功失敗是隨機的,當失敗了,調用empty方法,成功後調用success方法。
哈哈,就是這麼簡單,來看看代碼的效果:
ok~ok~,非常完美。
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
Android打造不一樣的EmptyView