Android学习笔记

Android学习笔记(1)-永远不变的Hello World

GoogleAndroid SDK发布也有一段时间了,一直想研究一下却苦于找不到时间。利用这个周未,开始强迫自己再次进入学习状态,原因很简单:我看好开放的gPhone

SDK的下载与安装并不复杂,网上也有不少同学已经进入状态了,我就不再重复了吧。

今天主要讨论的,还是永远不变的话题:Hello World.
1.最简单的HelloWorld

安装了SDK后,直接生成一个Android Project,一句代码不用写,就能跑出一个最简单的HelloWorld例程。我们看一下它的代码:

public void onCreate(Bundle icicle) ...{
        
super.onCreate(icicle);
        setTheme(android.R.style.Theme_Dark);
        setContentView(R.layout.main);    
}

看上去实在很简单,只有两句话而已。关键在这个R.layout.main上,凭直觉,这应该是定义的资源。的确,在R.java中只是定义了一个static int 而已,真正的资源描述在res/layout/main.xml文件里(注意:这里的R.java不要手工编辑,每次build project时它都会根据res下的资源描述被自动修改)。

<?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 id="@+id/txt"  
    android:layout_width
="fill_parent" 
    android:layout_height
="wrap_content" 
    android:text
="Hello World"
    
/>
</LinearLayout>

这个文件很好读,一个描述了这是一个线性排列的布局,android:orientation=vertical表示所有组件将纵向排布。而经典Hello World是用一个TextView来展示的。

由此,我们知道,Android的程序从一个Activity派生出来,并且从它的onCreate开始启动;Android里要显示的组件用XML文件描述而不用在代码中硬编码(这是一个好的习惯,我们应该从一开始就坚持下去);

2.Button来说Hello World
上面的例子是ADT自动生成的代码,似乎与我们一点关系也没有。那我们来改一下代码,因为在windows平台上的Helloworld经常是由一个按钮触发的,所以,我们想第二个Helloworld应该是这样的:加一个按钮和文本输入框,单击按钮后在原来的TextView后面加上输入框中输入的文字。
第一步是,增加一个Button和一个EditText,与TextView一样,它们也在main.xml里描述一下:

 <EditText id="@+id/edt"  
    android:layout_width
="fill_parent" 
    android:layout_height
="wrap_content" 
    android:text
=""
    
/>   
<Button id="@+id/go"
        android:layout_width
="wrap_content" android:layout_height="wrap_content" 
        android:text
="@string/go">
        
<requestFocus />
    
</Button>

这里有两个地方要注意:id=@+id/go,这表示需要一个唯一的UID来作为ButtonID,它的引用名是go。还有一个是android:text=@string/go表示这个按钮的文本不是直接写有main.xml里了,而是来源于另一个资源描述文件strings.xml里,本例中的strings.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    
<string name="app_name">helloTwo</string>
   
<string name="tit_dialog">提示</string>
    
<string name="msg_dialog">你好,中国</string>
    
<string name="ok_dialog">确定</string>
    
<string name="go">浏览</string> 
</resources>

然后,在代码里(onCreate函数中)我们加上以下代码(简单起见,用了嵌套类):

Button btn = (Button)findViewById(R.id.go);
        btn.setOnClickListener
(new View.OnClickListener()
        ...{
            
public void onClick(View v)
            ...{
                EditText edt=(EditText)helloTwo.
this.findViewById(R.id.edt);
                TextView txt= (TextView)helloTwo.
this.findViewById(R.id.txt);
                txt.setText(getString(R.string.msg_dialog)+edt.getText());    
            }
       
 });

为铵钮增加一个onClick事件处理器,在点击事件中,设置txt的文本为R.string.msg_dialgo+edt.getText()

这里的关键是两个函数的使用: findViewById(R.id.go)可以根据资源的名称加载View类型的资源,同样用函数getString(R.string.msg_dialog)可以加载字符串资源。

编译,run一下看看效果。
3.
再让菜单Say Hello

API文档中我们看到Activity中有两个函数:onCreateOptionsMenuonOptionsItemSelected,显示,这个OptionsMenu就是所谓的上下文菜单(在GPhone的模拟器上,有个键专用于弹出这个菜单)。下面我们就为这个HelloWorld例子加上一个菜单,并且让它可以Say hello

这次,我们不涉及到资源的描述文件了,而是直接使用这两个函数来实现,其实代码也很简单,所以,我们再增加一个退出应用的功能(否则每次都是按取消键退出应用显示太不专业了)。

代码如下:

  public boolean onCreateOptionsMenu(Menu menu)
    ...{
        
super.onCreateOptionsMenu(menu);
        menu.add(0,1,"say hello");
        menu.add(0,2,"exit");
        
return true;
    }
    
public boolean onOptionsItemSelected(Item item) 
    ...{
        
super.onOptionsItemSelected(item);
        
        
int id = item.getId();
        
switch(id)...{
        
case 1:
            AlertDialog.show(
this,getString(R.string.app_name), 
                           getString(R.string.msg_dialog), getString(R.string.ok_dialog), 
true);
            
break;
        
case 2:
            finish();
            
break;
        }

CreateOptionsMenu时,我们简单地增加两个菜单项,menu.add(ID,ID,显示文本),(注意:这里我直接将文字写在代码里,这并不提倡)。然后,在OptionsItemSelected事件中,我们根据选中的菜单项做相应处理,如果选中1,则弹出一个对话框显示资源文件中的你好,中国,如果选中2则退出应用。

AlertDialog.show是一个静态方法,类似于我们在WIN平台上经常使用的MessageBox一样,很方便的。

来源:http://www.sf.org.cn/Android/lumen/20976.html

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Android学习笔记(2)-初识Activity

根据文档的解释,ActivityAndroid开发中非常重要的一个基础类。我把它想像成J2ME中的Display类,或者是Win32平台上的Form类,也许不准确,但是它的重要性我觉得应该是一样的(当然,如果我们写的是一个没有界面的应用,例如后台运行的服务之类的,可以不用Display的)。

1.     在一个Activity中使用多个View

如果把Activity看作MVC中的Control?它负责管理UI和接受事件(包括用户的输入),虽然说一个Activity通常对应一个屏幕,但事实上,我们是可以只用一个Activity管理多个不同的View来实现简单的逻辑。
首先,我们增加一个新的资源描述layout/second.xml

<?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 id="@+id/txt"  
    android:layout_width
="fill_parent" 
    android:layout_height
="wrap_content" 
    android:text
="Hello 中国"
    
/>
 
<Button id="@+id/go2"
        android:layout_width
="wrap_content" android:layout_height="wrap_content" 
        android:text
="back">
        
<requestFocus />
    
</Button>   
</LinearLayout>

除了一个“Hello中国以外,增加一个按钮可以返回前一个界面。然后,在代码中我们要为helloTwo增加两个方法,setViewOneCommandsetViewTwoCommand,分别处理一下在不同界面时,从资源里加载组件并为组件绑定一个事件处理器。

  public void setViewOneCommand()
    ...{
        Button btn = (Button)findViewById(R.id.go);
        btn.setOnClickListener(
new View.OnClickListener()
        ...{
            
public void onClick(View v)
            ...{
                helloTwo.
this.setContentView(R.layout.second);
                helloTwo.
this.setViewTwoCommand();                
            }
        });       
        Button btnExit=(Button)findViewById(R.id.exit);
        btnExit.setOnClickListener(
new View.OnClickListener()...{
            
public void onClick(View v)...{
                helloTwo.
this.finish();
            }
        });    
    }
    
public void setViewTwoCommand()
    ...{
        Button btnBack=(Button)findViewById(R.id.go2);
        btnBack.setOnClickListener(
new View.OnClickListener()...{
            
public void onClick(View v)...{
                helloTwo.
this.setContentView(R.layout.main);
                helloTwo.
this.setViewOneCommand();
            }                          
        });
    }

最后,我们需要在onCreate的时候,也就是启动后的main界面上设置一下按钮事件处理器。新的onCreate方法如下:

    public void onCreate(Bundle icicle) ...{
        super.onCreate(icicle);
        setTheme(android.R.style.Theme_Dark);
        setContentView(R.layout.main);    
        setViewOneCommand();        
    }

编译,运行,OK

2.     还是回到正道上,多个Activity之间的跳转

Android中提供一个叫Intent的类来实现屏幕之间的跳转,按文档的说法,似乎他们也建议采用这种方法,Intent的用法比较复杂,现在我先看看它最简单的用法。

先在应用中增加两个Activity,这需要修改AndroidManifest.xml文件了,如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package
="cn.sharetop.android.hello.three">
    
<application android:icon="@drawable/icon">
        
<activity class=".HelloThree" android:label="@string/app_name">
            
<intent-filter>
                
<action android:value="android.intent.action.MAIN" />
                
<category android:value="android.intent.category.LAUNCHER" />
            
</intent-filter>
        
</activity>
        
<activity class=".HelloThreeB" android:label="@string/app_name">
        
</activity>
    
</application>
</manifest> 

很简单,就是加一个标签而已,新标签的class.HelloThreeB显示的应用标题与前一个Activity一样而已,然后第二步就是修改一个HelloThree类的实现,在onCreate方法中绑定按钮的事件处理器:

    public void onCreate(Bundle icicle) ...{
        
super.onCreate(icicle);
        setTheme(android.R.style.Theme_Dark);
        setContentView(R.layout.main);
        setViewOneCommand();
    }
    
public void setViewOneCommand()
    ...{
        Button btn = (Button)findViewById(R.id.go);
        btn.setOnClickListener(
new View.OnClickListener()
        ...{
            
public void onClick(View v)
            ...{
                Intent intent = 
new Intent();
                intent.setClass(HelloThree.
this, HelloThreeB.class);
                startActivity(intent);
                finish();            
            }
        });       
        Button btnExit=(Button)findViewById(R.id.exit);
        btnExit.setOnClickListener(
new View.OnClickListener()...{
            
public void onClick(View v)...{
                HelloThree.
this.finish();
            }
        });    
    } 

这里的跳转功能Intent来操作,它的最简单用法就是用函数setClass()设置跳转前后两个Activity类的实例,然后调用Activity自己的startActivity(intent)即可。最后一句finish()表示将当前Activity关掉(如果不关掉会如何?你可以自己试一下看效果,事实上有时我们是不需要关掉当前Activity的)。

然后,我们同样弄一个ActivityHelloThreeB,代码与前面的差不多,只是将setClass的两个参数反一下,这样就可以简单地实现在两个Activity界面中来回切换的功能了。

3.     如果我想在两个Activity之间进行数据交换,怎么办?

前例中的startActivity()只有一个参数,如果需要向新打开的Activity传递参数,我们得换一个函数了, Android提供了startSubActivity(Intent,int)这个函数来实现这个功能。

函数原型为: public void startSubActivity(Intent intent, int requestCode)

这里的requestCode用来标识某一个调用,一般由我们定义一个常量。

如何把参数传过去呢?Intent类在提供setClass()函数的同时也提供了一个setData()函数。

函数原型为public Intent setData(ContentURI data)

参数类型是ContentURI,它的详细内容下回再分析,现在就把它当成一个String类型来用吧。

参数带到新的Activity后,同样用Activity.getIntent()函数可以得到当前过来的Intent对象,然后用getData()就取到参数了。

把参数带回来的方法是Activity.setResult(),它有几个形式,现在先看最简单的一个吧。

函数原型是public final void setResult(int resultCode, String data)

resultCode是返回代码,同样用来标识一个返回类型,而data则是它要返回的参数。

在原来的Activity中的事件处理回调函数onActivityResult,会被系统调用,从它的参数里可以得到返回值。

函数原型为protected void onActivityResult(int requestCode, int resultCode,String data, Bundle extras)

这里的requestCode就是前面启动新Activity时的带过去的requestCode,而resultCode则关联上了setResult中的resultCodedata是参数,extras也是一个很重要的东西,后面再研究一下它的作用。

下面,我们来看一下代码吧,先看看HelloThree中的代码:

    public void setViewOneCommand()
    ...{
        Button btn = (Button)findViewById(R.id.go);
        btn.setOnClickListener(
new View.OnClickListener()
        ...{
            
public void onClick(View v)
            ...{
                
try
                ...{                    
                    Intent intent = 
new Intent();
                    intent.setClass(HelloThree.
this, HelloThreeB.class);
                    
intent.setData(new ContentURI("One"));                   
                    
startSubActivity(intent,REQUEST_TYPE_A);
                }
                
catch(Exception ex)...{}
            }
        });       
        Button btnExit=(Button)findViewById(R.id.exit);
        btnExit.setOnClickListener(
new View.OnClickListener()...{
            
public void onClick(View v)...{
                HelloThree.
this.finish();
            }
        });    
    } 
    
protected void onActivityResult(int requestCode, int resultCode,
            String data, Bundle extras)
    ...{
        
if (requestCode == REQUEST_TYPE_A) ...{
            
if (resultCode == RESULT_OK) ...{
                Log.v(TAG,data);
                TextView txt = (TextView)findViewById(R.id.txt);
                txt.setText(data);                
            }
        }
    }

这里的REQUEST_TYPE_A是我们定义的一个常量。在onActivityResult中用它与RESULT_OK一起作为条件判断如何处理返回值,这里只是简单将TextView显示值换成传来的字串。

再来看看另一个HelloThreeB类的实现代码:

    private Intent i;
    
protected void onCreate(Bundle icicle) ...{
        super.onCreate(icicle);
        setContentView(R.layout.second);    
        
        i = getIntent();
        
        android.util.Log.v(TAG,"onCreate");
        Button btn = (Button)findViewById(R.id.go);
        btn.setOnClickListener(
new View.OnClickListener()...{
            
public void onClick(View v)...{
                String result=HelloThreeB.this.i.getData().toString()+" And Two";
                
HelloThreeB.this.setResult(RESULT_OK,result);
                finish();
            }            
        });
        
        TextView v = (TextView)findViewById(R.id.txt);
        v.setText("Param is "+i.getData().toString());
        
    }

在按钮处理事件中,从Intent取出参数,处理一下再用setResult返回给前一个Activity即可。

编译运行即可。

来源:http://www.sf.org.cn/Android/lumen/20977.html

Android学习笔记(3)Activity的生命周期

注意到在ActivityAPI中有大量的onXXXX形式的函数定义,除了我们前面用到的onCreate以外,还有onStartonStop以及onPause等等。从字面上看,它们是一些事件回调,那么次序又是如何的呢?其实这种事情,自己做个实验最明白不过了。在做这个实验之前,我们先得找到在Android中的Log是如何输出的。

显然,我们要用的是android.util.log类,这个类相当的简单易用,因为它提供的全是一些静态方法:

Log.v(String tag, String msg);        //VERBOSE
Log.d(String tag, String msg);       //DEBUG   
Log.i(String tag, String msg);        //INFO
Log.w(String tag, String msg);     //WARN
Log.e(String tag, String msg);      //ERROR

前面的tag是由我们定义的一个标识,一般可以用类名_方法名来定义。

输出的LOG信息,如果用Eclipse+ADT开发,在LogCat中就可以看到,否则用adb logcat也行,不过我是从来都依赖于IDE环境的。

好了,现在我们修改前面的HelloThree代码

   public void onStart()
    ...{
        
super.onStart();
        Log.v(TAG,"onStart");
    }
    
public void onStop()
    ...{
        
super.onStop();
        Log.v(TAG,"onStop");
    }
    
public void onResume()
    ...{
        
super.onResume();
        Log.v(TAG,"onResume");
    }
    
public void onRestart()
    ...{
        
super.onRestart();
        Log.v(TAG,"onReStart");
    }
    
public void onPause()
    ...{
        
super.onPause();
        Log.v(TAG,"onPause");
    }
    
public void onDestroy()
    ...{
        
super.onDestroy();
        Log.v(TAG,"onDestroy");
    }
    
public void onFreeze(Bundle outState)
    ...{
        
super.onFreeze(outState);
        Log.v(TAG,"onFreeze");
    }

HelloThreeB中也同样增加这样的代码,编译,运行一下,从logcat分析输出的日志
在启动第一个界面Activity One时,它的次序是:

onCreate (ONE) - onStart (ONE) - onResume(ONE)
虽然是第一次启动,也要走一遍这个resume事件。然后,我们点goto跳到第二个Activity Two中(前一个没有关闭),这时走的次序是:

onFreeze(ONE) - onPause(ONE) - onCreate(TWO) - onStart(TWO) - onResume(TWO) - onStop(ONE)

说明,第二个Activity Two在启动前,One会经历一个:冻结、暂停的过程,在启动Two后,One才会被停止?

然后,我们再点back回到第一个界面,这时走的次序是:

onPause(TWO) - onActivityResult(ONE) - onStart(ONE) - onRestart(ONE) - onResume(ONE) - onStop(TWO) - onDestroy(TWO)

说明,返回时,Two没有经历冻结就直接暂停了,在One接收参数,重启后,Two就停止并被销毁了。

最后,我们点一下Exit退出应用,它的次序是:

onPause(ONE) - onStop(ONE) - onDestroy(ONE)

说明如果我们用了finish的话,不会有freeze,但是仍会经历pause - stop才被销毁。
这里有点疑问的是:为什么回来时先是Start才是Restart?可是文档中的图上画的却是先restartstart的啊?不过,后面的表格中的描述好象是正确的,start后面总是跟着resume(如果是第一次)或者restart(如果原来被stop掉了,这种情况会在startresume中插一个restart)。

下面不跑例子了,看看文档吧。

1.AndroidActivity Stack来管理多个Activity,所以呢,同一时刻只会有最顶上的那个Activity是处于active或者running状态。其它的Activity都被压在下面了。

2.如果非活动Activity仍是可见的(即如果上面压着的是一个非全屏的Activity或透明的Activity),它是处于paused状态的。在系统内存不足的情况下,paused状态的Activity是有可被系统杀掉的。只是不明白,如果它被干掉了,界面上的显示又会变成什么模样?看来下回有必要研究一下这种情况了。
3.
几个事件的配对可以比较清楚地理解它们的关系。CreateDestroy配成一对,叫entrie lifetime,在创建时分配资源,则在销毁时释放资源;往上一点还有StartStop一对,叫visible lifetime,表达的是可见与非可见这么一个过程;最顶上的就是ResumePause这一对了,叫foreground lifetime,表达的了是否处于激活状态的过程。

4.因此,我们实现的Activity派生类,要重载两个重要的方法:onCreate()进行初始化操作onPause()保存当前操作的结果。

除了Activity Lifecycle以外,Android还有一个Process Lifecycle的说明:

在内存不足的时候,Android是会主动清理门户的,那它又是如何判断哪个process是可以清掉的呢?文档中也提到了它的重要性排序:

1.最容易被清掉的是empty process,空进程是指那些没有Activity与之绑定,也没有任何应用程序组件(如Services或者IntentReceiver)与之绑定的进程,也就是说在这个process中没有任何activity或者service之类的东西,它们仅仅是作为一个cache,在启动新的Activity时可以提高速度。它们是会被优先清掉的。因此建议,我们的后台操作,最好是作成Service的形式,也就是说应该在Activity中启动一个Service去执行这些操作。

2.接下来就是background activity了,也就是被stop掉了那些activity所处的process,那些不可见的Activity被清掉的确是安全的,系统维持着一个LRU列表,多个处于backgroundactivity都在这里面,系统可以根据LRU列表判断哪些activity是可以被清掉的,以及其中哪一个应该是最先被清掉。不过,文档中提到在这个已被清掉的Activity又被重新创建的时候,它的onCreate会被调用,参数就是onFreeze时的那个Bundle。不过这里有一点不明白的是,难道这个Activitykilled时,Android会帮它保留着这个Bundle吗?

3.然后就轮到service process了,这是一个与Service绑定的进程,由startService方法启动。虽然它们不为用户所见,但一般是在处理一些长时间的操作(例如MP3的播放),系统会保护它,除非真的没有内存可用了。

4.接着又轮到那些visible activity了,或者说visible process。前面也谈到这个情况,被PausedActivity也是有可能会被系统清掉,不过相对来说,它已经是处于一个比较安全的位置了。

5.最安全应该就是那个foreground activity了,不到迫不得已它是不会被清掉的。这种process不仅包括resume之后的activity,也包括那些onReceiveIntent之后的IntentReceiver实例。

Android Application的生命周期的讨论中,文档也提到了一些需要注意的事项:因为Android应用程序的生存期并不是由应用本身直接控制的,而是由Android系统平台进行管理的,所以,对于我们开发者而言,需要了解不同的组件ActivityServiceIntentReceiver的生命,切记的是:如果组件的选择不当,很有可能系统会杀掉一个正在进行重要工作的进程。

来源:http://www.sf.org.cn/Android/lumen/20978.html

Android学习笔记(4)-学习Intent的使用

刚看到Intent的时候,我的确有点困惑:从字面上来说,它表示一种意图和目的;从使用上看,它似乎总是用于Activity之间的切换;而从它所在包android.content来看,它似乎与内容有关。所以,我想或许可以这样理解它: Intent类绑定一次操作,它负责携带这次操作所需要的数据以及操作的类型等。

如果是这样的话,是否可以将它与事件处理联想起来?即一个Intent类似于一个Event。从Intent的两个最重要的成员操作类型(Action)和数据(Data)来看,似乎是有道理的。文档中说,IntentAction的取值主要是一些定义好了的常量,例如PICK_ACTIONVIEW_ACTIONEDIT_ACTION之类的,而Data则是一个ContentURI类型的变量,这一点,我们前面提到过。

而且文档中说Intent分为两大类,显性的Explicit )和隐性的Implicit)。在前面的例子中,我们在两个Activity之间跳转时初步使用了Intent类,当时是用setClass来设置 Intent的发起方与接收方,它被称为显性的Intent,而隐性的Intent则不需要用setClasssetComponent来指定事件处理器,利用AndroidMenifest.xml中的配置就可以由平台定位事件的消费者。

一般来说,intent要定位事件的目的地,无外乎需要以下几个信息:

1.种类(category,比如我们常见的 LAUNCHER_CATEGORY 就是表示这是一类应用程序。

2.类型(type,在前面的例子中没用过,表示数据的类型,这是隐性Intent定位目标的重要依据。

3.组件(component,前面的例子中用的是setClass,不过也可以用setComponent来设置intent跳转的前后两个类实例。

4.附加数据(extras,在ContentURI之外还可以附加一些信息,它是Bundle类型的对象。
Implicit Intent
的使用相对有点麻烦,我们来做一个例子。首先,我们需要增加一个类:HelloThreeProvider,它必须实现于ConentProvider接口,所以代码如下:

public class HelloThreeProvider extends ContentProvider ...{

    
public boolean onCreate() ...{
        
return true;
    }
    
    
public int delete(ContentURI url, String where, String[] whereArgs) ...{
        
return 0;
    }
    
public ContentURI insert(ContentURI url, ContentValues initialValues)...{
        
return url;
    }
    
public Cursor query(ContentURI url, String[] projection, String selection,
            String[] selectionArgs, String groupBy, String having, String sort) ...{
        
return null;
    }

    public int update(ContentURI url, ContentValues values, String where, String[] whereArgs) ...{
        
return 0;
    }
    
    
public String getType(ContentURI url) ...{
        
return "vnd.sharetop.hello.three/vnd.hello.three";
    }

}

这里面有一堆方法要实现,因为它们都是ContentProvider中的abstract方法,但是今天的例子中它们多半没有什么用处,只是一个getType方法我们让它不管什么url都返回一个表示Intent所携带的数据类型是我们定义的一个长字串:vnd.sharetop.hello.three/vnd.hello.three
然后,在AndroidMenifest.xml中我们将上面这个HelloThreeProvider类加入应用程序:

<application android:icon="@drawable/icon">
        
<provider class="HelloThreeProvider" android:authorities="cn.sharetop.android.hello" />                
        
<activity class="HelloThree" android:label="@string/app_name">
            
<intent-filter>
                
<action android:value="android.intent.action.MAIN" />
                
<category android:value="android.intent.category.LAUNCHER" />
            
</intent-filter>
        
</activity>
        
<activity class="HelloThreeB" android:label="bbb">
        
<intent-filter>
             
<action android:value="android.intent.action.VIEW" />
             
<category android:value="android.intent.category.DEFAULT" />
             
<type android:value="vnd.sharetop.hello.three/vnd.hello.three" />
        
</intent-filter>
        
</activity>
    
</application>

相对于前面的例子,主要修改了HelloThreeB的配置,包括增加了一个<category>标签表示这是一个一般性的activity而已。增加了<action>标签,定义它负责处理VIEW_ACTION类型的操作。增加了<type>标签给出一个数据类型的定义串vnd.sharetop.hello.three/vnd.hello.three。最主要的是在<application>下增加的那个<provider>标签,有个authorities属性,我们给的值是cn.sharetop.android.hello,待一会我们再说它的用处。

最后就是修改以前的跳转代码如下:

Intent intent = new Intent();
  intent.setData(
new ContentURI("content://cn.sharetop.android.hello/one"));
  intent.setAction(intent.VIEW_ACTION);
  startActivity(intent);

现在我们的setData里的东西可与以前不一样的,是吧?注意到它的格式了吗?content://是个协议头,固定这样写就行了。然后就是那个authorities中定义的串了,再后面就是我们自定义的东西了,我这里很简单的写个one,其它还可以更长一点,如one/101之类的。它负责去关联上那个provider类。另外,增加了setAction的调用设置操作为VIEW_ACTION,与Menifest中的<action>又挂上了。Android平台负责根据IntentData信息中的authorities,找到ContentProvider,然后getType,用typeintent中的Action两个信息,再找到可以处理这个intent的消费者。

OK,编译运行。

其实,如果是在一个应用内部,这种隐性的intent实在有点别扭,个人觉得,这种松藕合的实现方法,只适用于那些较大的系统或者多个不同的应用之间的调用,可手机上又有什么较大的系统呢?无非是可以与不同来源的多个应用之间方便地互操作而已,那么会是什么样的场景呢?比如,给QQ好友发送gmail邮件,用GoogleMap查找QQ好友所在的位置?看上去挺不错的。

关于这个ContentProvider,其实还有话说,它主要是的那些看似数据库操作的方法我们都没真正去实现呢。不过今天就到这里了,等下回再去研究吧。

来源:http://www.sf.org.cn/Android/lumen/20979.html

 

Android学习笔记(5)-关于ListActivity的简单体验

今天学习点轻松的内容吧,看看android.app包里的几个类。首先是这个在平台自的例子中被广泛使用的ListActivity。这个类其实就是一个含有一个ListView组件的Activity类。也就是说,如果我们直接在一个普通的Activity中自己加一个ListView也是完全可以取代这个ListActivity的,只是它更方便而已,方便到什么程度呢?来做个例子瞧瞧。

public class HelloTwoB extends ListActivity...{    
    public void onCreate(Bundle icicle) ...{
        super.onCreate(icicle);
        setTheme(android.R.style.Theme_Dark);
        setContentView(R.layout.mainb);        
        List<String> items = fillArray();        
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,R.layout.list_row,items);
               
        this.setListAdapter(adapter);
    }
    private List<String> fillArray()    ...{
        List<String> items = new ArrayList<String>();
        items.add("
日曜日");
        items.add("
月曜日");
        items.add("
火曜日");
        items.add("
水曜日");
        items.add("
木曜日");
        items.add("
金曜日");
        items.add("
土曜日");
        return items;
    }
    @Override
    protected void onListItemClick(ListView l, View v, int position, long id)
    ...{
        TextView txt = (TextView)this.findViewById(R.id.text);
        txt.setText("
あすは "+l.getSelectedItem().toString()+"です。");
    }
}

的确可以简单到只需准备一个List对象并借助Adapter就可以构造出一个列表。重载onListItemClick方法可以响应选择事件,利用第一个参数可以访问到这个ListView实例以得到选中的条目信息。这里有一点要说明的,就是如果更简单的话,其实连那个setContentView都可以不要了,Android也会自动帮我们构造出一个全屏的列表。但是本例中我们需要一个TextView来显示选中的条目,所以我们需要一个layout.mainb描述一下这个列表窗口。

<?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 id="@+id/text"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text=""
    />
<ListView id="@id/android:list"
    android:layout_width="fill_parent"
    android:layout_height="0dip"
    android:layout_weight="1"
    android:drawSelectorOnTop="false"
    />
</LinearLayout>

这里需要注意的是那个ListViewID,是系统自定义的android:list,不是我们随便取的,否则系统会说找不到它想要的listview了。然后,在这个listview之外,我们又增加了一个TextView,用来显示选中的条目。

再来说说这里用到的ArrayAdapter,它的构造函数中第二个参数是一个资源IDArrayAdapterAPI文档中说是要求用一个包含TextViewlayout文件,平台用它来显示每个选择条目的样式,这里的取值是R.layout.list_row,所以,我们还有一个list_row.xml文件来描述这个布局,相当简单。

<?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 id="@+id/item"
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"/>
<TextView id="@+id/item2"
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"/>
</LinearLayout>

ArrayAdapter上溯到BaseAdapter,发现还有几个同源的Adapter也应该可以使用,象SimpleAdapterCursorAdapter,还是做个例子来实验一下吧。

先看看SimpleAdapter,说是simple却不simple

首先看看这个fillMaps方法,基本上就明白这个simpleAdapter是怎么回事了,在有些场合它还是挺有用的,可以为每个条目绑定一个值:

private List<HashMap<String, String>> fillMaps()
    ...{
        List<HashMap<String, String>> items = new ArrayList<HashMap<String,String>>();
       
        HashMap<String,String> i = new HashMap<String,String>();
        i.put("name","
日曜日");
        i.put("key", "SUN");
        items.add(i);
        HashMap<String,String> i1 = new HashMap<String,String>();
        i1.put("name","
月曜日");
        i1.put("key", "MON");
        items.add(i1);
        HashMap<String,String> i2 = new HashMap<String,String>();
        i2.put("name","
火曜日");  
        i2.put("key", "TUE");
        items.add(i2);
        HashMap<String,String> i3 = new HashMap<String,String>();
        i3.put("name","
水曜日");
        i3.put("key", "WED");
        items.add(i3);
        HashMap<String,String> i4= new HashMap<String,String>();
        i4.put("name","
木曜日");
        i4.put("key", "THU");
        items.add(i4);
        HashMap<String,String> i5 = new HashMap<String,String>();
        i5.put("name","
金曜日");
        i5.put("key", "FRI");
        items.add(i5);
        HashMap<String,String> i6 = new HashMap<String,String>();
        i6.put("name","
土曜日");
        i.put("key", "SAT");
        items.add(i6);

        return items;
    }

然后,在HelloTwoB中的onCreate函数中,修改代码,有几个不同:items的元素是HashMap实例,这是一点变化,然后构造函数除了要求items以外,还要求提供一个string[]来说明用hash表中的哪个字段显示在列表中,而后是一个资源ID的数组。我的代码是这样的:

//SimpleAdapter demo
List<HashMap<String, String>> items = fillMaps();
SimpleAdapter adapter=
new SimpleAdapter(this,items,R.layout.list_row,new String[]...{"name"},new int[]...{R.id.item});

编译跑一下可以看到结果了,是吧?只是显示的文字不太对,再改一下:

protected void onListItemClick(ListView l, View v, int position, long id)
    ...{
        TextView txt = (TextView)
this.findViewById(R.id.text);
        txt.setText("
あすは "+((HashMap)l.obtainItem(position)).get("key").toString()+"です。");
    }

这样就好多了,其实一般情况下我们都是用ListView中的obtainItem取得当前选中的条目,然后转成List中的对应类型来使用的。

上面的例子中只显示name对应的值,其实你也可以试一下这样:

SimpleAdapter adapter=new SimpleAdapter(this,items,R.layout.list_row,new String[]...{"name","key"},new int[]...{R.id.item,R.id.item2});

看看是什么效果。

再看看那个CursorAdapter吧,它的列表中元素要求是Cursor,这东西与DB有关,不过最简单的DB就是通讯簿。先从Contacts.People入手吧,同样修改代码:

//CursorAdapter demo

Cursor mCursor = this.getContentResolver().query(Contacts.People.CONTENT_URI, nullnullnullnull);
SimpleCursorAdapter adapter=
new SimpleCursorAdapter(this,R.layout.list_row,mCursor,new String[]...{Contacts.People.NAME},new int[]...{R.id.item});

因为单纯的CursorAdapter是抽象类,所以我用的是它的子类SimpleCursorAdapter,很好理解,先用ContentResolver查询通讯簿得到一个游标,然后告诉SimpleCursorAdapter要用其中的People.NAME作为显示项来构造出一个adapter即可。

现在的onListItemClick也不一样了,如下:

    protected void onListItemClick(ListView l, View v, int position, long id)
    ...{
        TextView txt = (TextView)
this.findViewById(R.id.text);
        Cursor c = (Cursor)l.obtainItem(position);
        txt.setText("SEL = "+c.getString(c.getColumnIndex(Contacts.People.NUMBER)));
    }

这里同样是先用obtainItem取到游标,然后用从记录中取出想要的字段显示即可。在做这个例子时,因为权限的问题我们还得修改一下AndroidManifest.xml文件,让我们的应用可以访问到通讯簿:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package
="cn.sharetop.android.hello.two">
    
<uses-permission id="android.permission.READ_CONTACTS" />
    
<application android:icon="@drawable/icon">
 ... ...

来源:http://www.sf.org.cn/Android/lumen/20980.html

Android学习笔记(6)—关于Dialog的简单体验

继续android.app中的几个类的学习,今天的内容是那几个Dialog的体验。

注意到android.app包下除了Dialog(可用于制作复杂的对话框)以外,还包括了几个系统定义好的对话框类,如DatePickerDialogTimePickerDialogAlertDialog

其中AlertDialog我上回用过一次,基本上就那样子了,今天看看另外两个对话框的使用吧。

首先是DatePickerDialog类,修改代码如下:

public class HelloTwoC extends Activity implements OnClickListener, OnDateSetListener ...{
 
public HelloTwoC() ...{
   
super();
 }
 
public void onCreate(Bundle icicle) ...{
     
super.onCreate(icicle);
     setTheme(android.R.style.Theme_Dark);
     setContentView(R.layout.mainc);
        
     Button btn = (Button)findViewById(R.id.date);
     btn.setOnClickListener(
this);        
 }
 @Override
 
public void onClick(View v) ...{
   Calendar d = Calendar.getInstance(Locale.CHINA);
   d.setTime(
new Date());
   DatePickerDialog dlg=
new DatePickerDialog(this,this,d.get(Calendar.YEAR),d.get(Calendar.MONTH),d.get(Calendar.DAY_OF_MONTH),d.get(Calendar.DAY_OF_WEEK));
   dlg.show(
); 
 }
 @Override
 public void dateSet(DatePicker dp, int y, int m, int d) ...{
  TextView txt = (TextView)findViewById(R.id.text);
  txt.setText(Integer.toString(y)+"-"+Integer.toString(m)+"-"+Integer.toString(d));
 }
}

很简单的,无非是需要一个OnDateSetListener接口的实现而已,在它里面的dateSet方法中就可以得到选择的日期了。而TimePickerDialogDatePickerDialog使用如出一辙,就不多说了。

看看另一个ProgressDialog的用法吧,这个类与AlertDialog一样包含了多个static的方法,所以使用起来是非常方便的。比如说,如果我们需要用它来表示一个长时间的操作,很简单的用一句话就可以了:

ProgressDialog.show(this,null, "operation running...",true,true);

URLhttp://www.sf.org.cn/Android/lumen/20981.html

Android学习笔记(7)—关于ServiceNotification的体验

大略地看了一下android.app下的Service类,觉得它与Activity非常相似,只是要注意几个地方:

1.生命周期,Service的从onCreate()->onStart(int,Bundle)->onDestroy()显得更为简单。但是它的onStart是带参数的,第一个ID可用来标识这个service,第二个参数显示是用来传递数据的了。比较Activity,传递数据的Bundle是在onCreate就带进入的。

2.Service的启动由Context.startService开始,其实Activity或者Service都是Context的派生类。结束于Context.stopService()或者它自己的stopSelf()

3.Service还有一个与Activity不一样的是它可以由另一个Context去绑定一个已存在的Service。就是这个方法Context.bindService(),被绑定的Service要求是已经onCreate了但可以没有onStart。在Service类中有个抽象方法getBinder()可以得到这个IBinder对象。关于这方面的细节,以后再看,这里只做个记录罢。

4.Service有关的还有一个安全的问题,可以在AndroidManifest.xml中用<uses-permission>标签来声明一个Service的访问权限,关于Android的安全问题也留待以后再解决吧。

我一直相信一种水到渠成的学习方法,先从最简单的东西入手,就不会觉得学习很枯燥了。

下面来做个例子。

修改AndroidManifest.xml文件,增加一个Activity和一个Service

        <activity class=".HelloTwoD" android:label="hello_two_d">
        
</activity>
        
<service class=".HelloTwoDService" />

HelloTwoD.java的代码比较简单,如下:

public class HelloTwoD extends Activity implements OnClickListener
...
 
public HelloTwoD()
    ...{
  
super();
    }
 
public void onCreate(Bundle icicle) ...{
        
super.onCreate(icicle);
        setTheme(android.R.style.Theme_Dark);
        setContentView(R.layout.maind);
        
        Button btn = (Button)findViewById(R.id.btnTest);
        btn.setOnClickListener(
this);
    }
 
 @Override
 
public void onClick(View arg0) ...{
  
// 用一个显式的Intent来启动服务 
  Intent i = new Intent();
  i.setClass(
this, HelloTwoDService.class);
  
//带上我的名字
  Bundle b= new Bundle();
  b.putString("name", "sharetop");
  
this.startService(i,b);
 }
 
}

当然要启动这个HelloTwoD,也需要在我最初的那个HelloTwo中加一点代码(我就不罗嗦了)。再看看那个HelloTwoDService是如何实现的:

public class HelloTwoDService extends Service ...{
 
public Timer timer;
 
public final String TAG="HelloTwoDService_TAG";
 
public void onCreate() ...{
        
super.onCreate();
        
        Log.d(TAG,"onCreate");
        
        timer = 
new Timer(true);

    }
 @Override
 
public IBinder getBinder() ...{
  
// TODO Auto-generated method stub
  return null;
 }
 
public void onStart(int startId, Bundle arg)
 ...
  
//看看startId是什么内容 
  if(arg!=null)
   Log.d(TAG,"onStart "+Integer.valueOf(startId).toString()+" from "+arg.getString("name"));
  
else
   Log.d(TAG,"onStart with null Bundle");
  
        timer.schedule(
new TimerTask()...{
         
public void run()...{
          
//表示一下我的存在
          Log.d(TAG,"say from a timer.");
          
//停掉自己这个服务
          HelloTwoDService.this.stopSelf();
         }         
        },5000);
 }
 
public void onDestroy()
 ...{
  Log.d(TAG,"onDestroy");
 }
}

这里我用一个定时器timer来延时5秒钟显示消息,否则立即就显示出来觉得不象一个后台服务了。用日志输出那个onStart中的startId看看,原来只是一个标识而已。

下面来个简单的NotificationManager吧,看了看API文档,觉得最简单地恐怕就是那个NotificationManager.notifyWithText()了,修改上面的run方法如下:

        timer.schedule(new TimerTask()...{
         
public void run()...{

          NotificationManager manager=(NotificationManager)getSystemService(NOTIFICATION_SERVICE);
          manager.notifyWithText(1001, "
わたしはSHARETOPです。", NotificationManager.LENGTH_LONG, null);

          HelloTwoDService.this.stopSelf();
         }         
        },5000);

再试试看效果。太简单了,Notification主要是用于后台服务用来通知前台,所以,Android提供了三类不同的通知方式,notifyWithText可以简单地显示一个字串,而notifyWithView稍复杂点,可以有一个view来构造这个显示信息框,而最灵活的就是那个notify(int id, Notification notification)了,参数notification是类Notification的实例。

修改一下刚才的那个run方法,如下:

        timer.schedule(new TimerTask()...{
         
public void run()...{
          
          NotificationManager manager=(NotificationManager)getSystemService(NOTIFICATION_SERVICE);          
          Notification nf = 
new Notification(R.drawable.icon,"这是信息的详细描述",null,"信息的标题",null);
          manager.notify(0,nf);          
          
          HelloTwoDService.
this.stopSelf();
         }         
        },5000);

这里创建一个Notification的实例nf,构造函数的第一个参数是那个显示在状态栏(也就是Android手机上面的那一条显示信号强度、电池电量等信息的位置)的图标。后面可以有

一个标题和点击以后的详细信息,这是字串形式,还可以有一个Intent用来表示点击后可以发生一个跳转行为。

URLhttp://www.sf.org.cn/Android/lumen/20982.html

Android学习笔记(8) — GridViewImageView

简单一点吧,就瞧瞧那个Grid的效果,Android提供了一个GridView,不过从APIDemo中看来,它似乎与PC上的GRID差别还是挺大的,更像那个IconView的感觉。不知道Android中如何实现表格界面?虽然在移动终端上,表格一般不会有谁使用,大家似乎更倾向于使用ListView,而Android对于ListView则有更简单的实现ListActivity

废话不说,还是自己写几句代码来实验一下。

<GridView id="@+id/grid"
    android:layout_width
="fill_parent" 
    android:layout_height
="fill_parent"
    android:padding
="10dip"
    android:verticalSpacing
="10"     
    android:horizontalSpacing
="10"
    android:numColumns
="auto_fit"
    android:columnWidth
="60"
    android:stretchMode
="columnWidth"     
    android:gravity
="center"
    
/>     

 从描述文件中的这些属性来看,与表格非常类似,除了paddingspacing以外,它还多了那个gravity,这里是center表示单元格中的内容居中放,在类GridView中也提供了方法setGravity(int)来实现这个效果。

接着,我们沿用以前那个fillMaps方法来构造SimpleAdapter,以前将这个adapter赋给ListActivity,现在同样的Adapter,却是赋给了GridView,效果又会是怎样呢?

List<HashMap<String, String>> items = fillMaps();

GridView grd=(GridView)this.findViewById(R.id.grid);
SimpleAdapter adapter=
new SimpleAdapter(this,items,R.layout.list_row,new String[]...{"name"},new int[]...{R.id.item});
grd.setAdapter(adapter);

我觉得GridView并不象表格,倒更象IconView,下面试试用图像作为GridView的内容。现在,不能用简单Adapter了,得自己弄一个ImageAdapter,就让它衍生于BaseAdapter类吧。

public class ImageAdapter extends BaseAdapter ...{
        
//这是资源ID的数组
        private Integer[] mThumbIds = ...{
                R.drawable.a,R.drawable.b,R.drawable.c,
                R.drawable.d,R.drawable.e,R.drawable.f,
                R.drawable.g,R.drawable.h,R.drawable.i
                };

        public ImageAdapter(Context c) ...{
            mContext = c;
        }

        public int getCount() ...{
            
return mThumbIds.length;
        }

        public Object getItem(int position) ...{
            
return position;
        }

        public long getItemId(int position) ...{
            
return position;
        }

        public View getView(int position, View convertView, ViewGroup parent) ...{
            ImageView i = 
new ImageView(mContext);
            
//设置图像源于资源ID
      i.setImageResource(mThumbIds[position]);
            i.setAdjustViewBounds(
true);            
            i.setBackground(android.R.drawable.picture_frame);

            return i;
        }

        private Context mContext;

    }

很简单,只要重载几个方法就可以了,关键是那个getView方法,它负责构建出每个单元格中的对象实例。这里我们构造的是一个ImageView实例。

然后就是同样的将这个Adapter赋给GridView即可,大家可以看看效果,注意在做这个例子前,先放几个小图片到res/drawable目录下,buildproject一下就可以得到那个R.drawable.a了(这里的a是图像文件名,如a.png)。

getView方法中我们使用了ImageView类,这又是一个widget。除了上面用到的几个方法以外,还有以下几个方法值得注意:

与图像来源有关的方法,我们只用了资源文件的方式。

    //不同的图像来源
public void setImageBitmap(Bitmap bm) 
public void setImageDrawable(Drawable drawable) 
public void setImageResource(int resid) 
public void setImageURI(ContentURI uri) 

图像效果的操作。

//颜色过滤
public void setColorFilter(int color, Mode mode) 
//矩阵变换
public void setImageMatrix(Matrix matrix) 
//透明度
public void setAlpha(int alpha) 

具体的使用可以参考API,动手试一下就差不多了。

URLhttp://www.sf.org.cn/Android/lumen/20983.html

Android学习笔记(9)-开始做一个数独游戏[]

不想再写Hello123了,今天开始做一个数独小游戏,因为这个游戏比较简单应该容易上手,就作为我学习Android之后的第一个程序比较合适。

初步的设计是只有一个界面(如下图),然后用绿色字体表示题目中有的固定的数字,黄色字体显示玩家输入的数字,而红色字体则是程序判断输入错误后的显示。另外模式分为三种:普通写入、标记(玩家用蓝色小方块标记当前单元格可以输入的数字)、排除模式(玩家指定数字,游戏自动判断一下这个数字肯定不能输入的单 元格,将它反相显示出来)。

准备工作就是做一张背景图(棋盘)和三套数字小图片(红、绿、黄)即可。

首先建立工程sudo,程序主体类 MainActivity以后,再修改一下那个main.xml文件,去掉TextView标签即可。因为我们会自己定义一个View,所以不再需要它了。程序不大,所以不打算做过多的类,下面把几个类的分工描述一下:

1MainActivity,主体类,负责处理键盘事件和维护一个题库。

2MainView,显示类,负责维护并显示当前棋局,它的核心在于它的onDraw函数。

3GridCellQuestion两个实体类,分别描述了棋盘单元格的信息和题目信息。

4Helper类,助手类,封装一些函数,如读写记录、自动解题函数等。

MainActivity中的onCreate中,加几句话就可以让游戏全屏显示了。如下:

setTheme(android.R.style.Theme_Dark);
requestWindowFeature(Window.FEATURE_NO_TITLE);          

getWindow().setFlags(WindowManager.LayoutParams.NO_STATUS_BAR_FLAG,WindowManager.LayoutParams.NO_STATUS_BAR_FLAG);       

主要来看看MainView类的代码吧,它的onDraw负责显示当前棋局,涉及到的API主要是android.graphics中的CanvasPaint

一是显示图片的方法,因为图片来源于资源,所以显示它的代码如:

Bitmap bmp = BitmapFactory.decodeResource(this.getResources(),R.drawable.grid);
canvas.drawBitmap(bmp, 0, 0, 
null);

这是显示背景,如果是数字呢,如何将数字1R.drawable.a1资源关联呢?

private int[] thumbNormal=new int[]...{0,
        R.drawable.a1,R.drawable.a2,R.drawable.a3,R.drawable.a4,R.drawable.a5,    
        R.drawable.a6,R.drawable.a7,R.drawable.a8,R.drawable.a9
    };

然后就简单地加载即可了。

Bitmap b = BitmapFactory.decodeResource(this.getResources(),this.thumbNormal[this.grid[i].value]);
canvas.drawBitmap(b, xx, yy, 
null);

二是显示文本的方法,刚才显示图像的drawBitmap中最后一个参数直接给了null,因为我们实在没有什么效果需要给图像的,但是文本则不同,我们用Paint来控制文本的样式。

Paint paintText=new Paint();
paintText.setFlags(Paint.ANTI_ALIAS_FLAG);
paintText.setColor(Color.WHITE);
... ...
canvas.drawText(Long.toString(
this.ti.code), xx, yy, paintText);

三是画一下框的方法,同样是用Paint来做的。

Paint paintRect = new Paint();
paintRect.setColor(Color.RED);
paintRect.setStrokeWidth(2);
paintRect.setStyle(Style.STROKE);
            
Rect r=
new Rect();
r.left=
this.curCol*CELL_WIDTH+GRID_X;
r.top=
this.curRow*CELL_WIDTH+GRID_Y;
r.bottom=r.top+CELL_WIDTH;
r.right=r.left+CELL_WIDTH;
            
canvas.drawRect(r, paintRect);

如果不setStyleStyle.STROKE,则缺省为填充模式。

四是反相显示的方法,更简单了,就是一句话了:

Paint paintHint=new Paint();
paintHint.setXfermode(
new PixelXorXfermode(Color.WHITE));

URLhttp://www.sf.org.cn/Android/lumen/20984.html

Android学习笔记(10)-开始做一个数独游戏[]

继续,今天讨论的是记录文件的读写。因为原来在Brew平台上实现的数独将题库是一个二进制文件,所以在Android就直接拿那个文件来用了。

 计划实现两个函数,先是LoadTiList(),加载题库,先装题库文件放在资源里,然后从资源里加载它作为一个DataInputStream即可。代码也没几行,如下:

          public static boolean LoadTiList(MainActivity me) ...{
        DataInputStream 
in = null;
        
try
        ...{
            
in = new DataInputStream(me.getResources().openRawResource(R.raw.ti));
            
            
byte[] bufC4=new byte[4];
            
byte[] bufC81=new byte[81];
            
            
//总个数            
            in.read(bufC4,0,4);
            
int len=((int)bufC4[3]<<24)+((int)bufC4[2]<<16)+((int)bufC4[1]<<8)+(int)bufC4[0];
            
            
for(int i=0;i<len;i++)
            ...{
                Question ti = 
new Question();
                                
                
//代码
                in.read(bufC4,0,4);
                ti.code=(
long) (((long)bufC4[3]<<24)+((long)bufC4[2]<<16)+((long)bufC4[1]<<8)+(long)bufC4[0]);
                
//时间
                in.read(bufC4,0,4);                
                SharedPreferences sp = me.getPreferences(Context.MODE_WORLD_READABLE);
                ti.time=sp.getLong(Long.toString(ti.code), 0);
                
//数据
                in.read(bufC81,0,81);
                
for(int j=0;j<81;j++)ti.data[j]=bufC81[j];
                me.tiList.add(ti);
            }
            
in.close();
        }
        
catch(Exception ex)...{
            
return false;
        }
        
finally...{
            
try...{in.close();}catch(Exception e)...{}
        }
        
return true;
    }

这里最麻烦的是因为java里没有unsigned类型,所以会溢出,比较郁闷,这个没有解决,只能是生成题库文件里注意一下了,不能与brew平台共用那个题库文件了。

二是保存记录,在brew平台我直接用一个文件搞定,读写它,但是android不能这样了,因为ti.dat是从资源中加载的,所以只能是静态的,不可修改,那记录只能放入preferences中了,代码如下:

    public static boolean SaveTiList(MainActivity me)
    ...{
        
try
        ...{
            SharedPreferences sp=me.getPreferences(Context.MODE_WORLD_WRITEABLE);
            Question ti = me.gridView.ti;
            sp.edit().putLong(Long.toString(ti.code),ti.time);
            sp.edit().commit();

        }
        
catch(Exception ex)...{            
            
return false;
        }
        
return true;
    }

SharePreferences可以按key-value来保存记录,所以key用题目的code,则value就是解它所用的时间了。

Android不能直接访问app目录下的文件,所以不能够象brew那样将数据文件放在程序目录下供它读写,而在Activity中提供的两个函数 openFileOutputopenFileInput,虽可用来读写文件,但是总是不太方便。

另外,用SQLite其实也不方便,因为手机中弄这些东西,事实上用处不大。

URL: http://www.sf.org.cn/Android/lumen/20985.html

Android学习笔记(11)-开始做一个数独游戏[]

继续,最后再讨论一下定时器的实现。

本来很简单的一件事,直接用java.util.timer应该就够用了,但是发现在它的task中无法去invalidate我们的MainView,很郁闷。这一点的处理说明 Android还是相对线程安全的。

折腾良久,明白了非得再做一个Handler,才能在线程中操作界面元素。所以,代码比brew复杂了一点。

先还是用TimerTimerTask来做,如下:

public TimerHandler timerHandler;

public Timer timer;
public MyTimerTask    task;

... ...

timer=
new Timer(true);
task=
new MyTimerTask(this);

... ...

那个MyTimerTaskMainActivity的一个内嵌类,实现如下:

    private class MyTimerTask extends TimerTask
    ...{
        
private MainActivity me;
        
private int a=0;
        
        
public MyTimerTask(MainActivity p)...{
            me=p;
        }
        
public void run()...{
            me.gridView.time++;                
            Log.d("MyTask",Integer.toString(me.gridView.time));    

            timerHandler.sendEmptyMessage(0);            
        }
    }    

这里做两件事,一是将gridView中的time加一,二是发送一个消息通知timerHandler。原来我在这里直接让MainView去刷新屏幕,发现不行,所以就改成这样处理了。

然后就是如何实现TimerHandler类的,也不复杂,就是让它去刷新一下屏幕即可。

public class TimerHandler extends Handler ...{

    
private MainView me;
    
public TimerHandler(MainView m)...{
        me=m;
    }
    
    @Override
    
public void handleMessage(Message msg) ...{
        Log.d("Ti",msg.toString());
        me.invalidate();
    }

}

如此一来,就顺了。

MainView中的onDraw,根据当前的time值显示成00:00:00的格式即可。

另外,发现Android的模拟器运算速度不如BREW的模拟器,相当的慢。

URL: http://www.sf.org.cn/Android/lumen/20986.html

Android学习笔记(12)-开始做一个数独游戏[补充]

再补充一点吧,如果需要给游戏加上背景音乐,其实也是非常容易的事情。因为Android提供了一个 MediaPlayer类可以方便的播放音乐文件。

android.media.MediaPlayer类没有构造函数,一般是用它的静态方法create生成实例,简单地告诉它音乐文件的资源ID即可(支持mp3/wav/midi等)。

首先,我们得建立一个Service,就叫MediaService吧,它的代码如下:

Android/UploadFiles_8448/200804/20080419152842539.gif" align=top> Android/UploadFiles_8448/200804/20080419152843671.gif" align=top>public class MediaService extends Service implements MediaPlayer.OnErrorListener,MediaPlayer.OnCompletionListener,MediaPlayer.OnPreparedListener ...{
      
      ... ...    

    
private MediaPlayer player;
    
    @Override
    
protected void onDestroy() ...{
        
// TODO Auto-generated method stub
        super.onDestroy();
        
if(player!=null)...{
            player.stop();
            player.release();
        }
    }

    @Override
    
protected void onStart(int startId, Bundle arguments) ...{
        
// TODO Auto-generated method stub
        Log.d("Media","onStart");
            
        player=MediaPlayer.create(
this.getApplication(),R.raw.tonhua);
        
if(player!=null)...{
            player.setAudioStreamType(AudioSystem.STREAM_MUSIC);
            
            player.setOnCompletionListener(
this);
            player.setOnPreparedListener(
this);
            player.setOnErrorListener(
this);
            
            player.prepareAsync();
        }
    }

    @Override
    
public void onCompletion(MediaPlayer arg0) ...{
        
// TODO Auto-generated method stub
        Log.d("Media","finished.");
    }

    @Override
    
public void onPrepared(MediaPlayer arg0) ...{
        
// TODO Auto-generated method stub
        Log.d("Media","prepared.");
        player.start();
    }
    
    @Override
    
public void onError(MediaPlayer arg0,int what, int extra) ...{
                
        Log.d("Media","onError");
        player.stop();
    }
}

这个服务主要就是用一个MediaPlayer去播放资源中的tonghua(一首MP3音乐)。次序一般是先create出这个实例,然后prepare一下(如果是文件直接prepare,如果是流则最好异步parepareAsync),接着就可以start了,同步可以直接start,异步则必须放到onPrepared中再start

MainActivity中启动这个服务即可。

mediaServiceIntent= new Intent();
mediaServiceIntent.setClass(
this, MediaService.class);
        
 
this.startService(mediaServiceIntent, new Bundle());

当前,在Activity停止时也别忘了将这个Service停掉,而在Service停止时关掉MediaPlayer

在模拟器上试了,效果不是太好,声音有点断断续续,不知道是不是我的解码器的问题(Vista系统)。

URL: http://www.sf.org.cn/Android/lumen/20987.html

 

消息机制,异步和多线程

有了framework后,我们不用面对赤裸裸的OS API,做一些重复而繁杂的事情。但天下没有免费的午餐,我们还是需要学会高效正确的使用不同的framework,很多处理某一特定问题的手法在不同的framework中,用起来都会有所不同的。

Android中,下层是Linux的核,但上层的java做的framework把这一切封装的密不透风。以消息处理为例,在MFC中,我们可以用PreTranslateMessage等东东自由处理消息,在C#中,Anders Hejlsberg老大说了,他为我们通向底层开了一扇救生窗,但很遗憾,在Android中,这扇窗户也被关闭了(至少我现在没发现...)。

Android中,你想处理一些消息(比如:Keydown之类的...),你必须寻找Activity为你提供的一些重载函数(比如 onKeyDown之类的...)或者是各式各样的listener(比如OnKeyDownListner之类的...)。这样做的好处是显而易见的,越多的自由就会有越多的危险和越多的晦涩,条条框框画好了,用起来省心看起来省脑,这是一个设计良好的framework应该提供的享受。对于我目前的工程而言,我没有什么BT的需求在当前API下做不到的,google的设计ms还是很nice的。

但世界是残酷的,有的时候我们还是必须有机制提供消息的分发和处理的,因为有的工作是不能通过直接调用来同步处理的,同时也不能通过Activity中内嵌的消息分发和接口设定来做到,比如说事件的定时触法,异步的循环事件的处理,高耗时的工作等等。在Android中,它提供了一些蛮有意思的方式来做这件事情(不好意思,我见不多识不广,我没见过类似玩法,有见过的提个醒 && 嘴下超生^_^),它有一个android.os.Handler的类,这个类接受一个Looper参数,顾名思义,这是一个封装过的,表征消息循环的类。默认情况下,Handler接受的是当前线程下的消息循环实例,也就是说一个消息循环可以被当前线程中的多个对象来分发,来处理(在UI线程中,系统已经有一个Activity来处理了,你可以再起若干个Handler来处理...)。在实例化一个 handlerInstance之后,你可以通过sendMessage等消息发送机制来发送消息,通过重载handleMessage等函数来分发消息。但是!该handlerInstance能够接受到的消息,只有通过handlerInstance.obtainMessage构造出来的消息(这种说法是不确切的,你也可以手动new一个Message,然后配置成该handlerInstance可以处理的,我没有跟进去分析其识别机制,有兴趣的自己玩吧^_^)。也就是说A, B, C, D都可以来处理同一线程内的消息分发,但各自都只能处理属于自己的那一份消息,这抹杀了B想偷偷进入A领地,越俎代庖做一些非份之事的可能(从理论上看,B还是有可能把消息伪装的和A他们家的一样,我没有尝试挑战一下google的智商,有BT需求的自行研究^_^)。这样做,不但兼顾了灵活性,也确保了安全性,用起来也会简单,我的地盘我做主,不用当心伤及无辜,左拥右抱是一件很开心的事情。。。

很显然,消息发送者不局限于自己线程,否者只能做一些定时,延时之类的事情,岂不十分无趣。在实例化Handler的时候,Looper可以是任意线程的,只要有Handler的指针,任何线程也都可以sendMessage(这种构造方式也很有意思,你可以在A线程里面传B线程的Looper来构造 Handler,也可以在B线程里构造,这给内存管理的方法带来很大的变数...)。但有条规则肯定是不能破坏的,就是非UI线程,是不能触碰UI类的。在不同平台上有很多解决方式(如果你有多的不能再多的兴趣,可以看一下很久很久以前我写的一个,不SB不要钱)。我特意好好跟了一下android中的AsyncQueryHandler类,来了解google官方的解决方案。

AsyncQueryHandlerHandler的子类,文档上说,如果处理ContentProvider相关的内容,不用需要自行定义一套东西,而可以简单的使用async方式。我想指代的就应该是AsyncQueryHandler类。该类是一个典型的模板类,为ContentProvider 的增删改查提供了很好的接口,提供了一个解决架构,final了一些方法,置空了一些方法。通过派生,实例化一些方法(不是每个对 ContentProvider的处理多需要全部做增删改查,我想这也是该类默认置空一些方法而不是抽象一些方法的原因),来达到这个目的。在内部,该类隐藏了多线程处理的细节,当你使用时,你会感觉异常便利。以query为例,你可以这么来用:

// 定义一个handler,采用的是匿名类的方式,只处理query,因此只重写了onQueryComplete函数:

queryHandler = new AsyncQueryHandler(this.getContentResolver()){

// 传入的是一个ContentResolver实例,所以必须在OnCreate后实例化该Handler

@Override

protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
        //
在这里你可以获得一个cursor和你传入的附加的tokencookie
        //
该方法在当前线程下(如果传入的是默认的Looper话),可以自由设定UI信息

}
};

// 调用时只需要调用startQuery(int token, Object cookie, ContentURI uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)函数即可:

queryHandler.startQuery(token, cookie, uri, projection, selection, selectionArgs, sortBy);

可见,该类的使用是多么简单(其实现可不会很容易,因为我尝试做了一次造车轮的工作*_*),比直接用Handler简单无数倍。但让我倍感孤独的是,不知道是没人做异步的ContentProvider访问,还是这个类使用太过于弱智(这个使用方法可是我摸索了半天的啊,难道我真的如此的弱@_@),抑或是大家都各有高招,从SDK到网上,没有任何关于该类的有点用的说明。而我又恰巧悲伤的发现,这个类其实有很多的问题,比如他吃掉异常,有错误时只是简单的返回null指针(这个其实不能怪他,你可以看看这里...);当你传一个nullContentResolver进去的时候,没有任何异常,只是莫名其妙的丢弃所有消息,使你陷入苦苦的等待而不知其因;更愤慨的是,他的token传递竟然有Bug(难道还是我使用不对&_&),从startXX传入的token,到了onXXComplete里面一律变成1,而文档上明明写着两个是一个东西(我的解决方法是用cookietoken,这个不会丢*_*)。不过我暂时还没有遗弃它的打算,虽然没人理睬,虽然有一堆问题,虽然我按图索骥造了个新轮子,但为了节省剩下的一些无聊的工作,我决定苟且偷生了。。。

还是习惯性跑题了,其实,我是想通过我对这个类的无数次Debugger跟进,说说它的多线程异步处理的解决策略的。他的基本策略如下:

1. 当你实例化一个AsyncQueryHandler类时(包括其子类...),它会单件构造一个线程(后面会详述...),这个线程里面会构建一个消息循环。

2. 获得该消息循环的指针,用它做参数实例化另一个Handler类,该类为内部类。至此,就有了两个线程,各自有一个Handler来处理消息。

3. 当调用onXXX的时候,在XXX函数内部会将请求封装成一个内部的参数类,将其作为消息的参数,将此消息发送至另一个线程。

4. 在该线程的Handler中,接受该消息,并分析传入的参数,用初始化时传入的ContentResolver进行XXX操作,并返回Cursor或其他返回值。

5. 构造一个消息,将上述返回值以及其他相关内容绑定在该消息上,发送回主线程。

6. 主线程默认的AsyncQueryHandler类的handleMessage方法(可自定义,但由于都是内部类,基本没有意义...)会分析该消息,并转发给对应的onXXXComplete方法。

7. 用户重写的onXXXComplete方法开始工作。

这就是它偷偷摸摸做过的事情,基本还是很好理解的。我唯一好奇的是它的线程管理方式,我猜测他是用的单件模式。第一个AsyncQueryHandler的实例化会导致创建一个线程,从此该线程成为不死老处男,所有的ContentResolver相关的工作,都由该线程统一完成。个人觉得这种解决方式很赞。本来这个线程的生命周期就很难估量,并且,当你有一个ContentProvider的请求的时候,判断你会做更多的类似操作并不过分。就算错了,花费的也只是一个不死的线程(与进程同生死共存亡...),换来的却是简单的生命周期管理和无数次线程生死开销的节约。同时另外一个很重要的问题,他并会涉及到单件中数据同步的问题,每个类都有各自的Handler类,彼此互不干扰,分发可以分别进行。当多个数据请求的时候,在同一个ContentResolver上进行的可能微乎其微,这就避免了堵塞。总而言之,这套解决办法和Android的整体设计算是天作之合了。
所以建议,如果你有什么非ContentProvider操作,却需要异步多线程执行的话,模拟一套,是个不错的策略,当然,具体情况具体分析,生搬硬套是学不好马列主义的。。。

URL: http://www.sf.org.cn/Android/lumen/21075.html

显示控件使用

Android的界面显示同样也是基于控件的。通常是用View(包括ViewGroup)控件配上XML的样式来做的。具体细节不想说了,可以参考 Samples里的ApiDemos/View,和ViewDoc,以及Implementing a UI这篇Doc。其他还有很多,感觉算是SDK讲述的最多的内容。

从控件的使用上,和网页的设计类似,尽量用parent_width之类的抽象长度,用Theme来做风格,抽取所有的字串等信息做本地化设计。相关内容参看Implementing a UI就好。

一类比较重要的是数据绑定控件。如果做过ASP.Net会从中看到很多类似的地方。一个支持数据绑定的控件,比如ListView。可以通过一个 ListAdapter绑定到一个数据源上。ListAdapter是一个抽象类,主要的实现类包括SimpleAdapter SimpleCursorAdapter。前者是绑定一个静态的Array,后者是绑定一个动态的CursorCursor前面说过,是一个指向数据源的随机迭代器,将View绑定到Cursor通常要设置这样几个参数。一个是每一行的样式,称作Row Layout,其实就是一个普通的LayoutXML文件。还有就是一个列和现实控件的对应关系。那个控件显示哪个列的值,这是需要配置的。为了定制一个良好的数据显示控件,最简单你可以定制很PPRow Layout,复杂一点就是可以重载绑定控件View,或者是适配器ListAdapter。如果是一个数据显示密集的应用,且你对UI有些追求,这个工作估计是必不可少的。

一个主要用于显示数据内容的Activity,可以选择派生自ListActivity。它提供了一个具有ListView Layout,还有simple_list_item_1, simple_list_item_2, two_line_list_item等默认的Row Layout,还有一些比较不错的API,和可供响应选择Item的事件。可以满足你比较基础的需求。如果你觉得只有一个ListView的界面太突兀,你可以为这个ListActivity指定一个Layout,需要注意的是,你需要提供一个id@android:id/listListView控件,避免Activity在内部偷偷寻找该控件的时候失败。

除了这些要求,做好UI还有注意易用性和效率。快捷键是一个比较不错的选择,在 Activity中调用setDefaultkeyMode(SHORTCUT_DEFAULT_KEYS),可以开启快捷键模式,然后你可以将菜单绑定到指定快捷键上就OK了。个人觉得Tip也是一个比较重要的东西,但目前观察看来,这个东西只能够自己提供了。界面的动态性有时候是不可避免的,比如说菜单就是一个需要经常根据光标位置提供不同的选项。这个东西Android很人道的考虑到了,你可以参看NodeList这个Sample。它采取的应该是一个静态模拟动态的方式,这样有助于提高速度。你也可以利用ViewInflate,动态从一个XML创建一个控件。成本据Doc说很大,不到万不得已不要使用。

URL: http://www.sf.org.cn/Android/lumen/21073.html

Intent消息传递

在前面写AndroidContentProvider时候,可以看到那是基于观察者模式的一个消息传递方法。每一个CursorContentResolver做为一个小的注册中心,相关观察者可以在这个中心注册,更新消息由注册中心分发给各个观察者。而在MFCWinform中,都会形成一个消息网,让消息在网中流动,被各节点使用、吃掉或者在出口死掉。

相比之下,我个人觉得基于IntentAndroid核心消息传递机制是有所不同的。它应该会有一个全局性的注册中心,这个注册中心是隐性的,整个Android系统中就那么一个。所有的消息接收者,都被隐形的注册到这个中心。包括ActivityServiceIntentReceiver。其实说隐形注册是不确切的,所有注册都还是我们手动告诉注册中心的,只是与传统的方式不一样,我们通常不是通过代码,而是通过配置文件来做。在应用的Manifest中,我们会为一些ActivityService添加上Intent-filter,或在配置文件中添加<receiver></receiver>项。这其实就相当于向系统的注册中心,注册了相关的Intent-filterreceiver(这个事情完全可以通过代码来做,只是这样就失去了修改的灵活性)。

当程序有一个消息希望发出去的时候,它需要将消息封装成一个Intent,并发送。这时候,应该是有一个统一的中心(恩,有可能Android底层实现的时候不是,但简单这样看是没问题的...)接受到这个消息,并对它进行解析、判定消息类型(这个步骤降低了耦合...),然后检查注册了相匹配的filterreceiver,并创建或唤醒接收者,将消息分发给它。这样做有很多好处。虽然这种传递有的时候不如点对点的传递快(这有些需要速度的地方,我们看到Android会通过直接通信来做),但有时候又因为它只经过一跳(姑且这么叫吧...),比复杂的流动又要更快。更重要的是,它耦合性低,在手机平台这种程序组件多变的条件下使用十分适合。并且它可以很容易实现消息的精确或模糊匹配,弹性很大。(我个人曾想在开发一个C++二次平台的时候引入这样的机制,但在C++中,建立一套完整的数据marshal机制不容易,相比之下,用java来做会简单很多...
恩,废话说了很多,具体讲讲AndroidIntent的使用。当你有一个消息需要传递,如果你明确知道你需要哪个Activity或者其他Class来响应的话,你可以指定这个类来接受该消息,这被称为显性发送。你需要将Intentclass属性设置成目标。这种情况很常见,比如startActivity的时候,会清楚当前Activity完了应该是哪个Activity,那就明确的发送这个消息。

但是,有的时候你并不确定你的消息是需要具体哪个类来执行,而只是知道接收者该符合哪些条件。比如你只需要有一个接收者能显示用户所选的数据,而不想制定某个具体的方法,这时候你就需要用到隐形发送(传统上,我们可能会考虑用多态,但显然这种方式更为灵活...)。在Android中,你可以为Intent指定一个action,表示你这个指令需要处理的事情。系统为我们定义了很多Action类型,这些类型使系统与我们通信的语言(比如在Activity里面加一个Mainfilter,该activity就会做成该应用的入口点),当然你也可以用于你自己的应用之间的通信(同样当然,也可以自定义...)。强烈建议,在自己程序接收或发出一个系统action的时候,要名副其实。比如你响应一个view动作,做的确实edit的勾当,你发送一个pick消息,其实你想让别人做edit的事,这样都会造成混乱。当然只有Action有时候是不够的,在Android中我们还可以指定catalog信息和type/data信息,比如所有的显示数据的Activity,可能都会响应View action。但很多与我们需要显示的数据类型不一样,可以加一个type信息,明确的指出我们需要显示的数据类型,甚至还可以加上一个catalog信息,指明只有你只有按的是中键并发出这样的消息才响应。

从上面可以看出,AndroidIntent可以添加上class, action, data/type, catalog等消息,注册中心会根据这些信息帮你找到符合的接收者。其中class是点对点的指示,一旦指明,其他信息都被忽略。Intent中还可以添加key/value的数据,发送方和接收方需要保持统一的key信息和value类型信息,这种数据的marshaljava里做,是不费什么力气的。

AndroidIntent发送,可以分成单播和广播两种。广播的接收者是所有注册了的符合条件的IntentReceiver。在单播的情况下,即使有很多符合条件的接收者,也只要有一个出来处理这个消息就好(恩,个人看法,没找到确切条款或抉择的算法,本来想实验一下,没来得及...),这样的情况很容易理解,当你需要修改某个数据的时候,你肯定不会希望有十个编辑器轮流让你来处理。当广播不是这样,一个receiver没有办法阻止其他receiver进行对广播事件的处理。这种情况也很容易理解,比如时钟改变了,闹钟、备忘录等很多程序都需要分别进行处理。在自己的程序的使用中,应该分清楚区别,合理的使用。

URL: http://www.sf.org.cn/Android/lumen/21072.html

ContentProvider数据模型概述

Android的数据(包括files, database...)都是属于应用程序自身,其他程序无法直接进行操作。因此,为了使其他程序能够操作数据,在Android中,可以通过做成 ContentProvider提供数据操作的接口。其实对本应用而言,也可以将底层数据封装成ContentProvider,这样可以有效的屏蔽底层操作的细节,并且是程序保持良好的扩展性和开放性。

ContentProvider,顾名思义,就是数据内容的供应者。在Android中它是一个数据源,屏蔽了具体底层数据源的细节,在ContentProvider内部你可以用Android支持的任何手段进行数据的存储和操作,可能比较常用的方式是基于AndroidSQLite数据库(恩,文档中和示例代码都是以此为例)。无论如何,ContentProvider是一个重要的数据源,可以预见无论是使用和定制ContentProvider都会很多。于是花了点时间仔细看了看。

数据库操作

从我目前掌握的知识来看,SQLite比较轻量(没有存储过程之类的繁杂手段),用起来也比较简单。实例化一个SQLiteDatabase类对象,通过它的APIs可以搞定大部分的操作。从sample中看,Android中对db的使用有一种比较简单的模式,即派生一个 ContentProviderDatabaseHelper类来进行SQLiteDatabase对象实例的获取工作。基本上, ContentProviderDatabaseHelper类扮演了一个singleton的角色,提供单一的实例化入口点,并屏蔽了数据库创建、打开升级等细节。在ContentProvider中只需要调用ContentProviderDatabaseHelperopenDatabase方法获取SQLiteDatabase的实例就好,而不需要进行数据库状态的判断。

URI

像进行数据库操作需要用SQL一样,对ContentProivder进行增删改查等操作都是通过一种特定模式的URI来进行的(igcontent: //provider/item/id),URI的能力与URL类似,具体细节可以查看SDK。建立自己的ContentProvider,只需要派生 ContentProivder类并实现insert, delete, update等抽象函数即可。在这些接口中比较特殊的是getType(uri)。根据传入的uri,该方法按照MIME格式返回一个字符串(==!没听过的诡异格式...)唯一标识该uri的类型。所谓uri的类型,就是描述这个uri所进行的操作的种类,比如content://xx/a content://xx/a/1不是一个类型(前者是多值操作,后者是单值),但content://xx/a/1content://xx/a/2 就会是一个类型(只是id号不同而已)。

ContentProvider通常都会实例化一个ContentURIPraser来辅助解析和操作传入的URI。你需要事先(在static域内)为该ContentURIPraser建立一个uri的语法树,之后就可以简单调用 ContentURIPraser类的相关方法进行uri类型判断(match方法),获取加载在uri中的参数等操作。但我看来,这只是在使用上简化了相关操作(不然就需要自己做人肉解析了...),但并没有改变类型判定的模式。你依然需要用switch...case...uri的类型进行判断,并进行相关后续的操作。从模式来看,这样无疑是具有强烈的坏味道,类似的switch...case...代码要出现N此,每次一个 ContentProvideruri类型的增减都会需要遍历修改每一个switch...case...,当然,如果你使用模式(策略模式...)进行改造对手机程序来说无疑是崩溃似的(类型膨胀,效率降低...),所以,只能是忍一忍了(恩,还好不会扩散到别的类中,维护性上不会有杀人性的麻烦...)。

增删改查

ContentProvider 和所有数据源一样,向外提供增删改查操作接口,这些都是基于uri的指令。进行insert操作的时候,你需要传入一个uri ContentValuesuri的作用基本就限于指明增减条目的类型(从数据库层面来看就是table名),ContentValues是一个 key/value表的封装,提供方便的API进行插入数据类型和数据值的设置和获取。在数据库层面上来看,这应该是column namevalue的对应。但为了屏蔽ContentProvider用户涉及到具体数据库的细节,在Android的示例中,用了一个小小的模式。它为每一个表建一个基于BaseColumn类的派生类(其实完全可以不派生自BaseColumn,特别当你的表不基于默认的自动id做主键的时候),这个类通常包括一个描述该表的ContentURI对象和形如 public static final TITLE = "title"这样的column到类数据的对应。从改变上角度来看,你可以修改column的名字而不需要更改用户上层代码,增加了灵活性。 insert方法如果成功会返回一个uri,该uri会在原有的uri基础上增加有一个row id。对于为什么使用row id而不是key id我想破了脑袋。到最后,我发现我傻了,因为ContentProvider不一定需要使用数据库,使用数据库对应的表也可以没有主键,只有row id,才能在任何底层介质下做索引标识。

但,基于row id在删除和修改操作是会造成一定的混乱。删除和修改操作类似。删除操作需要传入一个uri,一个where字串,一组where的参数(做条件判定...),而修改操作会多一个ContentValues做更新值。着两个操作的uri都支持在末尾添加一个row id。于是混乱就出现了。当在where参数中指明了key id,而在uri中提供了row id,并且row idkey id所指函数不一致的时候,你听谁的?示例代码中的做法是完全无视row id(无语...),如此野蛮的方式我估计也只能在示例中出现,在实际中该如何用,恩,我也不知道。幸运的是,我看了下上层对 ContentProvider的删除操作,其实都不会直接进行,而是通过调用Cursordelete方法进行,在这前提下,我想Cursor会处理好这些东西吧。

最后一个操作是查询操作,可以想见,查询的参数是最多的,包括uri和一组条件参数。条件参数类型和标准的sql类似,包括 sort, projection 之类的。从这些参数到sql语句的生成,可以寻求QueryBuilder类的帮助,它提供了一组操作接口,简化了参数到sql的生成工作,哪怕你不懂 sql都完全没有问题(这话说的我自己都觉得有点悬...)。查询返回一个CursorCursor是一个支持随机读写的指针,不仅如此,它还提供了方便的删除和修改的API,是上层对ContentProvider进行操作一个重要对象,需要仔细掌握(Cursor还可以绑定到view上,直接送显,并与用户进行交互,真是程序越往上,封装越好,工作越机械没有复杂性了...)。

数据模型

在与界面打交道的CursorContentResolver等数据操作层中,大量采用观察者模式建立数据层与显示层的联系。一个显示层的视图,可以做成某一种观察者注册到CursorContentResolver等数据中间层中,在实现底层ContentProvider中,我们需要特别注意在对数据进行修改操作(包括增删改...)后,调用相应类型的notify函数,帮助表层对象进行刷新(还有一种刷新方式是从一个view发起的)。可以看到 Android的整体数据显示框架有点像MVC的方式(贫瘠了...叫不出名)。CursorContentResolver相当于控制层,数据层和显示层的交互通过控制层来掌管,而且控制层很稳定不需要特别定制,通常工作只在定制数据层和显示层空间,还是比较方便和清晰的。

一个设计问题

现在有个设计问题,比如我要扩充一个已有的ContentProvider(第三方提供),我是建立一个ContentProvider,只保留第三方 ContentProviderkey信息,并为其添加更多的信息,在表层维护这两个ContentProvider的联系好;还是建议一个 ContentProvider,以第三方的ContentProvider做一部分底层数据源,像表层提供一个ContentProvider好。

前者无疑在实现上简单一些,如果第三方改变,灵活性也更好,只是需要仔细维护表层的相关代码。后者实现上需要付出大量的苦力劳动,当表层使用会简单多了。我举棋不定,期待你的意见。。。

URL: http://www.sf.org.cn/Android/lumen/21071.html

自定义ContentProvider的语义

ContentProvider中,最重要的就是query操作。query根据输入返回一个符合条件的Cursor。这就可能出现以下几种情况:1. 查询成功,包含几个正确的结果;2. 查询失败,没有符合的结果;3. 输入错误, 触发了某个异常;4. 没能查询到结果,但无法确定是输入错误还是查询失败。第一种情况是我们最需要的,当然是需要正确维系的,而最后一种情况在大部分应用中应该不会出现(但在我的应用中会的*_#),而第二种第三种是比较常见的。

经过我的测试,系统的ContentProvider维持这样的语义:如果是情况2,返回正常的Cursor,并且,其count0,相当于empty cursor;如果是情况3,不抛出任何异常,返回nullCursor。这样的话明明白白写出来是很好理解的,但由于没有官方的文档说明,在自定义的时候经常会误用。比如在某些情况下,用null表征查询失败,用抛出异常来描述错误的输入。

返回empty cursor,如果是通过databasecursor自然会有db帮你维护,但是如果返回ArrayListCursorMergeCursor或其他自定义的Cursor,就需要自己维系了。ArrayListCursor可以通过new ArrayListCursor(Columns, new ArrayList(){})来提供。其中Columns一定不为nullMergeCursor不能以new MergeCursor(new Cursor[]{})来创建,而需要通过new MergeCursor(new Cursor[]{aEmptyCursor, ...}来维系(其实很好理解,我呆了...)。自定义的Cursor也一定要提供生成empty cursor的方式。

如果将ContentProvider作为一个单独的module来理解,不通过异常而是通过null来返回MS是有好处的。在module的出口吃掉所有异常,虽然不能提供足够的信息(异常信息全部写入日志),但可能会使上层使用更简单。但在Android中,我并没有感觉到这一点。作为ContentProvider的上层函数,ListActivity.managedQuery ListView.setListAdapter等,根本不能处理一个nullCursor,在ListView中这会触发一个异常。更无语的是,当你把一个null Cursor设置为manage的后。它不会立即抛异常,而是在OnFreeze等生命周期函数的时候,因无法处理null Cursor而抛出一个异常。这使得你根本无法在当地catch该异常,换句话,ListActivitymanageCursor根本是个无法使用的函数。你必须用getContext().query()获得Cursor,然后判定该Cursor是否null,在进行startManagingCursor进行绑定。这远不如直接用异常进行错误路径的处理来的统一和方便。
当然,有些东西我们是不能改变的,只能去适应。对于自定义的cursor, ContentProvider,最重要的,是在无人造错误输入的情况下返回empty cursor,而不是null。至于使用null响应还是异常响应上,我个人觉得还是和系统同步为好,虽然别扭,但至少统一不容易有歧义。

此外,ContentProvider还有很多细致的语义。比如返回的Cursor需要绑定一个URI,以便自动响应更新。自定义的更新需要支持deleteRow等操作语义等等。

PS:而上层的ListView,更是陷阱重重。首先绑定到ListViewCursor必须有_id项,否则会有异常抛出。如果做过.net的开发,这一点是可以想到的,但是,这种问题应该在文档中写明。另外,在ListView中,如果你不绑定一个数据源,你一定不能在layout中添加涉及内容的属性。比如android:height="wrap_content",这会在onMeasure的时候抛出异常。

 

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值