Android小项目之笑话大全

陆陆续续学习Android开发也有一段时间了,总觉得应该写点东西练练手。这是尝试的第一个Demo,用到的技术点包括FragmentRecyclerView、网络请求、UI更新等。

<!--more-->

参考:

1. 活动与碎片管理

1.1. 单碎片活动

活动是Android的基础,碎片是基于活动之上的。这里参考《Android权威指南》采用的一个管理碎片的活动类

public abstract class SingleFragmentActivity extends AppCompatActivity {

    protected abstract Fragment createFragment();
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 这个视图上只有一个用于存放碎片的容器,即R.id.fragment_container
        setContentView(R.layout.activity_single_fragment);
        getSupportActionBar().hide();

        // 碎片管理器
        FragmentManager fragmentManager = getSupportFragmentManager();
        Fragment fragment = fragmentManager.findFragmentById(R.id.fragment_container);

        if (fragment == null){
            fragment = createFragment();
            fragmentManager.beginTransaction().add(R.id.fragment_container, fragment).commit();
        }
    }
}
public class JokeListActivity extends SingleFragmentActivity {

    // 通过抽象,多个活动都可以实现这种单碎片的布局
    @Override
    protected Fragment createFragment() {
        return new JokeListFragment();
    }

}

2. 使用RecyclerView与滚动加载

RecyclerView可以很方便地管理列表项,这里通过Android Studio搜索android.support.v7.widget.RecyclerView并将其添加到项目中。 Recylerview的任务仅限定于回收和定位屏幕上的view,为了能够显示条目view,还必须ViewHolder和Adapter的支持

  • ViewHolder用于引用视图
  • Adapter用于为ViewHolder引用的视图提供数据,且ViewHolder实际上是由Adapter创建的

2.1. Holder

需要将Holder通过LayoutInflater关联到对应的布局上

private class JokeViewHolder extends RecyclerView.ViewHolder{
    public JokeViewHolder (LayoutInflater inflater, ViewGroup parent) {
        // 准备好xml布局,R.layout.item_joke
        super(inflater.inflate(R.layout.item_joke, parent, false));

        // 可以通过 itemView.findViewById 获取布局上的子组件
    }

    // 填充数据,JokeBean这个类后面会提到
    public void bind(JokeBean joke){
        mJoke = joke;
        // 这里可以通过itemView自组件,然后将joke的属性内容填充到视图上
    }
}

2.2. Adapter

Adapter用来绑定数据项

private class JokeAdapter extends RecyclerView.Adapter<JokeViewHolder >{

    @Override
    public JokeViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(getActivity());
        // 绑定 holder
        return new JokeViewHolder (inflater, parent);
    }

    @Override
    public void onBindViewHolder(JokeViewHolder holder, int position) {
        // 获取单条数据
        JokeBean joke = mJokes.get(position);
        // 将数据传递到holder上
        holder.bind(joke);
    }

    @Override
    public int getItemCount() {
        // 统计recyclerview的长度
        return mJokes.size();
    }
}

2.3. 设置

完成了HolderAdapter之后,只需要显式为recyclerview组件设置adapter即可

mAdapter = new JokeAdapter();
mRv.setAdapter(mAdapter);

考虑到数据会动态加载,

if (mAdapter == null){
    mAdapter = new JokeAdapter();
    mRv.setAdapter(mAdapter);
}else {
    mAdapter.notifyDataSetChanged();
}

3. 网络请求与JSON解析

在前面了解到了Android中网络请求的基本知识,这里使用okHttp进行网络请求,在本地使用restify搭建的服务器环境。

3.1. 请求

使用okHttp发起请求十分简单(当然跟ajax没法比),这里可以对请求继续进一步封装,后面再折腾。

// 实例化客户端
OkHttpClient client = new OkHttpClient();

// 构造请求
Request request = new Request.Builder()
                .url("http://10.0.2.2:9999/joke?page="+page+"&size=5")
                .build();

// 异步发送请求
client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        e.printStackTrace();
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {

          // 获取响应,这里服务端返回的是json字符串
          String res = response.body().string();
          // todo: 解析数据
    }
})

拿到了数据之后,就需要考虑如何解析数据了,我们最终的目的是将内容展示到列表上。

3.2. bean类

看人家的Android项目源码的时候,发现往往都有一个bean文件夹,存放只包含基本属性和对应settergetter方法的数据类。一番查阅之后才发现这跟JavaBean还有点关联~。 我对于Android中的bean类的理解就是将对象映射成对应的JSON形式,比如我们的joke数据返回是

{
    "title": "我:“唉,我们终究还...",
    "text": "我:“唉,我们终究还是败给了时间。。。。”\r\n基友:“说人话”\r\n我:“尼玛来不及做暑假作业了!!”",
    "type": 1,
    "ct": "2017-09-12 14:30:11.385"
}

对应的bean类是下面这个样子的,大概是我理解有误,这个貌似违背了封装的原则~

public class JokeBean {
    public String title;
    public String text; // content
    public int type;
    public String ct; // created_time
}

3.3. Gson解析

Gson是专门用来将Java对象与json对象相互转换的库,基础使用方法可以参考Google Gson的使用方法,实现Json结构的相互转换

Gson gson = new Gson();
// JokeBean即对应的bean类
ArrayList<JokeBean> jokes = new ArrayList<JokeBean>();
JsonParser parser = new JsonParser();

// res 即服务端返回的数据
JsonArray arr = parser.parse(res).getAsJsonArray();

for (JsonElement joke : arr){
    JokeBean jokeBean = gson.fromJson(joke, JokeBean.class);
    jokes.add(jokeBean);
}

3.4. 更新UI

通过Gson我们得到了一个Java对象列表,接下来就是展示对应数据了。这里需要注意的问题就是如何在网络请求线程更新UI线程了。之前了解到可以通过runOnUIThreadview.postHandler等方法,这里是使用handler进行处理的。

// 定义handler
mHandler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);

        switch (msg.what){
            case 0:
                List<JokeBean> jokes = (List<JokeBean>) msg.obj;
                mJokes.addAll(jokes);

                // 这里是recyclerview的适配器
                mAdapter.notifyDataSetChanged();
                break;
            default:
                Log.d("txm", "handler default");
        }
    }
};    

在网络请求获取到数据并成功解析之后,通知handler

Message msg = new Message();
msg.obj = jokes;
msg.what = 0;

handler.sendMessage(msg);    

4. 自定义分享组件

由于分享是一个比较常见且通用的功能,因此我们可以将其进行封装(当然这里现在只是封装样式和基本操作,具体的分享功能需要接口实现)。

首先是定义布局,这里使用了一个RelativeLayout用于展示全屏的半透明背景,一个GridLayout摆放分享按钮

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="#80000000">
    <GridLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:background="#fff"
        android:columnCount="3"
        android:paddingBottom="10dp"
        android:rowCount="2">

        <TextView
            style="@style/share_btn"
            android:background="@drawable/bg_qq"
            android:text="QQ"/>
    </GridLayout>
</RelativeLayout>

其中rgba颜色是通过额外增加十六进制颜色的前两位来实现的,上面的80代表50%的透明度,常用的透明度可以参考这里

然后去定义具体的视图类即可,在构造函数中加载布局,其他的操作比如点击按钮,隐藏分享界面等逻辑也可以在这里处理

public class ShareLayout extends GridLayout {
    private View mView;
    public ShareLayout(Context context, AttributeSet attrs){
        super(context, attrs);
        mView = LayoutInflater.from(context).inflate(R.layout.layout_share, this);
    }
}

最后就可以在其他地方使用这个自定义的组件了(真不好意思叫做自定义组件~),记得设置android:visibility="invisible"

<com.shymean.joke.view.ShareLayout
    android:clickable="true"
    android:id="@+id/share_wrap"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:visibility="invisible"/>

4.1. 阻止点击穿透

完成布局之后,我们就可以通过setVisibility(View.VISIBLE);来控制整个组件的显示可隐藏了。但是发现即使分享弹出层覆盖了整个页面,仍旧可以滚动下面的recyclerview,也会触发滚动加载等事件,这就不太合理了。

我们知道在CSS中非静态定位的元素是有Z-index属性的,而高层级的元素会阻挡事件向被覆盖的底层级元素传递。 而在Android中,布局中的view是通过addView的顺序控制的,表现为写在xml布局文件靠后的组件会出现在上面

解决方案是:使用FrameLayout, 将ShareLayout放在后面,且添加上android:clickable="true"

<com.shymean.joke.view.ShareLayout
    android:clickable="true"
    android:id="@+id/share_wrap"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:visibility="invisible"/>

4.2. 向布局中动态添加视图

预先向布局文件中引入组件显得比较死板,试想如果多个页面上都需要进行分享操作,那还不得在每个布局文件中都引入一次?因此需要找到动态向页面插入组件的方法,参考这里

实际上动态添加组件十分简单,使用addView即可

// 同时移除布局中的 com.shymean.joke.view.ShareLayout 标签
// mShareLayout = (ShareLayout) v.findViewById(R.id.share_wrap);

// 实例一个组件
mShareLayout = new ShareLayout(getContext(), null);
mShareLayout.setVisibility(View.INVISIBLE);

// 找到根节点
mRoot = (FrameLayout) v.findViewById(R.id.view_container);
// 向根结点插入组件
mRoot.addView(mShareLayout);

另外还可以通过removeView移除组件,这里跟DOM操作基本类似。

5. SQLite与本地存储

前面我们添加了一个收藏的按钮,可以将喜欢的笑话保存在SQLite中。这在安卓入门之本地存储中也提到过,这里算是活学活用了。

5.1. 建立Schema

通过建立Schema来管理表名和字段,减少后续代码中的硬编码是十分必要的,这个看起来跟bean类比较相似

public class JokeDbSchema {
    public static final class JokeTable {
        public static final String NAME = "joke";

        public static final class Cols {
            public static final String TITLE = "title";
            public static final String CONTENT = "content";
            public static final String TYPE = "type";
            public static final String CREATED_AT = "created_at";
        }
    }
}

5.2. 建立DBHelper类

通过继承SQLiteOpenHelper来创建数据库及管理数据库版本

public class JokeDataBaseHelper extends SQLiteOpenHelper {
    private static final int VERSION = 1;
    private static final String DATABASE_NAME = "jokeBase.db";

    // 需要手动实现一个构造方法
    public JokeDataBaseHelper(Context context){
        super(context, DATABASE_NAME, null, VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        // 这里用到了前面的schema
        db.execSQL("create table " + JokeTable.NAME + "("  +
                "_id integer primary key autoincrement, "+"" +
                JokeTable.Cols.TITLE + ", " +
                JokeTable.Cols.TYPE + ", " +
                JokeTable.Cols.CONTENT + ", " +
                JokeTable.Cols.CREATED_AT +")"
        );
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}

在开发的时候,数据结构改动可能比较大,如果频繁的去维护数据库会比较麻烦,这里建议直接从模拟器删除APP然后重新编译安装即可,所以这里的onUpgrade是空的。

可以通过adb shell去查看对应的数据库,记得修改权限啥的,这些坑在前面也已经趟过了。

5.3. 使用数据库

数据库创建之后,就可以通过getWritableDatabase获取到对应的数据库实例,然后进行CURD操作了。

mBd = new JokeDataBaseHelper(getContext()).getWritableDatabase();

这里只展示了点赞将数据插入到数据库中的操作,其他的就不写了,篇幅有点长了。

public void addItem(JokeBean joke){
    ContentValues values = new ContentValues();

    values.put("title", joke.title);
    values.put("content", joke.text);
    values.put("type", joke.type);
    values.put("created_at", joke.ct);
    Toast.makeText(getContext(), "succsss", Toast.LENGTH_SHORT).show();
}

6. 小结

6.1. 遇见的问题

  • RecyclerView添加滚动事件时使用addOnScrollListener代替setOnScrollListener
  • 点赞图片使用的selector背景,默认状态需要放在所有item最后,否则看不见效果,参考stackoverflow
  • 之前为了防止背景图片变形一直采用bitmap,直到我发现了drawableTop等属性~~
  • RadioGroup布局中为某个RadioButton设置了checked=true属性之后,会出现同时选中两个radio的问题~~于是只能通过代码去实现默认选中

6.2. 最后

最近工作的事儿比较多,这个项目基本上是每天早晨和晚上抽空写一点儿,所以感觉不是很连贯。回想最初学Android的目的,是为了更深入的了解组件化开发,这确实是学到了一些东西,基本上用人家已经写好的轮子,就可以搭一个不错的APP,封装内部实现,对外部提供接口,不论是在Web开发还是在Android中,这些概念貌似都是通用的,无非是采用的语言不同,技术栈不同而已。

当然,后面还会继续学习Android开发,这里只是提醒自己,不要被别人已经写好的各式各样酷炫的组件所迷惑了,如果是工作,用成熟的框架是明智之选;如果是学习,了解原理貌似更重要一点,这点千万不能本末倒置了。说道框架,我不信Android框架有web前端的框架多~这是要打架的。