Skip to content

Instantly share code, notes, and snippets.

@chzyer
Last active December 31, 2015 10:26
Show Gist options
  • Save chzyer/4564639 to your computer and use it in GitHub Desktop.
Save chzyer/4564639 to your computer and use it in GitHub Desktop.
使用AndFree入门安卓开发

本文主要介绍如何通过开源安卓框架Andfree来入门安卓开发.

###前提条件

  1. 了解java的语法
  2. eclipse和ADT环境已经配置好
  3. 部分eclipse操作技巧

###前言

本篇文章严重按照个人的编程顺序撰写, 毫无模块化归类可言.为了保证阅读和操作效果, 请勿跳着阅读..
如果将本篇文章全部读完, 相信你已经能够胜任大部分安卓开发的任务了!

###项目配置

  1. 新建一个Android工程, 工程名AndFreeStartUp, 包名设置为org.chenye.startup. 进入工程目录, 输入命令

$ git clone git://github.com/chzyer/AndFree.git ``` 这样就可以在根目录看到AndFree文件夹.

  1. 进入eclipse, 可以看到AndFree文件夹, 展开, 选中src, 右键, Build Path->Use as Source Folder, 这样便成功将AndFree引入到工程中.

###成就榜

涉及的知识点如下:

  • 数据库的增删查改
  • 分页处理
  • 界面列表显示
  • 按钮单击事件的回调函数
  • 数组和哈希表的使用
  • 自定义控件的制作
  • XML与JAVA的结合
  • Activity的跳转和通信
  • 常用程序交互

###软件内容

嗯嗯, 要说说我们第一个软件大概是什么东西了. 想来想去, 在覆盖知识全面和上手度相权衡, 大概就只有信息管理软件了. 所以我们的第一个软件就是TODO List吧. 软件涉及的功能如下:

  • 首页查看任务列表, 优先显示未完成的
  • 未完成的任务可以打钩(完成), 已完成的可以取消完成
  • 可以添加/编辑TODO任务, 包含的内容有:
    • 标题
    • 时间段
  • 每页显示5个, 最下方按钮按下可加载下一页
  • 任务长按弹出菜单, 有以下选项:
    • 删除, 需要对话框确认
    • 编辑

###开始编程

编程的流程大概如下:

  1. 基础知识介绍
  2. 设计主界面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)
    • 其他的可以暂时不理

###设计主界面XML

新建工程之后, 可以在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>

需要注意的是ScrollViewlayout_height, layout_width都是match_parent.

###新建一个Activity

####添加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, 声明方法:

  1. 打开Androidmainfest.xml, 在下面的选项卡点击Application, 在Application Nodes可以看到程序自动建立的AndfreeStartupActivity, 我们可以在这里建立Todo窗口.
  2. 可以校对一下我的代码
<?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吧.

  1. Select the root element for the XML file:那里选择RelativeLayout
  2. Text Fields拖一个Plain Text到界面里, 水平居中, 并且宽度充满整个屏幕. 嗯, RelativeLayout有这功能.
  3. 再添加一个按钮, 放Plain Text的右下方
  4. 再添加一个按钮, 放Plain Text的左下方
  5. 全部代码如下
<?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文件都已经准备完毕, 现在是怎么将两者关联起来.实际上是很简单的.

  1. Todo.javaonCreate方法, 添加

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);
	}
  1. 给按钮添加事件:

    添加事件, 因为这个事件不是即时发生的, 而是在按钮按下的时候才执行, 所以我们需要用到回调. 在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!");
    			}
    		}
    }
  2. 初始化按钮

    现在按钮的名字还是Button吧, 我们给他改名字. 使用AndFree封装的控件支持链式表达.可以让针对一个控件的初始化尽量在一行搞定, 当然如果代码过长建议还是分开比较好.

     widget.save.setText("Save").setClick(this);
     widget.cancel.setText("Cancel").setClick(this);
  3. 窗体跳转

    本来这里我想说可以运行看看效果的, 但是貌似到现在为止进入的窗体是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

###显示Todo列表

终于又回到了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.listScrollView里面的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);
}

运行后, 就能看到刚才添加的数据了.

###自定义TODO的项目

刚才为了简单, 先用暂时的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件事:

  1. 声明控件, 申请内存
  2. 初始化, 给他传递内容
  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);
	}
}

###测试2

看看刚才测试1的内容吧😭

###完善添加TODO

可能你也发现了, 当内容为空时, 也可以添加, 而且添加后也不会返回. 这时候我们就要开始完善它

//在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();

###跳转到Todo

原来的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排序

你也应该发现了, 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. 所以, 当类implementsOnLineClick之后, 可以实现以下方法:

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

这次我们将提供一个修改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();

又搞定了, 最后还剩一个删除功能...

###删除Todo

这个在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();
	}
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment