抽取一个通用的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老规矩,代码不贴,仅看效果图!谢谢阅读!

更多相关文章

  1. WEEX-EEUI 页面的高度问题(页面高度设置为多少才对?)
  2. [Android(安卓)Studio系列(三)]Android(安卓)Studio 编译、同步
  3. Android底部导航栏标签切换: ToolBar+TabLayout+ViewPager+Fragm
  4. Android(安卓)Apk反编译和代码混淆(第二篇代码混淆)
  5. Android之解决全屏切换非全屏的页面压缩问题
  6. Android(安卓)访问隐藏API
  7. Android(安卓)主流开源框架(二)OkHttp 使用详解
  8. Android基础入门教程——7.1.2 Android(安卓)Http请求头与响应头
  9. 在Android中为啥建议你用Message.obtain()方法获取Message对象,而

随机推荐

  1. AndroidO Camera 分析(一): (API 2)openCame
  2. mac添加android的adb等工具到环境变量
  3. android 中文 api (71) ―― BluetoothServ
  4. React native Android(安卓)命令 打包apk
  5. 51. (android开发)线性布局、相对布局、
  6. Android札记
  7. 关于“Only the original thread that cr
  8. Android换肤机制
  9. Android点击事件的四种写法
  10. Android编程: 环境搭建、基本知识