Android打造不一样的EmptyView
2015-11-22 22:26 阅读(194)

141629ja5003wso2lwtstc.jpg

大家都对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 方法。

哈哈,就是这么简单,来看看代码的效果:

141629ja5003wso2lwtstc.jpg

ok~ok~,非常完美。