Android小项目之笑话大全
陆陆续续学习Android开发也有一段时间了,总觉得应该写点东西练练手。这是尝试的第一个Demo,用到的技术点包括Fragment
、RecyclerView
、网络请求、UI更新等。
参考:
- Android中ImageView和ImageButton的比较详解
- Android okHttp网络请求之Get/Post请求
- 你真的会用Gson吗?Gson使用指南
- 用GSON 五招之内搞定任何JSON数组
活动与碎片管理
单碎片活动
活动是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();
}
}
使用RecyclerView与滚动加载
RecyclerView
可以很方便地管理列表项,这里通过Android Studio
搜索android.support.v7.widget.RecyclerView
并将其添加到项目中。 Recylerview
的任务仅限定于回收和定位屏幕上的view,为了能够显示条目view,还必须ViewHolder和Adapter的支持
- ViewHolder用于引用视图
- Adapter用于为ViewHolder引用的视图提供数据,且ViewHolder实际上是由Adapter创建的
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的属性内容填充到视图上
}
}
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();
}
}
设置
完成了Holder
和Adapter
之后,只需要显式为recyclerview组件设置adapter即可
mAdapter = new JokeAdapter();
mRv.setAdapter(mAdapter);
考虑到数据会动态加载,
if (mAdapter == null){
mAdapter = new JokeAdapter();
mRv.setAdapter(mAdapter);
}else {
mAdapter.notifyDataSetChanged();
}
网络请求与JSON解析
在前面了解到了Android中网络请求的基本知识,这里使用okHttp
进行网络请求,在本地使用restify
搭建的服务器环境。
请求
使用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: 解析数据
}
})
拿到了数据之后,就需要考虑如何解析数据了,我们最终的目的是将内容展示到列表上。
bean类
看人家的Android项目源码的时候,发现往往都有一个bean
文件夹,存放只包含基本属性和对应setter
与getter
方法的数据类。一番查阅之后才发现这跟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
}
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);
}
更新UI
通过Gson
我们得到了一个Java对象列表,接下来就是展示对应数据了。这里需要注意的问题就是如何在网络请求线程更新UI线程了。之前了解到可以通过runOnUIThread
,view.post
和Handler
等方法,这里是使用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);
自定义分享组件
由于分享是一个比较常见且通用的功能,因此我们可以将其进行封装(当然这里现在只是封装样式和基本操作,具体的分享功能需要接口实现)。
首先是定义布局,这里使用了一个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"/>
阻止点击穿透
完成布局之后,我们就可以通过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"/>
向布局中动态添加视图
预先向布局文件中引入组件显得比较死板,试想如果多个页面上都需要进行分享操作,那还不得在每个布局文件中都引入一次?因此需要找到动态向页面插入组件的方法,参考这里。
实际上动态添加组件十分简单,使用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操作基本类似。
SQLite与本地存储
前面我们添加了一个收藏的按钮,可以将喜欢的笑话保存在SQLite中。这在安卓入门之本地存储中也提到过,这里算是活学活用了。
建立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";
}
}
}
建立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
去查看对应的数据库,记得修改权限啥的,这些坑在前面也已经趟过了。
使用数据库
数据库创建之后,就可以通过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();
}
小结
遇见的问题
- 为
RecyclerView
添加滚动事件时使用addOnScrollListener
代替setOnScrollListener
- 点赞图片使用的
selector
背景,默认状态需要放在所有item最后,否则看不见效果,参考stackoverflow - 之前为了防止背景图片变形一直采用
bitmap
,直到我发现了drawableTop
等属性~~ - 在
RadioGroup
布局中为某个RadioButton
设置了checked=true
属性之后,会出现同时选中两个radio的问题~~于是只能通过代码去实现默认选中
最后
最近工作的事儿比较多,这个项目基本上是每天早晨和晚上抽空写一点儿,所以感觉不是很连贯。回想最初学Android的目的,是为了更深入的了解组件化开发,这确实是学到了一些东西,基本上用人家已经写好的轮子,就可以搭一个不错的APP,封装内部实现,对外部提供接口,不论是在Web开发还是在Android中,这些概念貌似都是通用的,无非是采用的语言不同,技术栈不同而已。
当然,后面还会继续学习Android开发,这里只是提醒自己,不要被别人已经写好的各式各样酷炫的组件所迷惑了,如果是工作,用成熟的框架是明智之选;如果是学习,了解原理貌似更重要一点,这点千万不能本末倒置了。说道框架,我不信Android框架有web前端的框架多~~~这是要打架的。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。