抽取一个通用的Android的Loading页面
最近好懒,这篇博文端午之前就写好了demo,但是就是懒得更博,对自己的这种状态我也好无奈呀!话不多说,我们开始啦!
我们知道,在Android应用程序开发过程中,我们有很多页面需要去请求网络数据,请求网络是一个耗时操作,这时,一般我们不允许用户进行其它操作。为了解决用户在等待中出现不耐烦或者误因为应用假死的情况出现,我们这时需要在请求网络的过程中,显示一个页面提示用户当前的网络请求状态的页面,通常是一个圆形进度条或者比较可爱的卡通动画的一个Loading页面。这样,有助于提升用户体验。那么下面,我们来讨论一下,如何抽取一个通用的Loading页。
我们将从以下几方面进行讨论:
- 分析网络请求的各种状态
- 创建不同状态下对应的页面
- 抽取加载网络的抽象方法
- 抽取请求网络成功时构建成功页面的抽象方法
- 执行顺序分析及实际开发中优化提示
- demo示例
1、分析网络请求的各种状态
一般来说,网络请求的过程,存在着很大不确定的因素影响我们最终的请求结果。作为一个成熟可商用的APP,我们必须充分考虑到所有请求过程中,可能出现的问题,并且进行相应容错、差异化处理。在网络请求的过程中,主要可能出现以下几种可能性:
1、请求成功
2、请求失败
3、请求成功,但服务器无数据可供显示
4、网络错误
请求成功,这个很好理解,成功了就需要展示相应的数据。请求失败时,从上面的分析可以看到,我把它分又为了三种状态,我们先讲请求成功,但是服务器无数据显示的情况,这时,我们需要明确地告诉用户,当前是请求成功的,但是没有内容可以显示,无需继续进行请求操作了;网络错误,可能是用户没有把数据打开,或是wifi没有连接时,我们需要提醒用户打开数据功能或者连接一个可用的wifi热点,然后重新刷新页面请求数据;剩下的情况我们把它归结为一类请求失败,这时,可能是因为网络质量差、服务器返回一个异常的code码、又或者连接了一个无Internet网络连接的一个wifi热点,我们这时,可用给一个比较友好的提示告知当前的状态,并且,允许用户进行刷新操作重试。
所以,我们先创建一个Java类,我把它叫做LoadingFrame。首先我们分析一下这个类的写法,我们知道,一个Activity的页面内容填充,是通过setContentView,传递一个View对象用于显示的,最终,我们是要将我们抽取的这个类,作为一个View,显示到Activity中的,那么我们的LoadingFrame这个类,必须就是一个View对象。我们可以考虑继承自View或者View的之类对象,后期我们使用时,我们很容易就直接将我们创建的LoadingFrame对象作为一个View设置给contentView显示。那么我们根据上面的分析,考虑到,这个类,需要显示几种状态,每种状态对应的页面效果也是不一样的,我们可以考虑让LoadingFrame继承ViewGroup或者其子类,我们这里选用让它继承一个比较简单的FrameLayout,一帧一帧地居中显示。至于为什么要写成抽象类,后面你就知道了。
然后我们可以讲刚刚我们分析的这几种状态值给定义成常量,并且创建一个成员变量,记录当前的状态(currentState)
代码如下:
public abstract class LoadingFrame extends FrameLayout {
private Context mContext;
private static final int LOADING = 1;//加载中
private static final int LOADERROR = 2;//加载失败
private static final int NETERROR = 3;//网络错误
private static final int LOADED = 4;//加载完成
private static final int NODATA = 5;//无数据可显示
//当前的状态值,用于记录当前网络请求的状态
private int currentState = LOADING;
public LoadingFrame(Context context) {
super(context);
this.mContext = context;
}
public LoadingFrame(Context context, AttributeSet attrs) {
super(context, attrs);
this.mContext = context;
}
public LoadingFrame(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
}
}
上面,我通过分析,定义了5种不同的状态值,并且记录了当前的状态初始化为加载中,重写了三个构造方法用于结束context上下文对象。
那么现在,我们就来构建这几个状态对应的不同的页面吧。
2、创建不同状态下对应的页面
通过上面的分析,我们很容易地写出其对应的页面,那么,我们开始吧,首先,我们构建一个加载中的页面。
先看代码:
private void createLoadingView() {
mlinearLayoutLoading = new LinearLayout(mContext);
LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.WRAP_CONTENT);
linearLayoutParams.gravity = Gravity.CENTER;
mlinearLayoutLoading.setOrientation(LinearLayout.VERTICAL);
loadingView = new ImageView(mContext);
loadingView.setImageResource(R.drawable.loading);
AnimationDrawable animationDrawable = (AnimationDrawable) loadingView.getDrawable();
animationDrawable.start();
mlinearLayoutLoading.addView(loadingView,linearLayoutParams);
TextView textView = new TextView(mContext);
textView.setText("正在加载中");
mlinearLayoutLoading.addView(textView,linearLayoutParams);
mlinearLayoutLoading.setVisibility(View.GONE);
}
看看最终页面效果:
我们可以看到,布局非常简单,上面一个ImageView,下面有个TextView构成的一个页面。由于布局过于简单时,我习惯通过代码来写布局,读者见谅。所以,我新建了一个LinearLayout,往这个线性布局里面加了ImageView和TextView两个子View,并且设置垂直居中显示。
上面的ImageView其实是一个动画,实际效果是一个卡通人物在拼命奔跑的帧动画。在这里,还要感谢我亲爱的老婆大人,为我本次更博,熬夜绘制了几个非常可爱的卡通(不加这句,回去要跪搓衣板!)。动画我用xml定义出来了,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false"
>
<item
android:drawable="@drawable/loading1"
android:duration="200"></item>
<item
android:drawable="@drawable/loading2"
android:duration="200"></item>
</animation-list>
读者可以看到,非常简单,就是一个两帧重复无限循环的帧动画。读者们如果想尝试写,可以不必用动画,毕竟,在公司,有UI设计师提供,不是每一个程序员都有一个UI设计的老婆的,我是幸运的!
加载失败、网络错误、无数据的页面布局与加载中几乎一致,仅仅是换了张图片和文字而已,我就不做分析,直接贴出相应的代码
无数据
private void createNoDataView() {
mlinearLayoutNoData = new LinearLayout(mContext);
LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.WRAP_CONTENT);
linearLayoutParams.gravity = Gravity.CENTER;
mlinearLayoutNoData.setOrientation(LinearLayout.VERTICAL);
noDataView = new ImageView(mContext);
noDataView.setImageResource(R.drawable.nodata);
mlinearLayoutNoData.addView(noDataView,linearLayoutParams);
TextView textView = new TextView(mContext);
textView.setText("没有数据可供显示!");
mlinearLayoutNoData.addView(textView,linearLayoutParams);
mlinearLayoutNoData.setVisibility(View.GONE);
}
加载错误
private void createLoadedErrorView() {
mlinearLayoutLoadError = new LinearLayout(mContext);
LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.WRAP_CONTENT);
linearLayoutParams.gravity = Gravity.CENTER;
mlinearLayoutLoadError.setOrientation(LinearLayout.VERTICAL);
noDataView = new ImageView(mContext);
noDataView.setImageResource(R.drawable.nodata);
mlinearLayoutLoadError.addView(noDataView,linearLayoutParams);
TextView textView = new TextView(mContext);
textView.setText("加载失败!点击重试");
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
//点击操作的响应
}
});
mlinearLayoutLoadError.addView(textView,linearLayoutParams);
mlinearLayoutLoadError.setVisibility(View.GONE);
}
无网络
private void createNetErrorView() {
mlinearLayoutNetError = new LinearLayout(mContext);
LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.WRAP_CONTENT);
linearLayoutParams.gravity = Gravity.CENTER;
mlinearLayoutNetError.setOrientation(LinearLayout.VERTICAL);
netErrorView = new ImageView(mContext);
netErrorView.setImageResource(R.drawable.net_error);
mlinearLayoutNetError.addView(netErrorView,linearLayoutParams);
TextView textView = new TextView(mContext);
textView.setText("网络错误,检查您的网络或点击重试");
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
//点击操作的响应
}
});
需要指出的是,按照我们一开始的分析,无网络和请求失败时,我们需要提供重新请求网络的操作,所以我们在TextView上面,设置了一个点击监听。以上,我们就讲完了页面的改造啦!逻辑清晰的读者会说,等等,还有一个加载成功的页面呢,为什么就讲完了?
不要着急,下面会单独讲。
我们可以提供一个createView方法,在构造方法里面就调用一次,把这上面几个View创建出来,如下:
private void createView() {
params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER;
createLoadingView();
createNoDataView();
createNetErrorView();
createLoadedErrorView();
addView(mlinearLayoutLoading, params);
addView(mlinearLayoutNoData, params);
addView(mlinearLayoutNetError, params);
addView(mlinearLayoutLoadError, params);
refreshView();
}
在代码的后面,我调用refreshView()方法,其实这个方法,就是根据当前的状态去刷新界面的方法:
private void refreshView() {
mlinearLayoutLoading.setVisibility(currentState == LOADING ? View.VISIBLE : View.GONE);
mlinearLayoutNoData.setVisibility(currentState == NODATA ? View.VISIBLE : View.GONE);
mlinearLayoutNetError.setVisibility(currentState == NETERROR ? View.VISIBLE : View.GONE);
mlinearLayoutLoadError.setVisibility(currentState == LOADERROR ? View.VISIBLE : View.GONE);
}
3、抽取加载网络的抽象方法
在讲加载成功页面的创建之前,我们需要先知道,本次网络请求的状态到底是什么?我们可以想想,每个页面请求网络的URL地址都是不一致的,我们在这个抽象类里面还不能确定请求的地址,所以我们需要有一个抽象方法,让子类去具体实现请求网络的操作,这就是一开始我们把这个类定义成为抽象类的原因啦!这个方法,我们只关注返回值的结果,这里,我让它直接返回的int类型的code码,纯粹为了方便而且。实际开发中,我们应该限制这个返回值的类型,比如定义一个枚举类,这个读者自行实现。
我们知道,请求返回的code常见的有200、201、404等等,每种code码有其特定的含义,在公司开发中,服务端同学也会定义一些具有特定含义的code码,不同的公司有不一样的规则定义,具体可根据协议文档来写。
所以我们定义一个加载数据的方法,这里我们调用我们刚刚定义的onLoad这个抽象方法,然后根据code来维护currentState这个状态值。
private void initData() {
currentState = LOADING;
new Thread(new Runnable() {
@Override
public void run() {
SystemClock.sleep(3000);
int code = onLoad();
if (code == 200) {
((Activity) mContext).runOnUiThread(new Runnable() {
@Override
public void run() {
currentState = LOADED;
//加载成功的逻辑
}
});
} else if (code == 201) {
((Activity) mContext).runOnUiThread(new Runnable() {
@Override
public void run() {
currentState = NODATA;
//无的逻辑
}
});
}else if (code == 404) {
((Activity) mContext).runOnUiThread(new Runnable() {
@Override
public void run() {
currentState = LOADERROR;
//加载失败的逻辑
}
});
} else if (code == -1) {
((Activity) mContext).runOnUiThread(new Runnable() {
@Override
public void run() {
currentState = NETERROR;
//无网络的逻辑
}
});
}
}
}).start();
}
因为网络请求是耗时操作,不能放在UI线程中进行,不然会报错,所以我们开了一个线程,在调用onLoad方法之前,还给了3秒的阻塞时间,用于模拟请求网络的过程。然后后面,根据onLoad的返回结果,分别处理,这是,涉及到界面UI的操作,必须在主线程中进行,所以我们有回到主线程中处理UI问题,并给currentState 赋值。
4、抽取请求网络成功时构建成功页面的抽象方法
在上面initData方法中,code码为200时,我们需要创建一个请求成功正常显示的页面,当然,这个页面也是无法在此时就确定下来的,所以我们也要提供一个抽象方法比如叫onSuccessView(),返回类型是一个View对象。当请求成功时,我们可以调用这个方法,为加载成功的页面对象赋值为这个返回值。所以我们定义一个成员变量successView,当请求成功时,我们就可以调用这个方法为successView赋值了。所以,initData里面的200分支的判断逻辑可以这样写:
currentState = LOADED;
successView = onSuccessView();
addView(successView, params);
refreshView();
注意,后面我们又调用了一次refreshView方法刷新当前页面,但是,我们前面并没有对状态为LOADED做刷新,我们需要改造一下refreshView方法:
private void refreshView() {
mlinearLayoutLoading.setVisibility(currentState == LOADING ? View.VISIBLE : View.GONE);
mlinearLayoutNoData.setVisibility(currentState == NODATA ? View.VISIBLE : View.GONE);
mlinearLayoutNetError.setVisibility(currentState == NETERROR ? View.VISIBLE : View.GONE);
mlinearLayoutLoadError.setVisibility(currentState == LOADERROR ? View.VISIBLE : View.GONE);
if (successView != null) {
successView.setVisibility(currentState == LOADED ? View.VISIBLE : View.GONE);
}
}
这里的这个非空判断为什么要加呢?因为这个方法,我们在构造器里面了调用了一次,这时,加载成功的View还没有被创建出来呢,所以这里需要做个非空判断。我们可以想到,是不是所有的分支后面都需要refreshView呢,所以我们干脆这样,把这个refreshView直接放在后面,这句话必定要执行的,所以initData这个方法最终写成这样:
private void initData() {
currentState = LOADING;
new Thread(new Runnable() {
@Override
public void run() {
SystemClock.sleep(3000);
int code = onLoad();
if (code == 200) {
((Activity) mContext).runOnUiThread(new Runnable() {
@Override
public void run() {
currentState = LOADED;
successView = onSuccessView();
addView(successView, params);
}
});
} else if (code == 201) {
((Activity) mContext).runOnUiThread(new Runnable() {
@Override
public void run() {
currentState = NODATA;
}
});
}else if (code == 404) {
((Activity) mContext).runOnUiThread(new Runnable() {
@Override
public void run() {
currentState = LOADERROR;
}
});
} else if (code == -1) {
((Activity) mContext).runOnUiThread(new Runnable() {
@Override
public void run() {
currentState = NETERROR;
}
});
}
refreshView();
}
}).start();
}
到这里,我们就完成了几乎全部的代码编写任务,接下来,我们只需要,将执行顺序捋清楚就可以了。
5、执行顺序分析及实际开发中优化提示
我们知道,假如不考虑静态代码块的话,Java一开始执行的方法就是构造方法,所以我们在构造方法中就调用了createView方法,接着createView方法里面构建了4种状态的View界面,最后调用了一次刷新方法初始化显示View。到这里,前面我们的代码调用就断开了,View创建好了,什么时候赋值呢?所以,我们可以提供一个show()方法,show方法里面就只调用initData方法。
public void show() {
initData();
}
这个方法我们给public权限,在要显示界面的时候,再用LoadingFrame的对象去调用。这时候,我们就整个串起来了,只有调用show方法的时候,显示页面并且去请求网络,最后根据请求结果,再刷新一次页面。到这里,我们不要忘记了,我们在加载失败和网络错误时的两个点击事件还没有实现呢!其实很简单,我们只需要把状态查询赋值无加载中,并且调用一次show()方法和refreshView()方法就可以了。这时候,我们才终于写完了这个类,就可以写一个demo测试一下啦。
不过我们最后再测试,这里,我们再谈几点优化的问题,因为在实际开发过程中,框架写成这样,随随便便new Thread,会被别人骂死的。我们可以做以下改造
1、考虑将initData方法改造成为用Asynctask来实现;
2、文本提示提供set方法设置;
3、加强兼容性,比如提供离线页面等;
4、数据从有到无或者从无到有的方法设置;
5、refreshView中只是把View的可见性设置为GONE,但是实际View对象还是存在于内存中的,我们可以考虑直接removeView方法来代替。
6、限定onLoad的返回结果;
7、优化之路永无止境……
最后贴出完整代码:
public abstract class LoadingFrame extends FrameLayout {
private Context mContext;
private static final int LOADING = 1;
private static final int LOADERROR = 2;
private static final int NETERROR = 3;
private static final int LOADED = 4;
private static final int NODATA = 5;
private ImageView loadingView;
private LinearLayout mlinearLayoutLoading;
private ImageView noDataView;
private LinearLayout mlinearLayoutNoData;
private LinearLayout mlinearLayoutLoadError;
private ImageView netErrorView;
private LinearLayout mlinearLayoutNetError;
private View successView;
private int currentState = LOADING;
private FrameLayout.LayoutParams params;
public LoadingFrame(Context context) {
super(context);
this.mContext = context;
createView();
}
public LoadingFrame(Context context, AttributeSet attrs) {
super(context, attrs);
this.mContext = context;
createView();
}
public LoadingFrame(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private void initData() {
currentState = LOADING;
new Thread(new Runnable() {
@Override
public void run() {
SystemClock.sleep(3000);
int code = onLoad();
if (code == 200) {
((Activity) mContext).runOnUiThread(new Runnable() {
@Override
public void run() {
currentState = LOADED;
successView = onSuccessView();
addView(successView, params);
}
});
} else if (code == 201) {
((Activity) mContext).runOnUiThread(new Runnable() {
@Override
public void run() {
currentState = NODATA;
}
});
}else if (code == 404) {
((Activity) mContext).runOnUiThread(new Runnable() {
@Override
public void run() {
currentState = LOADERROR;
}
});
} else if (code == -1) {
((Activity) mContext).runOnUiThread(new Runnable() {
@Override
public void run() {
currentState = NETERROR;
}
});
}
refreshView();
}
}).start();
}
private void refreshView() {
mlinearLayoutLoading.setVisibility(currentState == LOADING ? View.VISIBLE : View.GONE);
mlinearLayoutNoData.setVisibility(currentState == NODATA ? View.VISIBLE : View.GONE);
mlinearLayoutNetError.setVisibility(currentState == NETERROR ? View.VISIBLE : View.GONE);
mlinearLayoutLoadError.setVisibility(currentState == LOADERROR ? View.VISIBLE : View.GONE);
if (successView != null) {
successView.setVisibility(currentState == LOADED ? View.VISIBLE : View.GONE);
}
}
public void show() {
initData();
}
private void createView() {
params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER;
createLoadingView();
createNoDataView();
createNetErrorView();
createLoadedErrorView();
addView(mlinearLayoutLoading, params);
addView(mlinearLayoutNoData, params);
addView(mlinearLayoutNetError, params);
addView(mlinearLayoutLoadError, params);
refreshView();
}
private void createNetErrorView() {
mlinearLayoutNetError = new LinearLayout(mContext);
LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.WRAP_CONTENT);
linearLayoutParams.gravity = Gravity.CENTER;
mlinearLayoutNetError.setOrientation(LinearLayout.VERTICAL);
netErrorView = new ImageView(mContext);
netErrorView.setImageResource(R.drawable.net_error);
mlinearLayoutNetError.addView(netErrorView,linearLayoutParams);
TextView textView = new TextView(mContext);
textView.setText("网络错误,检查您的网络或点击重试");
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
currentState = LOADING;
show();
refreshView();
}
});
mlinearLayoutNetError.addView(textView,linearLayoutParams);
mlinearLayoutNetError.setVisibility(View.GONE);
}
private void createNoDataView() {
mlinearLayoutNoData = new LinearLayout(mContext);
LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.WRAP_CONTENT);
linearLayoutParams.gravity = Gravity.CENTER;
mlinearLayoutNoData.setOrientation(LinearLayout.VERTICAL);
noDataView = new ImageView(mContext);
noDataView.setImageResource(R.drawable.nodata);
mlinearLayoutNoData.addView(noDataView,linearLayoutParams);
TextView textView = new TextView(mContext);
textView.setText("没有数据可供显示!");
mlinearLayoutNoData.addView(textView,linearLayoutParams);
mlinearLayoutNoData.setVisibility(View.GONE);
}
private void createLoadedErrorView() {
mlinearLayoutLoadError = new LinearLayout(mContext);
LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.WRAP_CONTENT);
linearLayoutParams.gravity = Gravity.CENTER;
mlinearLayoutLoadError.setOrientation(LinearLayout.VERTICAL);
noDataView = new ImageView(mContext);
noDataView.setImageResource(R.drawable.nodata);
mlinearLayoutLoadError.addView(noDataView,linearLayoutParams);
TextView textView = new TextView(mContext);
textView.setText("加载失败!点击重试");
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
currentState = LOADING;
show();
refreshView();
}
});
mlinearLayoutLoadError.addView(textView,linearLayoutParams);
mlinearLayoutLoadError.setVisibility(View.GONE);
}
private void createLoadingView() {
mlinearLayoutLoading = new LinearLayout(mContext);
LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.WRAP_CONTENT);
linearLayoutParams.gravity = Gravity.CENTER;
mlinearLayoutLoading.setOrientation(LinearLayout.VERTICAL);
loadingView = new ImageView(mContext);
loadingView.setImageResource(R.drawable.loading);
AnimationDrawable animationDrawable = (AnimationDrawable) loadingView.getDrawable();
animationDrawable.start();
mlinearLayoutLoading.addView(loadingView,linearLayoutParams);
TextView textView = new TextView(mContext);
textView.setText("正在加载中");
mlinearLayoutLoading.addView(textView,linearLayoutParams);
mlinearLayoutLoading.setVisibility(View.GONE);
}
public abstract View onSuccessView();
public abstract int onLoad();
}
6、demo示例
demo老规矩,代码不贴,仅看效果图!谢谢阅读!