乐者为王

Do one thing, and do it well.

实现带图标的ListView(改进版)

带图标的ListView工作起来还不错,但是如果手机里安装的软件比较多的话就有问题了。在程序启动时会黑屏一段时间,就好像程序挂起了一样。所以这时需要显示一个对话框,告诉用户正在加载数据,最好还能准确告诉用户加载的进度。

显示对话框用ProgressDialog,它有两种样式:HORIZONTAL和SPINNER。这里暂时选择SPINNER,等以后有机会再改成HORIZONTAL。

我们在加载数据前显示ProgressDialog,在之后关闭对话框。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
ProgressDialog pd = ProgressDialog.show(this, "Wait", "loading...");

ListView lv = (ListView)findViewById(R.id.list_view);
adapter = new SimpleIconAdapter(this,
        getInstalledApps(false),
        R.layout.list_item,
        new String[] {APP_ICON, APP_TITLE, APP_PACKAGE},
        new int[] {R.id.app_icon, R.id.app_title, R.id.app_package});
lv.setAdapter(adapter);

pd.dismiss();

但结果ProgressDialog没有显示出来,因为加载数据的代码也执行在UI线程中,所以对话框的显示被阻塞了。怎么办呢?可以试着创建一个新线程,然后把加载数据这个耗时的操作放到这个非UI线程中去执行,这样不就可以了么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ProgressDialog pd = ProgressDialog.show(this, "Wait", "loading...");

new Thread() {
    @Override
    public void run() {
        ListView lv = (ListView)findViewById(R.id.list_view);
        adapter = new SimpleIconAdapter(Main.this,
                getInstalledApps(false),
                R.layout.list_item,
                new String[] {APP_ICON, APP_TITLE, APP_PACKAGE},
                new int[] {R.id.app_icon, R.id.app_title, R.id.app_package});
        lv.setAdapter(adapter);
    }
}.start();

pd.dismiss();

这下更惨了,直接报RuntimeException,连界面都打不开了。

深入研究一下Android的线程模型,看看Android到底是怎么处理UI线程和其它线程的交互的,为什么上面的代码会报异常?

When an application is launched, the system creates a thread called "main" for the application. The main thread, also called the UI thread, is very important because it is in charge of dispatching the events to the appropriate widgets, including drawing events. It is also the thread where your application interacts with running components of the Android UI toolkit.

For instance, if you touch the a button on screen, the UI thread dispatches the touch event to the widget, which in turn sets its pressed state and posts an invalidate request to the event queue. The UI thread dequeues the request and notifies the widget to redraw itself.

This single-thread model can yield poor performance unless your application is implemented properly. Specifically, if everything is happening in a single thread, performing long operations such as network access or database queries on the UI thread will block the whole user interface. No event can be dispatched, including drawing events, while the long operation is underway. From the user's perspective, the application appears hung. Even worse, if the UI thread is blocked for more than a few seconds (about 5 seconds currently) the user is presented with the infamous "application not responding" (ANR) dialog.

看了上面这段话可以知道为啥程序一打开就黑屏,像挂掉了一样。

那为什么用新建线程来执行耗时操作的代码也有问题呢?这是因为它虽然没有阻塞UI线程,但它违背了单线程模型,Android的UI操作并不是线程安全的,并且这些操作必须在UI线程中执行。在这段代码片段中,在另一个线程中执行数据加载绑定,这会引起一些古怪的问题。

Andriod提供了几种在其它线程中访问UI线程的方法:

上面的任何一个类或方法都可以修复我们前面代码中遇到的问题,很不幸的是这些类或方法同样会使你的代码变的很复杂很难理解。而且当你需要实现一些很复杂的操作并需要频繁地更新UI时会变得更糟糕。为了解决这个问题,Android 1.5提供了一个工具类AsyncTask,它使创建需要与用户界面交互的长时间运行的任务变得更简单。在Android 1.0和1.1中具有与AsyncTask相同功能的类UserTask,它提供了完全一样的API,你需要做的只是把它的代码拷贝的你的程序中。

其实这些方案背后采用的都是Handler。所以这里仍然使用旧有的,复杂的Handler来修复上述问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.ListView;

public class MainActivity extends Activity {
    private static final String APP_ICON = "app_icon";
    private static final String APP_TITLE = "app_title";
    private static final String APP_PACKAGE = "app_package";

    private SimpleIconAdapter adapter;
    private List<HashMap<String, Object>> listData;
    private ProgressDialog pd;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        ListView lv = (ListView)findViewById(R.id.list_view);
        listData = new ArrayList<HashMap<String, Object>>();
        adapter = new SimpleIconAdapter(this,
                listData,
                R.layout.list_item,
                new String[] {APP_ICON, APP_TITLE, APP_PACKAGE},
                new int[] {R.id.app_icon, R.id.app_title, R.id.app_package});
        lv.setAdapter(adapter);

        pd = ProgressDialog.show(this, "Wait", "loading...");

        new Thread() {
            @Override
            public void run() {
                // 耗时操作,加载数据
                listData.addAll(getInstalledApps(false));
                handler.sendEmptyMessage(0);
            }
        }.start();
    }

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // 通知UI更新。必须要放在这里
            adapter.notifyDataSetChanged();

            pd.dismiss();
        }
    };

no such file to load -- xxx (MissingSourceFile) on Heroku

终于解决掉了困扰我多日的一个问题。

部署在Heroku上的项目,在控制器中require 'calc'时总是出现:

1
no such file to load -- calc (MissingSourceFile)

本地机器上运行的好好的,在Heroku上就出现Application error,百思不得其解,网上也没有找到类似问题的解决方法。

咨询Heroku的技术支持,几封邮件交流下来对方也没有办法(感觉Heroku的技术支持不咋的)。

今天把问题解决掉后再回过头来看,发现这其实是个非常简单的问题,就是Linux和Windows对待文件名大小写的差异。因为Linux是区分大小写的,所以用require 'calc'引入Calc.rb时就会失败,而Windows则可以成功。将文件名Calc.rb改成calc.rb再push到Heroku就把问题解决了。

就这么个大写字母竟然让我思考了几天,还真是郁闷啊!

实现带图标的ListView

listview-screenshot

main.xml不需要修改,还是用不使用ListActivity实现ListView中的那个。

在list_item.xml中添加ImageView控件,设置高度和宽度分别为32dip,这是因为有些应用图标尺寸比较大的缘故,如果不设置的话有些行就会撑大:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content">
    <ImageView
        android:id="@+id/app_icon"
        android:layout_gravity="center_vertical"
        android:layout_width="32.0dip"
        android:layout_height="32.0dip"
        android:layout_marginLeft="3.0dip"
        android:layout_marginRight="3.0dip" />
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1.0">
        <TextView
            android:id="@+id/app_title"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:textSize="16.0dip"
            android:textStyle="bold" />
        <TextView
            android:id="@+id/app_package"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:textSize="13.0dip" />
    </LinearLayout>
</LinearLayout>

新建SimpleIconAdapter类,继承SimpleAdapter适配器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.List;
import java.util.Map;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.SimpleAdapter;

public class SimpleIconAdapter extends SimpleAdapter {

    public SimpleIconAdapter(Context context, List<? extends Map<String, ?>> data,
            int resource, String[] from, int[] to) {
        super(context, data, resource, from, to);
    }

    @SuppressWarnings("rawtypes")
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view = super.getView(position, convertView, parent);
        ImageView iv = (ImageView)view.findViewById(R.id.app_icon);
        iv.setImageDrawable((Drawable)((Map)getItem(position)).get("app_icon"));
        return view;
    }
}

修改MainActivity文件,更换绑定的适配器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import android.app.Activity;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.widget.ListView;
import android.widget.SimpleAdapter;

public class MainActivity extends Activity {
    private static final String APP_ICON = "app_icon";
    private static final String APP_TITLE = "app_title";
    private static final String APP_PACKAGE = "app_package";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        ListView listView = (ListView)findViewById(R.id.list_view);
        SimpleAdapter adapter = new SimpleIconAdapter(this,
                getInstalledApps(false),
                R.layout.list_item,
                new String[] {APP_ICON, APP_TITLE, APP_PACKAGE},
                new int[] {R.id.app_icon, R.id.app_title, R.id.app_package});
        listView.setAdapter(adapter);
    }

    private List<HashMap<String, Object>> getInstalledApps(boolean getSysPackages) {
        List<HashMap<String, Object>> listItem = new ArrayList<HashMap<String, Object>>();

        List<PackageInfo> pkgs = getPackageManager().getInstalledPackages(0);
        for (int i = 0; i< pkgs.size(); i++) {
            PackageInfo pkg = pkgs.get(i);
            if (!getSysPackages
                    && (pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) > 0) {
                continue;
            }
            Drawable icon = pkg.applicationInfo.loadIcon(getPackageManager());
            String label = pkg.applicationInfo.loadLabel(getPackageManager()).toString();
            String version = pkg.versionName;
            String pkgName = pkg.packageName;

            HashMap<String, Object> map = new HashMap<String, Object>();
            map.put(APP_ICON, icon);
            map.put(APP_TITLE, label + " " + version);
            map.put(APP_PACKAGE, pkgName);
            listItem.add(map);
        }
        return listItem;
    }
}

不使用ListActivity实现ListView

main.xml布局代码:

1
2
3
4
5
6
7
8
9
10
<?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="wrap_content">
    <ListView
        android:id="@+id/list_view"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

list_item.xml布局代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content">
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1.0">
        <TextView
            android:id="@+id/app_title"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:textSize="16dip" />
        <TextView
            android:id="@+id/app_package"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>
</LinearLayout>

创建一个继承自Activitiy的类,覆写onCreate方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import android.app.Activity;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.os.Bundle;
import android.widget.ListView;
import android.widget.SimpleAdapter;

public class MainActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        ListView listView = (ListView)findViewById(R.id.list_view);
        SimpleAdapter adapter = new SimpleAdapter(this,
                getInstalledApps(false),
                R.layout.list_item,
                new String[] {"app_title", "app_package"},
                new int[] {R.id.app_title, R.id.app_package});
        listView.setAdapter(adapter);
    }

    private List<HashMap<String, Object>> getInstalledApps(boolean getSysPackages) {
        List<HashMap<String, Object>> listItem = new ArrayList<HashMap<String, Object>>();

        List<PackageInfo> pkgs = getPackageManager().getInstalledPackages(0);
        for (int i = 0; i< pkgs.size(); i++) {
            PackageInfo pkg = pkgs.get(i);
            if (!getSysPackages
                    && (pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) > 0) {
                continue;
            }
            String label = pkg.applicationInfo.loadLabel(getPackageManager()).toString();
            String version = pkg.versionName;
            String pkgName = pkg.packageName;

            HashMap<String, Object> map = new HashMap<String, Object>();
            map.put("app_title", label + " " + version);
            map.put("app_package", pkgName);
            listItem.add(map);
        }
        return listItem;
    }
}

至此,一个不使用ListActivity的ListView就完成了。

在ListView中显示已安装应用的名字和包名

通过PackageManager获取手机中已安装应用的信息,具体代码如下:

1
2
PackageManager packageManager = this.getPackageManager();
List<PackageInfo> packageInfoList = packageManager.getInstalledPackages(0);

通过以上方法,可以得到所有已安装的应用程序,既包括了手动安装的应用程序的信息,也包括了系统预装的应用程序的信息。要区分这两类应用可使用以下方法:

  1. 从packageInfoList获取的packageInfo,再通过packageInfo.applicationInfo获取applicationInfo;
  2. 判断applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM的值,该值大于0时,表示获取的应用为系统预装的应用,反之则为手动安装的应用。

列表的显示需要三个元素:

  1. 用来显示数据的ListView;
  2. 需要显示的数据;
  3. 绑定数据和ListView的Adapter。

Adapter通常有ArrayAdapter、SimpleAdapter和CursorAdapter等。其中SimpleAdapter有最好的扩充性,可以自定义出各种效果。

1
2
SimpleAdapter(Context context, List<? extends Map<String, ?>> data,
              int resource, String[] from, int[] to)

参数解释:

  1. context:上下文;
  2. data:数据列表。列表里的每一项都是一个Map对象,都和ListView里的一项进行数据绑定;
  3. resource:布局资源。可以引用系统提供的,也可以自定义;
  4. from:名字数组。每个名字都是用来索引数据列表中每个Map里的Object;
  5. to:资源索引数组,以R.id.xxx的形式表示。

main.xml布局代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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">
    <TextView
        android:id="@+id/app_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="22px" />
    <TextView
        android:id="@+id/app_package"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</LinearLayout>

上面代码中的android:orientation="vertical"很重要,没有它两个TextView就不会显示成两行。

继承了ListActivity的MainActivity代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import android.app.ListActivity;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.os.Bundle;
import android.widget.SimpleAdapter;

public class MainActivity extends ListActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        SimpleAdapter adapter = new SimpleAdapter(this,
                getInstalledApps(false),
                R.layout.main,
                new String[] {"app_title", "app_package"},
                new int[] {R.id.app_title, R.id.app_package});
        setListAdapter(adapter);
    }

    private List<HashMap<String, Object>> getInstalledApps(boolean getSysPackages) {
        List<HashMap<String, Object>> list = new ArrayList<HashMap<String, Object>>();

        List<PackageInfo> pkgs = getPackageManager().getInstalledPackages(0);
        for (int i = 0; i< pkgs.size(); i++) {
            PackageInfo pkg = pkgs.get(i);
            if (!getSysPackages
                    && (pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) > 0) {
                continue;
            }
            String label = pkg.applicationInfo.loadLabel(getPackageManager()).toString();
            String version = pkg.versionName;
            String pkgName = pkg.packageName;

            HashMap<String, Object> map = new HashMap<String, Object>();
            map.put("app_title", label + " " + version);
            map.put("app_package", pkgName);
            list.add(map);
        }
        return list;
    }
}

构建支持OAuth的新浪微博桌面客户端

要使用OAuth,首先要去新浪微博注册一个自己的微博应用。注册之后,会得到微博应用的Consumer key和Consumer secret,都是一个字符串。之后就可以进行OAuth的认证过程了。

https://code.google.com/p/sina-weibo4j/ 下载新浪微博的Java SDK包,当然也可以在 http://open.weibo.com/wiki/index.php/SDK 下载。推荐使用前者,已经打好包了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.Properties;
import java.util.PropertyResourceBundle;

import weibo4j.Weibo;
import weibo4j.WeiboException;
import weibo4j.http.AccessToken;
import weibo4j.http.RequestToken;

public class UpdateStatus {
    private static final String ACCESS_SECRET = "access_secret";
    private static final String ACCESS_TOKEN = "access_token";
    private static final String FILE_NAME = "sina_token.properties";

    public static void main(String[] args) {
        // 在这里填写在应用申请中得到的App Key和App Secret
        System.setProperty("weibo4j.oauth.consumerKey", YOUR_CONSUMER_KEY);
        System.setProperty("weibo4j.oauth.consumerSecret", YOUR_CONSUMER_SECRET);

        Weibo weibo = new Weibo();

        try {
            // 读取存储起来的Access Token
            AccessToken accessToken = loadAccessToken();
            if (accessToken == null) {
                String backUrl = "http://zhubabo.appspot.com";

                // 请求Request Token(未授权令牌)
                RequestToken requestToken = weibo.getOAuthRequestToken(backUrl);
                System.out.println("Got request token...");
                System.out.println("Request token: " + requestToken.getToken());
                System.out.println("Request token secret: " + requestToken.getTokenSecret());

                // 将以下打印出的授权链接在浏览器中打开,完成应用授权,会自动跳转到
                // 指定的callback url,并将oauth_verifier一起返回
                System.out.println("Open the following url in a browser");
                System.out.println(" http://api.t.sina.com.cn/oauth/authorize?oauth_token=" + requestToken.getToken());

                // 输入上面得到的oauth_verifier的值,取得Access Token,这个Token是长期有效的
                System.out.println("please input verifier:");
                String verifier = readLine();
                accessToken = weibo.getOAuthAccessToken(
                    requestToken.getToken(),
                    requestToken.getTokenSecret(),
                    verifier);

                // 将Access Token保存下来,以后就可以直接通过此Token向新浪围脖发消息了
                storeAccessToken(accessToken.getToken(), accessToken.getTokenSecret());
            }

            weibo.setToken(accessToken.getToken(), accessToken.getTokenSecret());

            System.out.println("Input message to sina:");
            String message = readLine();
            while (!"exit".equals(message.trim())) {
                weibo.updateStatus(message);
                System.out.println("Input message to sina:");
                message = readLine();
            }
        } catch (WeiboException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void storeAccessToken(String token, String tokenSecret) throws IOException {
        OutputStream out = new FileOutputStream(new File(FILE_NAME));
        Properties p = new Properties();
        p.setProperty(ACCESS_TOKEN, token);
        p.setProperty(ACCESS_SECRET, tokenSecret);
        p.store(out, "sina access token");
        out.flush();
        out.close();
    }

    private static AccessToken loadAccessToken() throws IOException {
        File f = new File(FILE_NAME);
        if (!f.exists()) {
            return null;
        }
        InputStream in = new FileInputStream(f);
        PropertyResourceBundle bundle = new PropertyResourceBundle(in);
        in.close();
        return new AccessToken(bundle.getString(ACCESS_TOKEN), bundle.getString(ACCESS_SECRET));
    }

    private static String readLine() throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        byte[] bs = reader.readLine().getBytes("gbk");
        return new String(bs);
    }
}

Rails 2.3和Paperclip实现多图片上传(改进版)

从Rails 2.3开始,有了新的方法来处理多模型表单,这就是accepts_nested_attributes_for,它允许直接赋值到子对象上,对于标准的属性使用相同的散列格式。

例如,一个parent有多个children,包含所有类的POST数据将会是下面这样:

1
2
3
4
5
6
7
8
9
10
11
params =>
  action => update
  id => 1
  controller => parents
  parent =>
    first_name => John
    last_name => Doe
    age => 40
    children_attributes =>
      1 => { id => 16, :name => Jack }
      2 => { id => 18, :name => Mary }

注意,那个children_attributes元素在parent的元素中。fields_for会为这些子元素生成必要的POST数据,这些数据会被转换成方便accepts_nested_attributes_for解释的散列。children_attributes不是数组而是散列,它的键是一个简单的索引(不是模型的ID),被用来从一个单一的实体聚合属性。

不用ID的理由是简单的:正在被编辑的模型可能不会被保存,发送到了客户端,这时它的ID为nil。因此为每个被显示的children的id关联一个隐藏字段是个好的实践。

下面我们就用accepts_nested_attributes_for来改写上传多图片的代码。

修改albums_controller.rb中的new方法,删除下面的这句代码:

1
1.upto(3) { @album.photos.build }

将album.rb中的

1
2
3
4
5
def photo_attributes=(photo_attributes)
  photo_attributes.each do |attributes|
    photos.build(attributes)
  end
end

代码改成

1
accepts_nested_attributes_for :photos

修改_form.html.erb中的

1
2
3
4
5
6
7
<div id="photos">
  <% if @album.new_record? %>
    <%= render :partial => 'photo', :collection => @album.photos %>
  <% end %>
</div>

<%= link_to_function "Add Photo" do |page| page.insert_html :bottom, :photos, :partial => 'photo', :object => Photo.new end %>

为下面的代码

1
2
3
4
5
6
7
<div id="photos">
  <% if @album.new_record? %>
    <%= render :partial => 'photo', :locals => { :form => f, :photo => @album.photos.build } %>
  <% end %>
</div>

<%= add_object_link("Add Photo", f, @album.photos.build, "photo", "#photos") %>

再将_photo.html.erb中的

1
2
3
4
5
<% fields_for "album[photo_attributes][]", photo do |p| %>
  <%= p.label :photo %><br />
  <%= p.file_field :data, :index => nil %>
  <%= link_to_function "delete", "remove_field($(this), ('.photo'))" %>
<% end %>

改为

1
2
3
4
5
<% form.fields_for :photos, photo, :child_index => (photo.new_record? ? "index_to_replace_with_js" : nil) do |p| %>
  <%= p.label :photo %><br />
  <%= p.file_field :data %>
  <%= link_to_function "delete", "remove_field($(this), ('.photo'))" %>
<% end %>

在albums_helper中添加两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def add_object_link(name, form, object, partial, where)
  html = render(:partial => partial, :locals => { :form => form }, :object => object)
  link_to_function name, %{
    var new_object_id = new Date().getTime();
    var html = jQuery(#{js html}.replace(/index_to_replace_with_js/g, new_object_id)).hide();
    html.appendTo(jQuery("#{where}")).slideDown('slow');
  }
end

def js(data)
  if data.respond_to? :to_json
    data.to_json
  else
    data.inspect.to_json
  end
end

到这里就修改完成了。试试看,是不是和原来的效果一样。

Rails 2.3和Paperclip实现多图片上传

1
2
3
4
5
6
7
8
9
rails upload
cd upload
script/plugin install git://github.com/thoughtbot/paperclip.git
script/plugin install git://github.com/aaronchi/jrails.git
script/generate scaffold album name:string
script/generate model photo album:references
script/generate paperclip photo data
rake db:migrate
script/server

修改album.rb添加photo_attributes属性:

1
2
3
4
5
6
7
8
9
10
class Album < ActiveRecord::Base
  has_many :photos
  validates_presence_of :name

  def photo_attributes=(photo_attributes)
    photo_attributes.each do |attributes|
      photos.build(attributes)
    end
  end
end

修改photo.rb添加附件相关属性:

1
2
3
4
5
6
7
8
9
10
class Photo < ActiveRecord::Base
  belongs_to :album

  has_attached_file :data,
    :url => "/uploads/:style_:basename.:extension",
    :styles => { :thumb => "50x50#", :large => "640x480#" }
  validates_attachment_presence :data
  validates_attachment_content_type :data,
    :content_type => ['image/jpeg', 'image/jpg', 'image/png']
end

修改new.html.erb为以下代码:

1
2
3
4
5
6
7
8
<h1>New album</h1>

<% form_for @album, :html => { :multipart => true } do |f| %>
  <%= render :partial => 'form', :locals => { :f => f } %>
  <p><%= f.submit "Create" %></p>
<% end %>

<%= link_to 'Back', albums_path %>

对edit.html.erb做同样的修改:

1
2
3
4
5
6
7
8
9
<h1>Editing album</h1>

<% form_for @album, :html => { :multipart => true } do |f| %>
  <%= render :partial => 'form', :locals => { :f => f } %>
  <p><%= f.submit "Update" %></p>
<% end %>

<%= link_to 'Show', @album %> |
<%= link_to 'Back', albums_path %>

把从new.html.erb和edit.html.erb中抽取出来的代码保存为_form.html.erb文件:

1
2
3
4
5
6
<%= f.error_messages %>

<p>
  <%= f.label :name %>
  <%= f.text_field :name %>
</p>

然后在后面添加如下代码:

1
2
3
4
5
6
7
<div id="photos">
  <% if @album.new_record? %>
    <%= render :partial => 'photo', :collection => @album.photos %>
  <% end %>
</div>

<%= link_to_function "Add Photo" do |page| page.insert_html :bottom, :photos, :partial => 'photo', :object => Photo.new end %>

创建_photo.html.erb文件,代码如下:

1
2
3
4
5
6
7
8
9
<div class="photo">
  <p>
  <% fields_for "album[photo_attributes][]", photo do |p| %>
    <%= p.label :photo %><br />
    <%= p.file_field :data, :index => nil %>
    <%= link_to_function "delete", "remove_field($(this), ('.photo'))" %>
  <% end %>
  </p>
</div>

再在application.js中添加下列代码,这样就可以删除file字段了。

1
2
3
function remove_field(element, item) {
  element.up(item).remove();
}

然后修改albums_controller.rb中的new方法:

1
2
3
4
5
6
7
8
def new
  @album = Album.new
  1.upto(3) { @album.photos.build }

  respond_to do |format|
    format.html # new.html.erb
  end
end

这样,上传多文件的功能基本就完成了。下面就来实现显示和修改的功能。

在show.html.erb的末尾添加下列代码,上传成功后用来显示图片:

1
2
3
4
#loop through the albums photos
<% for photo in @album.photos %>
  <%= image_tag photo.data.url(:thumb) %>
<% end %>

修改albums_controller.rb中的edit和update方法,用来删除图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def edit
  @album = Album.find(params[:id])
  if @album.photos.first.nil?
    1.upto(3) { @album.photos.build }
  end
end

def update
  params[:photo_ids] ||= []
  @album = Album.find(params[:id])
  unless params[:photo_ids].empty?
    Photo.destroy_pics(params[:id], params[:photo_ids])
  end

  respond_to do |format|
    if @album.update_attributes(params[:album])
      flash[:notice] = 'Album was successfully updated.'
      format.html { redirect_to(@album) }
    else
      format.html { render :action => "edit" }
    end
  end
en

在photo.rb中加上下面的代码:

1
2
3
def self.destroy_pics(album, photos)
  Photo.find(photos, :conditions => { :album_id => album }).each(:destroy)
end

然后新建_album_photo.html.erb,代码如下:

1
2
3
4
<% unless album_photo.new_record? %>
  <%= image_tag album_photo.data.url(:thumb) %>
  <%= check_box_tag "photo_ids[]", album_photo.id %>
<% end rescue nil %>

接着在_form.html.erb中加入下面的代码就可以删除图片了:

1
2
3
<div class="album_photos">
  <%= render :partial => 'album_photo', :collection => @album.photos %>
</div>

在Rails应用中集成支付宝

注意:notify_url为服务器通知,支付宝可以保证99.9999%的通知到达率,前提是你的网络通畅,在notify_url中可以做对数据库的业务操作。return_url中可以做数据库的更新也可以做显示。第一次交易状态改变(即时到帐中的交易完成的交易状态)时,支付宝发起的通知的时间与返回页自动跳转回的时间近乎同时。

支付处理代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class OrdersController < ApplicationController
  before_filter :login_required

  # place order
  def place_order
    parameters = {
      'service' => 'create_direct_pay_by_user',
      'partner' => ALIPAY_ACCOUNT,
      'seller_email' => ALIPAY_EMAIL,
      'out_trade_no' => @order.out_trade_no,
      'subject' => 'payment subject',
      'body' => 'payment body',
      'price' => @order.price.to_s,
      'quantity' => @order.quantity.to_s,
      'payment_type' => '1',
      '_input_charset' => 'utf-8',
      'notify_url' => url_for(:only_path => false, :action => 'notify'),
      'return_url' => url_for(:only_path => false, :action => 'done')
    }

    # 即时到帐中交易状态为“等待买家付款”的状态默认是不会发送通知的,自己手动设置一下
    @order.status = 'WAIT_BUYER_PAY'
    @order.user = current_user
    @order.save

    values = {}
    # 支付宝要求传递的参数必须要按照首字母的顺序传递,所以这里要sort
    parameters.keys.sort.each do |k|
      values[k] = parameters[k];
    end

    # 一定要先unescape后再生成sign,否则支付宝会报ILLEGAL SIGN
    sign = Digest::MD5.hexdigest(CGI.unescape(values.to_query) + ALIPAY_KEY)
    gateway = 'https://www.alipay.com/cooperate/gateway.do?'
    redirect_to gateway + values.to_query + '&sign=' + sign + '&sign_type=MD5'
  end

  # 返回success或fail。如果返回fail,支付宝会每隔一段时间就自动调用notify_url通信接口
  def notify
    render :text => 'success'
  end

  def done
    if verify_sign
      order = Order.find_by_out_trade_no(params[:out_trade_no])
      # 支付宝即时到帐接口只有一种交易状态,就是“交易成功”,更新一下
      order.update_attributes(params[:trade_status])
      render :text => 'Payment successful'
    else
      render :text => 'Alipay Error: ILLEGAL_SIGN'
    end
  end

protected
  def verify_sign
    params.delete("sign_type")
    sign = params.delete("sign")

    values = {}
    params.keys.sort.each do |k|
      values[k] = params[k];
    end

    sign.downcase == Digest::MD5.hexdigest(CGI.unescape(values.to_query) + ALIPAY_KEY)
  end
end

使用wizardly插件创建multi-step wizard

wizardly是一个非常容易使用的创建multi-step wizard的Rails插件,只需要三步就可以。

安装插件:

1
2
gem install wizardly
script/generate scaffold user first_name:string last_name:string age:integer gender:boolean

第一步:

1
2
3
class User < ActiveRecord::Base
  validation_group :step1, :fields => [:first_name, :last_name]
  validation_group :step2, :fields => [:age, :gender]

第二步:

1
2
class UsersController < ApplicationController
  act_wizardly_for :user

第三步:

1
script/generate wizardly_scaffold users

现在你就可以通过访问http://localhost:3000/users/step1来查看效果了。