1. 程式人生 > >Android打造不一樣的EmptyView

Android打造不一樣的EmptyView

大家都對ListView非常熟悉,目測也會經常使用ListView的一個方法setEmptyView,來設定當資料載入中或者資料載入失敗的一個提醒的效果,這個方法雖然使用起來簡單,但是如果你提供一個複雜的佈局,例如:

在資料載入失敗後,新增一個Button讓使用者可以選擇重新載入資料。

那麼,你可能會這麼做,find這個button,然後給button設定點選事件,好吧。。。一個兩個的還可以忍受,那多了呢?比如我遇到的這個情況,在測試階段,老闆讓加一個重新整理的功能,要是按照這種方法,估計現在現在我還在加班(2015/7/27 23:00),那有沒有一種更加方便的方式,幾行程式碼就可以搞定?而且不需要寫那些煩人的setOnClickListener

?能不能提供一個不僅僅侷限於ListViewEmptyView,因為我不僅僅在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了一個TextViewButton並新增到該控制元件中,TextViewButton是上下排列的。至此,佈局已經完成了,那怎麼控制呢?我們想要的是什麼效果呢?

在資料載入的時候呼叫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~,非常完美。