本文主要介绍如何通过开源安卓框架Andfree来入门安卓开发.
- 了解java的语法
- eclipse和ADT环境已经配置好
- 部分eclipse操作技巧
本篇文章严重按照个人的编程顺序撰写, 毫无模块化归类可言.为了保证阅读和操作效果, 请勿跳着阅读..
如果将本篇文章全部读完, 相信你已经能够胜任大部分安卓开发的任务了!
-
新建一个Android工程, 工程名
AndFreeStartUp
, 包名设置为org.chenye.startup
. 进入工程目录, 输入命令
$ git clone git://github.com/chzyer/AndFree.git
```
这样就可以在根目录看到AndFree
文件夹.
- 进入
eclipse
, 可以看到AndFree
文件夹, 展开, 选中src
, 右键,Build Path
->Use as Source Folder
, 这样便成功将AndFree引入到工程中.
涉及的知识点如下:
- 数据库的增删查改
- 分页处理
- 界面列表显示
- 按钮单击事件的回调函数
- 数组和哈希表的使用
- 自定义控件的制作
- XML与JAVA的结合
- Activity的跳转和通信
- 常用程序交互
嗯嗯, 要说说我们第一个软件大概是什么东西了. 想来想去, 在覆盖知识全面和上手度相权衡, 大概就只有信息管理软件了. 所以我们的第一个软件就是TODO List吧. 软件涉及的功能如下:
- 首页查看任务列表, 优先显示未完成的
- 未完成的任务可以打钩(完成), 已完成的可以取消完成
- 可以
添加/编辑
TODO任务, 包含的内容有:- 标题
- 时间段
- 每页显示5个, 最下方按钮按下可加载下一页
- 任务长按弹出菜单, 有以下选项:
- 删除, 需要对话框确认
- 编辑
编程的流程大概如下:
- 基础知识介绍
- 设计主界面XML
- 刚刚看到Android工程目录, 可能感觉到混乱, 如果一开始不清楚目录的作用的话将会不知如何下手, 所以我感觉有必要在最开始了解一下目录结构
src
代码的目录Android x.x.x
这个文件夹代表了使用安卓SDK的版本, 展开里面会有个android.jar
, 最核心的android代码都在里面(被编译成.class字节码)res
这里面可以存放资源/素材文件, 比如xml(布局代码, 类似html), 图片(png), 静态数据(全局共享的String).gen
这个文件夹和res
相关联, 这个文件是自动生成的, 不能更改, 当res
当中的资源被修改时, 编译器会自动将res
中的文件翻译为R.java中, 目的是可以在java代码内访问res
中的资源assets
这个可以存放资源文件, 和res
的差别是, 这里面的数据不会被R.java收录其中, 可以通过AssetManager
访问AndroidMainfest.xml
这个可以理解为C语言的main, 整个项目的核心配置文件, 包含了权限声明
,Activity声明(同时还可以定义哪个可作为默认启动Activity)
- 其他的可以暂时不理
新建工程之后, 可以在res
->layout
下好到main.xml
, 双击打开就能看到, 先从Form Widgets
中脱出一个Button
, 再从Composite
里面拖出一个ScrollView
, 拉伸他到底部.布局的代码将会是这样
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<Button android:text="添加"
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</Button>
<ScrollView android:id="@+id/scrollView1"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout android:id="@+id/linearLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>
</ScrollView>
</LinearLayout>
需要注意的是ScrollView
的layout_height
, layout_width
都是match_parent
.
####添加JAVA文件:
这个Activity的作用是添加TODO的作用. 在src
->org.chenye.andfreestartup
右键, 添加一个Class
, 名字就叫做Todo
, 考虑到这个Activity的作用不仅仅添加, 还有编辑, 就不叫做AddTodo
, EditTodo
之类的名字了, Activity第一个字母按照规范是要大写.
新建类之后, 这时候他还不是Activity, 需要继承, 在这里, 我选择继承AFActivity
, 这个是AndFree
里面经过修改的Activity, 然后想加一些代码在Activity启动的时候执行, 这时候就要声明onCreate
, 当然, 不用去背, 在空白出输入oncr
, 然后再按alt+/
打开自动补全, 找到onCreate
就行了.
####声明Activity:
刚才说到AndroidMainfest.xml
是负责声明Activity的, 如果没声明的话跳转到这个Activity会触发一个Exception, 声明方法:
- 打开
Androidmainfest.xml
, 在下面的选项卡点击Application
, 在Application Nodes
可以看到程序自动建立的AndfreeStartupActivity
, 我们可以在这里建立Todo
窗口. - 可以校对一下我的代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.chenye.andfreestartup"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="9" />
<application android:icon="@drawable/icon"
android:label="@string/app_name">
<activity android:name=".AndfreeStartupActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="Todo"></activity>
</application>
</manifest>
####界面设计:
这时候Todo窗体还是一个空壳, 我们为他设计一个界面吧, 和为主窗体的操作不同的是, 这次我们要自己创建xml.
嗯, 还是进入res
->layout
, 新建一个activity_todo.xml
, 命名注意, 只能全部是小写字母, 并且用_
连接起来, 至于为什么, 大概是这些都要收录进R.java
吧.
- 在
Select the root element for the XML file:
那里选择RelativeLayout
- 在
Text Fields
拖一个Plain Text
到界面里, 水平居中, 并且宽度充满整个屏幕. 嗯,RelativeLayout
有这功能. - 再添加一个按钮, 放
Plain Text
的右下方 - 再添加一个按钮, 放
Plain Text
的左下方 - 全部代码如下
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText android:id="@+id/editText1"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_width="match_parent">
<requestFocus></requestFocus>
</EditText>
<Button android:id="@+id/button1"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="Button"
android:layout_below="@+id/editText1"
android:layout_alignParentRight="true">
</Button>
<Button android:text="Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/button2"
android:layout_below="@+id/editText1"
android:layout_alignParentLeft="true">
</Button>
</RelativeLayout>
####界面绑定: 到这里, java和xml文件都已经准备完毕, 现在是怎么将两者关联起来.实际上是很简单的.
-
到
Todo.java
的onCreate
方法, 添加
setContentView(R.layout.activity_todo);
有几个问题:
> ###setContentView的作用
> 无可厚非, 这里的作用肯定是将xml当做Activity的模板, 需要注意的是, Activity支持的一些方法`findViewById`是需要先设置`setContentView`, 原因很简单.
> ###R.layout.activity_todo是怎么来的
> 刚刚说到`gen`的作用是将`res`中的数据收录到`R.java`中, 刚刚我们做的操作是在`res/layout/`添加`activity_todo.xml`, 当我们按下保存按钮时, 编译器会将res里面的文件夹进行编译, 还有`drawable-ldpi`, `drawable-hdpi`, 会被合并为`R.drawable`, `values`会根据里面的内容再单独编译, 比如`values`文件夹里面存在`strings.xml`, 就会编译成`R.strings`.还有一个特殊的, 编译器会自动收集`layout`里面的id, 合并相同的, 生成`R.id`.
> 所以, 我们能拿到`R.layout.activity_todo`, 类型是`int`
2. 定义控件, 在Todo内添加
```java
//注意大小写
class Widget extends WidgetList {
public AFEdit title = edt(R.id.editText1);
public AFButton save = btn(R.id.button1);
public AFButton cancel = btn(R.id.button2);
};Widget widget;
public void onCreate(Bundle bundle){
//xxx
setContentView(R.layout.activity_todo);
widget = new Widget();
widget.initAll(this);
}
###作用:
这个使用了AndFree定义控件定义方式, 将控件名称和对应的id在明显的位置声明出来.然后在
onCreate
内实例化Widget
, 并且通过widget.initAll(this)
初始化内部所有的控件. ###原来的定义方式是?
嗯, 为了方便阅读, 使用了基于AndFree的控件定义方式, 不然, 控件的定义方式将如下
public EditText title;
public Button save;
public void onCreate(Bundle bundle){
//xxx
setContentView(R.layout.activity_todo);
title = (EditText) findViewById(R.id.editText1);
save = (Button) findViewById(R.id.button1);
}
-
给按钮添加事件:
添加事件, 因为这个事件不是即时发生的, 而是在按钮按下的时候才执行, 所以我们需要用到回调. 在JAVA里面, 要达到回调的效果就要使用
interface
. 不过, 对于按钮单击,android
提供一个OnClickListener
的接口, 不用我们自己去定义.给按钮添加事件非常简单.//写法一 widget.save.setClick(new View.OnClickListener() { public void onClick(View v) { toast("save button pressed!"); } });
在上一点中已经实例化了widget, 并且调用了
initAll
, 所以这时候widget内部的控件都已经能够正常使用.widget.save
对应的就是保存按钮.给他定义单击事件可以通过setClick
设置.按钮单击后执行的事件是new View.OnClickListener() { public void onClick(View v) { toast("save button pressed!"); } }
View.OnClickListener
是一个interface, 在这里直接实例化并且在onClick
里面加入代码, 然后这整个interface被当做参数传进入, 当按钮被单击时, 系统会通过click.onClick(v);
来调用单击事件(假设这个interface传进去后被变量click
储存).还有第二种写法, 将View.OnClickListener作为变量储存起来
View.OnClickListener onclick = new View.OnClickListener() { public void onClick(View v) { toast("save button pressed!"); } } widget.save.setClick(onclick);
当该Activity有多个单击事件, 但是又不想每次都声明一个interface的时候, 可以使用一个interface传递给所有的按钮. 在interface内部, 可以看到
onClick(View v)
, 这里的参数v
是被点击控件的实例, 也就是说可以根据v
来判断用户到底点击了哪个按钮.View.OnClickListener onclick = new View.OnClickListener() { public void onClick(View v) { if (widget.save.equalView(v)) { toast("save button pressed!"); } if (widget.cancel.equalView(v)) { toast("cancel button pressed!"); } } } widget.save.setClick(onclick); widget.cancel.setClick(onclick);
如果还想要让代码量更少一点, 可以直接让Activity去实现接口
View.OnClickListener
. 反正这个Activity我就想处理一个OnClickListener
而已.//只显示必要代码 public class Todo extends AFActivity implements View.OnClickListener { public void onCreate(Bundle bundle){ //代码省略 widget.save.setClick(this); widget.cancel.setClick(this); } public void onClick(View v) { if (widget.save.equalView(v)){ toast("save button pressed!"); } if (widget.cancel.equalView(v)){ toast("cancel button pressed!"); } } }
-
初始化按钮
现在按钮的名字还是
Button
吧, 我们给他改名字. 使用AndFree
封装的控件支持链式表达
.可以让针对一个控件的初始化尽量在一行搞定, 当然如果代码过长建议还是分开比较好.widget.save.setText("Save").setClick(this); widget.cancel.setText("Cancel").setClick(this);
-
窗体跳转
本来这里我想说可以运行看看效果的, 但是貌似到现在为止进入的窗体是
AndfreeStartupActivity
, 怎么进入Todo
呢, 这里就涉及到窗体跳转的问题了.到AndfreeStartupActivity
, 首先把他从继承Activity
改为AFActivity
吧.public class AndfreeStartupActivity extends AFActvity { }
然后在setContentView(R.layout.main);
下面加上
```java
startActivity(Todo.class);
```
加上之后, 程序会在进入AndfreeStartupActivity
后自动跳转到Todo
窗体, 当然, 作为可选方案, 可以在AndroidMainfest.xml
找到Todo
的定义, 在里面加上
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
```
这样的话程序就会出现在快捷方式里面, 而且是两个(还有AndfreeStartupActivity), 如果不要`AndfreeStartupActivity`的话, 要将`AndfreeStartupActivity`的和上面代码一样的地方`删除`.
**由于我们仅仅是调试目的, 所以我就不推荐使用这种方式更改了.**
> Run!
####数据库设计:
既然我们已经将界面和交互做好了, 那么接下来就要设计todo的数据库了, 然后再把数据存入数据库里面.安卓原生对数据库的编写方式比较原始, 所以`AndFree`对数据库操作和定义进行了封装, 可以很方便的设计使用数据库.
首先我们定义一下结构:
* **todo_list**
* `_id`, 自动增长的id
* `title`, todo的名称
* `done`, 是否已完成
* `create`, 添加日期
* `done_date`, 完成的日期
然后我们新建一个包名`_andfree`, 用来存放对`andfree`的配置信息.`org.chenye.andfreestartup._andfree`, 在里面添加一个Class, 名字为`dbcore`. 让他继承`AFCore`.根据上面的定义, 可以写出一下代码:
```java
public class dbcore extends AFCore{
public static class todo_list extends AFTable{
//请一定要有_id字段
public final static DBField _id = DBField.primaryInt("_id");
public final static DBField title = DBField.text("title");
public final static DBField done = DBField.boolInt("done", 0);
public final static DBField create = DBField.dateline("create");
public final static DBField done_date = DBField.dateline("done_date");
}
}
####插入数据:
这下可以回到Todo
里面去修改save
的代码了.
将按下save的代码改为
if (widget.save.euqalView(v)){
saveTodo();
}
然后新建一个saveTodo()
方法, 将数据传入数据库. 等等, 文本框的内容要怎么获得? 其实和简单
String todoString = widget.title.getText();
然后使用AndFree
内置的Line
类型, 将数据以哈希表的形式储存起来.
//声明变量
Line todo = new Line(dbcore.todo_list.class);
可以看到, 在声明的时候, 将todo
和数据库表todo_list
关联起来了, 然后添加数据
todo.put(dbcore.todo_list.title, todoString);
todo.put(dbcore.todo_list.create, FuncTime.time());
这里只添加必要的信息, FuncTime.time()
这是AndFree
内置的辅助函数, 用于拿到时间的long值.
然后要将数据插入! 只是在AndFree
里面, 不用去理到底是插入还是更新, 只要一句save命令就搞定了.那么, 有一个问题, 他是怎么判断什么时候要更新, 什么时候要插入的呢, 这个就需要开发者提供一个要确保唯一
的字段, 但这个字段的值在这个表中已经存在, 便更新, 反之则插入. 当不提供任何字段时, 以_id
为准, 如果已有_id
, 就更新, 反之插入, 当然我们这次没有给他赋值_id
, 就铁定是插入. 再举个例, 如果想让title
不重复, 可以执行以下:
todo.save(dbcore.todo_list.title);
当然这个实际意义不太大, 我们只要简单的插入就行了.
todo.save();
所以, saveTodo()
的完整代码如下:
public void saveTodo() {
String todoString = widget.title.getText();
Line todo = new Line(dbcore.todo_list.class);
todo.put(dbcore.todo_list.title, todoString);
todo.put(dbcore.todo_list.create, FuncTime.time());
todo.save();
}
####怎么调试?
至少要有一个方法来查看是否插入成功了是不是啊...
暂时用toast看吧, 将下面的代码加入到todo.save()
下面
Line todo_list = new dbcore.todo_list().result();
toast(todo_list.toString());
或者简写为:
toast(new dbcore.todo_list().result().toString());
添加后就可以看到数据库内容以json的格式输出了. 如果出现错误, 可以查看Logcat
终于又回到了AndfreeStartupActivity
.记得如果取消他作为默认启动, 记得改回来啊.
在这个窗口里面呢, 要解决的是展示的问题. 和连接到添加Todo的窗体
先去掉自动跳转的代码startActivity(Todo.class);
关于控件的定义就不废话了
class Widget extends WidgetList {
public AFButton add = btn(R.id.button1);
public AFRlayout list = llayout(R.id.linearLayout1);
};Widget widget;
接下来是显示内容, 暂时用TextView
来显示吧.
首先从数据库查询数据出来
Line todo_list = new dbcore.todo_list().result();
这里面就可以遍历出数据(可以在调试中查看他输出json的文本), 接下来就是遍历他, 列出每一行的数据
//在java内, 可以用for..in..来遍历一个数据
for (Line todo: todo_list){
//...
//在for里面可以直接用todo来访问每一条数据
}
然后我们在for里面添加代码
AFText title = new AFText(this);
动态声明一个TextView
String todo_str = todo.str(dbcore.todo_list.title);
title.setText(todo_str);
todo
是一个Line
类型, todo.str
是将他哈希表里面对应键值输出为字符串, 参数支持直接提供数据库字段dbcore.todo_list.title
.即上面的代码直接等于todo.str("title")
.然后将数据赋值给title
的值.
widget.list.addView(title);
将title
添加进widget.list
, widget.list
是ScrollView
里面的LinearLayout
.
所以, 综上, 代码如下:
for (Line todo: todo_list){
AFText title = new AFText(this);
String todo_str = todo.str(dbcore.todo_list.title);
title.setText(todo_str);
widget.list.addView(title);
}
运行后, 就能看到刚才添加的数据了.
刚才为了简单, 先用暂时的AFText来显示文本, 但是我们要的是一个可以勾选的Todo List, 应该怎么做呢? 这时候就可以自定义写一个控件了...
新建一个包org.chenye.andfreestartup._widget
, 添加一个TodoItem
, 继承ExpandWidget
, 然后完成构造函数
和需要实现的函数
, 构造函数需要完成两个,
TodoItem(Context context)
, TodoItem(Context context, AttributeSet as)
. 完成后可以看到代码如下,
public class TodoItem extends ExpandWidget{
public TodoItem(Context context) {
super(context);
// TODO Auto-generated constructor stub
}
public TodoItem(Context context, AttributeSet as) {
super(context, as);
// TODO Auto-generated constructor stub
}
@Override
protected void onInit(boolean inXML) {
// TODO Auto-generated method stub
}
}
onInit
会在控件初始化的时候访问, 所以这时访问的属性都会是null, 即使已经设定默认值了.
下面的代码会触发NullPointerException
,
//ERROR!
public String str = "a";
protected void onInit(boolean inXML) {
log(str.length());
}
接下来我们想想对于一个TODO的项目, 应该对外支持什么接口
- 定义TODO的内容
- 定义是否已完成
- 传入id, 以便点击后将他列为
已完成
然后, 设计了以下接口
setDone(Boolean done)
setContent(String content)
setId(int _id)
####接下来开始设计item的界面:
新建一个item_todo.xml
, 根控件使用RelativeLayout
, 拖动一个CheckBox
左居中, 一个TextView
对齐CheckBox
, TextView
宽度右对齐屏幕右侧.将CheckBox
的内容设置为空.代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<CheckBox android:layout_width="wrap_content"
android:id="@+id/checkBox1"
android:text=""
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true">
</CheckBox>
<TextView android:layout_width="wrap_content"
android:id="@+id/textView1"
android:layout_height="wrap_content"
android:text="TextView"
android:layout_centerVertical="true"
android:layout_toRightOf="@+id/checkBox1"
android:layout_alignParentRight="true">
</TextView>
</RelativeLayout>
####绑定TodoItem
的XML和控件
在控件里面绑定XML和在Activity里面不太一样, 可以通过类似控件定义的方式.
@Override
protected void onInit(boolean inXML) {
widget = new Widget();
widget.initAll(this, widget.layout);
}
class Widget extends WidgetList {
public AFRlayout layout = rlayout(R.layout.item_todo);
public AFText content = txt(R.id.textView1);
public AFCheck check = check(R.id.checkBox1);
}; Widget widget;
当需要引入xml的时候, 就直接把他传入widget.initAll
的第二个参数就行了.他会作为layout进行解析, 得到的控件类型就是xml的跟类型.
####实现接口 接下来就可以定义接口了
public void setDone(Boolean done) {
widget.check.setCheck(done);
}
public void setContent(String content) {
widget.content.setText(content);
}
private int _id;
public void setId(int id){
_id = id;
}
然后我们定义用户点击check后的事件处理
public class TodoItem extends ExpandWidget implements View.OnClickListener {
protected void onInit(boolean inXML){
//...
widget.check.setClick(this);
}
public void onClick(View v) {
if (widget.check.equalView(v)){
boolean checked = widget.check.isChecked();
Line update = new Line(dbcore.todo_list.class);
update.put(dbcore.todo_list._id, _id);
update.put(dbcore.todo_list.done, checked);
update.save();
return;
}
}
}
嗯, 到这里, 一个自定义控件已经完成了, 再看看在
AndfreeStartupActivity
要怎么用吧
回到AndfreeStartupActivity
, 这时候只需要修改for里面的内容了. 直接换成
TodoItem ti = new TodoItem(this);
ti.setContent(todo.str(dbcore.todo_list.title));
ti.setId(todo._id());
ti.setDone(todo.bool(dbcore.todo_list.done));
widget.list.addView(ti);
这段代码只完成了3件事:
- 声明控件, 申请内存
- 初始化, 给他传递内容
- 把他添加进父级的视图里面
到这里, 已经可以正常运行了. 验证是否成功的方法是, 给一个item打钩, 然后退出程序, 在打开, 看看是否保持打钩状态.
多添加几个数据, 进行测试. 添加到11个todo吧. 什么? 添加按钮按了没反应?
给按钮添加事件相信已经明白了, 这里就不废话了
public class AndfreeStartupActivity extends AFActivity implements View.OnClickListener{
public void onCreate(Bundle bundle) {
//...
widget.add.setClick(this);
}
public void onClick(View v) {
startActivity(Todo.class);
}
}
看看刚才测试1的内容吧😭
可能你也发现了, 当内容为空时, 也可以添加, 而且添加后也不会返回. 这时候我们就要开始完善它
//在saveTodo()第一行加入
if (widget.title.isTextEmpty()) {
toast("please enter the details!");
return;
}
然后把调试用的toast给删了吧, 换成
finish();
另外, cancel
按钮按了也没用, 我们就给他加入finish的功能, 替换掉原来的代码
//old toast("cancel button pressed!");
finish();
加入这里算搞定了.
添加回来之后会发现内容不会自动刷新. 因为我们还没有添加Activity Result. ####什么是Activity Result 简单来说就是跳转到Activity的时候, 给他一个id号, 新Activity退出的时候, 会返回给原Activity刚刚提供的id和他想返回的数据(可选的) ####自动刷新的思路 我们可以在添加成功后, 返回新添加成功的TODO item, 然后回到原Activity之后, 将这条数据转成View, 添加到第一行.
####保存后返回数据内容
回到saveTodo
, 在执行todo.save()
之后, 可以把todo返回回去, 将finish();
换为如下代码
setResult(todo).finish();
原来的startActivity
默认是不获取result的. 当操作只有一个的时候可以这样定义一个回调函数, 换掉原来的startActivity
startActivityForResult(Todo.class, new onActivityResult(){
@Override
public void callback(boolean result_ok, Line data) {
if ( ! result_ok) return;
TodoItem ti = new TodoItem(m);
ti.setContent(data.str(dbcore.todo_list.title));
ti.setId(data._id());
ti.setDone(data.bool(dbcore.todo_list.done));
widget.list.addTopView(ti);
}
});
这样弄好了, 是刷新了, 可是怎么只能看见一个的? 原因是
widget.list
默认是横向排列的, 我们要改变为纵向
public void onCreate(Bundle bundle) {
//...
widget.add.setClick(this);
widget.list.setVertical(); // new
Line todo_list = new dbcore.todo_list().result();
//...
}
你也应该发现了, Todo变得混乱不堪, 当我们第一次进来的时候, 是希望先看到未完成的是吧? 剩下的再按日期排列.再往回看数据库查询的时候, 可以发现数据库查询语句是想当简洁的.
Line todo_list = new dbcore.todo_list().result();
嗯, 接下来, 就要介绍怎样通过
AndFree
使用比较常用的数据库语句.
按照上面的想法, 我们需要实现
SELECT * FROM todo_list ORDER BY `done`, `create` DESC
等价的语句如下
Line todo_list = new dbcore.todo_list().order(
dbcore.todo_list.done.ASC(),
dbcore.todo_list.create.DESC()
).result();
当todo的数量特别多的时候, 在进入Activity的一刹那, 会有卡顿的感觉, 当卡顿超过3秒, 就直接面临卡死...所以, 必要的时候, 我们需要给todo做一个分页
首先要修改一下sql,
SELECT * FROM todo_list ORDER BY `done`, `create` DESC LIMIT 0, 10
对应的代码是
Line todo_list = new dbcore.todo_list().order(
dbcore.todo_list.done.ASC(),
dbcore.todo_list.create.DESC()
).limit(0, 10).result();
但是仅仅这样还不够, 还需要检测总数量, 未读取数据的数量大于10个时一直显示加载更多, 反之不显示.
接下来我们就不得不把资料查询封装成一个函数了,
public void showTodoList(int start, int limit){
Line todo_list = new dbcore.todo_list().order(
dbcore.todo_list.done.ASC(),
dbcore.todo_list.create.DESC()
).limit(start, limit).result();
for(Line todo: todo_list){
TodoItem ti = new TodoItem(this);
ti.setContent(todo.str(dbcore.todo_list.title));
ti.setId(todo._id());
ti.setDone(todo.bool(dbcore.todo_list.done));
widget.list.addView(ti);
}
}
因为我们会多一个加载更多的按钮
, 但是这个按钮是动态生成的, 所以可以按照不提供Id
定义在Widget
里面,
class Widget extends WidgetList {
public AFButton add = btn(R.id.button1);
public AFRlayout list = llayout(R.id.linearLayout1);
public AFButton more = btn(); // new
};Widget widget;
然后再完成一个函数来完成自动计算下一页并且调用showTodoList
public void autoNextPage(){
int limit = 5;
int start = widget.list.getChildCount();
widget.list.removeView(widget.more);
showTodoList(start, limit);
int count = new dbcore.todo_list().count();
if (count > start + limit) {
widget.more.newInstance().setClick(this).setText("加载更多");
widget.list.addBottomView(widget.more);
}
}
可以看到widget.more
使用了setClick(this)
, 所以要对onClick
进行判断
if (widget.add.equalView(v)){
//...
return;
}
if (widget.more.equalView(v)) {
autoNextPage();
}
然后在onCreate最后去掉原来的查询代码, 换成autoNextPage();
这下可以多添加几个Todo, 用来测试下一页是否正常了...或者暂时降低limit的值
目前还差两个功能, 编辑和删除, 但是界面上我们已经没有空间了, 按照安卓的设计方法, 一般是可以通过长按Item弹出菜单来操作. 这一章就是要讲这一点.
接下来的修改将都在TodoItem
进行,
弹出菜单的代码如下
HelperDialog hd = new HelperDialog(m);
hd.addItem("Edit", Line.Put("index", 0));
hd.addItem("Delete", Line.Put("index", 1));
hd.setLineClick(this);
hd.show();
这段代码的意思是, 声明一个Dialog, 给他添加项目, 每个项目带有一个标签名
和Line信息
, 最后统一注册一个OnLineClick, 当用户点击某个Item的时候, 可以在onClick(Line data)
中的data
拿到对应的Line. 所以, 当类implements
了OnLineClick
之后, 可以实现以下方法:
public void onClick(Line data) {
int index = data.integer("index");
if (index == 0) {
toast("edit" + widget.content.getText());
return;
}
if (index == 1) {
toast("delete" + widget.content.getText());
return;
}
}
然后这个弹出菜单
需要在Item长按的时候触发, 所以我们还要注册widget.layout
的长按事件.
public class TodoItem extends ExpandWidget implements View.OnClickListener, View.OnLongClickListener, OnClickLine {
protected void onInit(boolean inXML) {
//xxx
widget.check.setClick(this);
widget.layout.setOnLongClickListener(this); // new
}
int EDIT = 0;
int DELETE = 1;
public boolean onLongClick(View v) {
HelperDialog hd = new HelperDialog(m);
hd.addItem("Edit", Line.Put("index", EDIT));
hd.addItem("Delete", Line.Put("index", DELETE));
hd.setLineClick(this);
hd.show();
return false;
}
}
为了确保代码清晰, 将index换成了变量, 同时把
onClick(Line data)
也改了吧
这次我们将提供一个修改TODO内容的选项, 为了贪图方便, 就不重新建立一个Activity了, 完全可以复用
Todo.java
, 当然, 这需要做一些改动.本篇将讲解这个过程.
思路大致如下:
AndfreeStartupActivity
: 跳转到Todo.java
, 并且发送当前todo item的id, 等待修改完后, 再更新这个item的值Todo
: 检测跳转到该窗口时有没有传递id, 没有的话就是添加操作, 有的话就是修改.当保存时, 如果是修改的操作, 要返回修改成功的信息回去, 通知原Activity更新值.
问题:
我们对菜单的修改只停留在
TodoItem
, 不是在AndfreeStartupActivity
, 所以不能调用startActivityForResult
这个命令, 该怎么办?
答案有几个, 我只说我认为还不错的. 将Edit
执行的内容换为外界决定.也就是说对于TodoItem
, 我们还需要设计一个接口用于定义Edit
事件, 同时定义我们自己的interface!
- setOnEditClick()
OnClickListener _edit_click;
public void setOnEditClick(OnClickListener click) {
_edit_click = click;
}
publuc interface OnClickListener {
public void onClick(TodoItem ti);
}
这时候, 当执行setOnEditClick
后, _edit_click
将是对应的操作, 然后再修改刚才的编辑
按钮执行的代码
// old: toast("edit" + widget.content.getText());
if (_edit_click != null) {
_edit_click.onClick(this);
}
回到AndfreeStartupActivity
, 让他继承TodoItem.OnClickListener
, 然后实现下面的方法:
public void onClick(TodoItem ti) {
startActivityForResult(Todo.class, Line.PutId(ti.getId()), new onActivityResult(){
@Override
public void callback(boolean result_ok, Line data) {
if ( ! result_ok) return;
ti.setContent(data.str(dbcore.todo_list.title));
}
});
}
这段代码可能需要解释下:
-
startActivityForResult
, 这个应该都认识, 因为这次需要传递被按下TodoItem
的id, 因此ti.getId
这句可能会错误, 因为我们还没有实现它// in TodoItem.java public int getId(){ return _id; }
实现后,
Line.PutId(ti.getId())
等价于{'_id': ti.getId()}
.这部分其实是传送给Todo
的值, 而第三个参数是得到Result后的回调函数 -
除了这个错误, 还会有
ti.setContent(...)
这句是红的, 因为ti
这个变量本来不属于这个onActivityResult
作用域的, 而是他父作用域
的, 他要访问到的前提是, 这个变量要加上final
关键字, 所以我们还要修改函数的第一行为:public void onClick(final TodoItem ti) {
####Todo的修改:
Todo
需要能检测是否有信息传递过来, 并且即便没信息传递过来, 也要能不产生Exception
, 这个就是既能当添加
, 又能当修改
的关键.
Line _todo;
@Override
protected void onLine(Line data) {
_todo = new dbcore.todo_list().getByPrimaryKey(data._id());
}
onLine
会自动判断是否有数据传过来, 当没有时, onLine是不会执行的.此时_todo就是为null
.
然后在onCreate
最后面加上初始化的代码
if (_todo == null) return;
widget.title.setText(_todo.str(dbcore.todo_list.title));
保存的那部分也要修改
todo.put(dbcore.todo_list.create, FuncTime.time());
// new
if (_todo != null) {
todo.put(dbcore.todo_list._id, _todo._id());
}
todo.save();
又搞定了, 最后还剩一个删除功能...
这个在TodoItem
便可完成,
将
toast("delete" + widget.content.getText());
换成
HelperDialog.Alert(m, "确定要删除", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
new dbcore.todo_list().delete(_id);
removeSelfFromParent();
}
});
完