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~,非常完美。