用户工具

站点工具


android_notes

目录

Android 笔记

开发环境

Installing the Android Development Environment

下载安装最近新的 JDK:http://www.oracle.com/technetwork/java/javase/downloads/index.html

下载安装最新的 Eclipse Classic 版:http://www.eclipse.org/downloads/

在 Eclipse 里安装 ADT:菜单 Help → Install New Software…,使用 https://dl-ssl.google.com/Android/eclipse/

命令行

进入虚拟机的 shell

adb shell

上传和下载文件

把本地的文件复制到虚拟机的 SD 卡上

adb push foo.txt /sdcard

把虚拟机根目录下的 init.rc 复制到本地,本地路径可省略

adb pull /init.rc

安装和删除程序

adb install test.apk
adb install -r test.apk
adb uninstall com.foo.bar.test-1

注:test.apk 安装后位于虚拟机的 /data/app/com.foo.bar.test-1.apk
应用程序的私有数据文件在 /data/data/[package_name]/files 目录下,用 Context.getFilesDir() 可以取到这个路径

运行程序

adb shell am start -n package_name/package_name.main_activity_name

把 logcat 里的 log 保存到磁盘文件里

adb logcat -v time > log.txt

上述命令会在保存的每一行 log 前加上时间,比如:

01-01 08:01:16.065 W/EntropyService( 2683): unable to load initial entropy (first boot?)

sendevent

获取设备列表

adb shell cat /proc/bus/input/devices

在 Samsung Galaxy S II 上的结果如下:

I: Bus=0019 Vendor=0001 Product=0001 Version=0001
N: Name="max8997-muic"
P: Phys=deskdock-key/input0
S: Sysfs=/devices/platform/s3c2410-i2c.5/i2c-5/5-0066/max8997-muic/input/input0
U: Uniq=
H: Handlers=kbd event0 
B: EV=100003
B: KEY=c0000 0 0 0
I: Bus=0019 Vendor=0001 Product=0001 Version=0100
N: Name="sec_key"
P: Phys=gpio-keys/input0
S: Sysfs=/devices/platform/sec_key.0/input/input1
U: Uniq=
H: Handlers=kbd event1 
B: EV=3
B: KEY=100000 0 0 1c0000 0 0 0
I: Bus=0000 Vendor=0000 Product=0000 Version=0000
N: Name="sec_touchscreen"  # 触摸屏
P: Phys=
S: Sysfs=/devices/virtual/input/input2
U: Uniq=
H: Handlers=event2 
B: EV=9
B: ABS=2650000 0
I: Bus=0000 Vendor=0000 Product=0000 Version=0000
N: Name="gyro_sensor"  # 陀螺仪
P: Phys=
S: Sysfs=/devices/virtual/input/input3
U: Uniq=
H: Handlers=event3 
B: EV=5
B: REL=38
I: Bus=0000 Vendor=0000 Product=0000 Version=0000
N: Name="proximity_sensor"  # 接近传感器
P: Phys=
S: Sysfs=/devices/virtual/input/input4
U: Uniq=
H: Handlers=event4 
B: EV=9
B: ABS=2000000
I: Bus=0000 Vendor=0000 Product=0000 Version=0000
N: Name="light_sensor"  # 光线传感器
P: Phys=
S: Sysfs=/devices/virtual/input/input5
U: Uniq=
H: Handlers=event5 
B: EV=9
B: ABS=100 0
I: Bus=0019 Vendor=0000 Product=0000 Version=0000
N: Name="sec_touchkey"  # 最下面的触摸式键盘(menu, home, back, search)
P: Phys=melfas-touchkey/input0
S: Sysfs=/devices/virtual/input/input6
U: Uniq=
H: Handlers=kbd event6 
B: EV=3
B: KEY=40000800 800 0 0 10000000
I: Bus=0018 Vendor=0000 Product=0000 Version=0000
N: Name="compass_sensor"  # 指南针/数字罗盘
P: Phys=
S: Sysfs=/devices/virtual/input/input7
U: Uniq=
H: Handlers=event7 
B: EV=5
B: REL=3ff

命令 adb shell ls /dev/input 返回:

event0
...
event7
mice

监测

可用 adb shell getevent

模拟

adb shell sendevent /dev/input/eventX 1 Y 1
adb shell sendevent /dev/input/eventX 0 0 0
adb shell sendevent /dev/input/eventX 1 Y 0
adb shell sendevent /dev/input/eventX 0 0 0
  • X = 1
    • Y = 114 - 降低音量
    • Y = 115 - 升高音量
    • Y = 116 - 锁屏
    • 213 - 相机
  • X = 6
    • Y = 229 - menu
    • Y = 102 - home
    • Y = 158 - back
    • Y = 231 - call
    • Y = 107 - end call

Layout

用代码创建 RelativeLayout

RelativeLayout rl = new RelativeLayout( context );
RelativeLayout.LayoutParams rl_lp = new RelativeLayout.LayoutParams(
	ViewGroup.LayoutParams.WRAP_CONTENT,
	ViewGroup.LayoutParams.WRAP_CONTENT );  
rl_lp.addRule( RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE );  
ProgressBar pb = new ProgressBar( context, null, android.R.attr.progressBarStyleLarge );
rl.addView( pb, rl_lp );

Shared Preferences

Shared preferences 数据在 /data/data/[package_name]/shared_prefs 目录下,默认的 shared preferences 文件名是 [package_name]_preferences.xml,内容示例如下:

<?xml version='1.0' encoding='utf-8' standalone='yes'?>
<map>
	<string name="key">value</string>
	<boolean name="key" value="true"/>
</map>

获取和操作默认的 shared preferences

读取数据:

SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences( this );
boolean value = sp.getBoolean( "key", false );

写入数据:

SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences( this );
SharedPreferences.Editor editor = sp.edit();
editor.putString( "key", "value" );
editor.commit();

获取和操作自定义的 shared preferences

不需要加“.xml”扩展名,Android 会自动添加一个 .xml 的扩展名。

SharedPreferences sp = getSharedPreferences( "filename", MODE_PRIVATE );

SQLite

应用程序的 SQLite 数据库文件位于 /data/data/[package_name]/databases 目录下。

Android 使用 SQLite 数据库的模式是,每个数据库(不是数据表)都提供一个对应的 SQLiteOpenHelper 派生类,用来管理数据库中表的创建和升级操作(onOpen() 为可选重载):

class MyDBHelper extends SQLiteOpenHelper
{
	MyDBHelper( Context context, String name, CursorFactory cursorFactory, int version )
	{
		super( context, name, cursorFactory, version );
	}
 
	@Override
	public void onCreate( SQLiteDatabase db )
	{
		db.execSQL( "CREATE TABLE foo(bar VARCHAR(256) PRIMARY KEY)" );
	}
 
	@Override
	public void onUpgrade( SQLiteDatabase db, int oldVersion, int newVersion )
	{
		;
	}
}

最终为了方便一般都要对 SQLiteOpenHelper 的派生类做包装使用。真正的数据库操作,都是围绕着从 SQLiteOpenHelper 得到的 SQLiteDatabase 实例接口展开的:

class MyDB
{
	private final static int DB_VERSION = 1;
 
	private MyDBHelper     m_dbHelper = null;
	private SQLiteDatabase m_db       = null;
 
	MyDB( Context context, String dbName )
	{
		m_dbHelper = new MyDBHelper( context, dbName, null, DB_VERSION );
		m_db       = m_dbHelper.getWritableDatabase();
	}
 
	void close()
	{
		if( m_db != null )
			m_db.close();
	}
}

SQLiteDatabase 的使用

Desktop Management Tools

SQLite Manager for Firefox

语法

分页

取第 11 到第 20 条(跳过前 10 条):

SELECT * FROM foo LIMIT 9 OFFSET 10
SELECT * FROM foo LIMIT 10,9

删除所有数据

DELETE FROM foo

Internal Storage

External Storage

AndroidMenifest.xml 里的权限声明:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
  • Environment.getExternalStorageDirectory().toString() == /mnt/sdcard
  • Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_DOWNLOADS ).toString() == /mnt/sdcard/Download
  • getExternalFilesDir( null ).toString() == /mnt/sdcard/Android/data/[package_name]/files
  • getExternalFilesDir( Environment.DIRECTORY_DOWNLOADS ).toString() == /mnt/sdcard/Android/data/[package_name]/files/Download
  • getExternalCacheDir().toString() == /mnt/sdcard/Android/data/[package_name]/cache

Date / Time

时间字符串 => 时间戳

// Mon, 19 Mar 2012 17:58:03 GMT
SimpleDateFormat sdf = new SimpleDateFormat( "EEE, dd MMM yyyy HH:mm:ss z", Locale.US );
Date date = null;
try
{
	date = sdf.parse( pubDate );
}
catch( ParseException e )
{
	Log.w( "Failed to parse date string", e.getMessage() );
	date = new Date();
}
long timestamp = date.getTime();  // ms

创建 SimpleDateFormat 时如果不带 locale,默认使用当前系统设置,这样可能导致解析失败。

时间戳 => 时间字符串

SimpleDateFormat sdf = new SimpleDateFormat( "MMM dd" );
String str = sdf.format( new Date( timestamp ) );

基本原则

The Android UI toolkit is not thread-safe and must always be manipulated on the UI thread.

Android offers several ways to access the UI thread from other threads. You may already be familiar with some of them but here is a comprehensive list:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable, long)
  • Handler

多线程及耗时工作最佳实践

AsyncTask

使用是必须进行扩展,基类的三个参数是 Params, Progress 和 Result(不需要使用的话可以用 Void)。执行过程大致有 4 个步骤:

class MyTask extends AsyncTask<Params, Progress, Result>
{
	@Override
	protected Result doInBackground( Params... params )
	{
		return( null );
	}
}
  • protected void onPreExecute(),可选;在 UI 线程被调用
  • protected Result doInBackground( Params… params ),必须实现;在工作线程被调用,这一步可以调用 publishProgress( Progress… ) 来发布进度信息;
  • protected void onProgressUpdate( Progress… progress ),可选;在 UI 线程被 publishProgress() 触发;
  • protected void onPostExecute( Result result ),可选但一般都会实现,用来把结果通知到 UI;在 UI 线程被调用

执行时只要: (new MyTask()).execute( param1, param2, param3 );

注意事项:

  • 子类实例必须在 UI 线程创建;execute( Params… ) 也必须在 UI 线程调用
  • AsyncTask 会同步各个步骤,所以 doInBackground() 可以安全的设置类成员,onPostExecute() 随后可以安全的访问类成员

Intent

Send E-mail

Intent email = new Intent( android.content.Intent.ACTION_SEND );
email.setType( "plain/text" );
email.putExtra( android.content.Intent.EXTRA_EMAIL,   new String[] { "test@gmail.com" } );
email.putExtra( android.content.Intent.EXTRA_SUBJECT, "Subject" );
email.putExtra( android.content.Intent.EXTRA_TEXT,    "Content" );
email.putExtra( Intent.EXTRA_STREAM, Uri.fromFile( new File( getFilesDir(), "test.txt" ) ) );  // Method 1
//email.putExtra( Intent.EXTRA_STREAM, Uri.parse( "file://" + getFilesDir().toString() + "/test.txt" ) );  // Method 2
startActivity( Intent.createChooser( email, "Send email by" ) );

注:

  1. setType() 设置的是附件的类型,不是邮件正文的类型
  2. 如果使用自带的邮件客户端,上述代码就可以发出附件;但是如果要用 GMail 的话,附件必须放在 external storage 里面

打开 market

Intent intent = new Intent( Intent.ACTION_VIEW );
intent.setData( Uri.parse( "market://details?id=com.android.example" ) );
startActivity( intent );

打开 GPS 设置

import android.provider.Settings;
 
Intent intent = new Intent();  
intent.setAction( Settings.ACTION_LOCATION_SOURCE_SETTINGS );
startActivity( intent );

HTTP

HttpURLConnection connection = null;
InputStream is = null;
try
{
	URL url = new URL( "http://www.google.com" );  // MalformedURLException
	connection = (HttpURLConnection)url.openConnection();  // IOException
	is = new BufferedInputStream( connection.getInputStream() );  // IOException
}
catch( MalformedURLException e )
{
	;
}
catch( IOException e )
{
	;
}
finally
{
	connection.disconnect();
}

Upload file with POST request

public boolean uploadFileByHttp( String target, String filename, HashMap<String,String> data )
{
	int    responseCode    = 0;
	String responseMessage = null;
 
	HttpURLConnection connection = null;
	DataOutputStream outputStream = null;
	//DataInputStream inputStream = null;
 
	String line_end   = "\r\n";
	String two_hyphen = "--";
	String boundary   =  "*1*2*3*4*5*6*7*8*9*0*";
 
	int maxBufferSize = 1 * 1024 * 1024;
 
	try
	{
		FileInputStream file = new FileInputStream( new File( filename ) );
 
		URL url = new URL( target );
		connection = (HttpURLConnection)url.openConnection();
 
		// Allow Inputs & Outputs
		connection.setDoInput( true );
		connection.setDoOutput( true );
		connection.setUseCaches( false );
 
		// Enable POST method
		connection.setRequestMethod( "POST" );
 
		connection.setRequestProperty( "Connection",   "Keep-Alive" );
		connection.setRequestProperty( "Content-Type", "multipart/form-data;boundary=" + boundary );
 
		outputStream = new DataOutputStream( connection.getOutputStream() );
		outputStream.writeBytes( two_hyphen + boundary + line_end );
		outputStream.writeBytes( "Content-Disposition: form-data; name=\"uploadedfile\";filename=\"" + filename +"\"" + line_end );
		outputStream.writeBytes( line_end );
 
		int bytesAvailable = file.available();
		int bufferSize = Math.min( bytesAvailable, maxBufferSize );
		byte[] buffer = new byte[bufferSize];
 
		// Read file
		int bytesRead = file.read( buffer, 0, bufferSize );
		while( bytesRead > 0 )
		{
			outputStream.write( buffer, 0, bufferSize );
			bytesAvailable = file.available();
			bufferSize = Math.min( bytesAvailable, maxBufferSize );
			bytesRead = file.read( buffer, 0, bufferSize );
		}
 
		outputStream.writeBytes( line_end );
		outputStream.writeBytes( two_hyphen + boundary + two_hyphen + line_end );
 
		// Responses from the server (code and message)
		responseCode    = connection.getResponseCode();
		responseMessage = connection.getResponseMessage();
 
		file.close();
		outputStream.flush();
		outputStream.close();
	}
	catch( Exception e )
	{
		Log.e( "", e.getMessage() );
		return( false );
	}
 
	return( true );
}

XML 解析

解析来自 InputStream 的 XML:

try
{
	XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
	XmlPullParser xpp = factory.newPullParser();
	xpp.setInput( inputStream, "utf-8" );
 
	int eventType = parser.getEventType();
	while( eventType != XmlPullParser.END_DOCUMENT )
	{
		switch( eventType )
		{
			case XmlPullParser.START_TAG:
				String tagName = xpp.getName();
				String value = xpp.getAttributeValue( null, "attrName" );
				break;
			case XmlPullParser.TEXT:
				String text = xpp.getText();
				break;
			case XmlPullParser.END_TAG:
				String tagName = xpp.getName();
				break;
		}
 
		eventType = parser.next();
	}
}
catch( XmlPullParserException e )
{
	;
}
catch( IOException e )
{
	;
}

如果要解析的是资源里打包的 XML,那么只有创建 parser 部分不太一样,其它用法不变:

XmlResourceParser xrp = getResources().getXml( R.xml.foo );
...
xrp.close();

Regular Expression

Pattern p = Pattern.compile( "<span>([^<]+)</span>" );
Matcher m = p.matcher( str );
if( m.find() )
	String foo = m.group( 1 );

Service

Service 跟他的宿主 application 运行在同一个进程,并且跑在该进程的主线程里。所以如果 service 要执行一些耗时操作的话,需要开自己的工作线程。

各种 data holder 互转

Java 的 String 都是 Unicode 编码。

InputStream to byte[]

ByteArrayOutputStream baos = new ByteArrayOutputStream();
try
{
	int    nRead  = 0;
	byte[] buffer = new byte[1024];
 
	while( ( nRead = is.read( buffer, 0, buffer.length ) ) != -1 )
		baos.write( buffer, 0, nRead );
 
	baos.flush();
}
catch( IOException e )
{
	;
}
 
byte[] data = baos.toByteArray();

InputStream to String

StringBuilder sb = new StringBuilder();
try
{
	Reader reader = new InputStreamReader( is, "gb2312" );  // 此 InputStream 的编码是 gb2312
	int read = 0;
	char[] buffer = new char[1024];
	do
	{
		read = reader.read( buffer, 0, buffer.length );
		if( read > 0 )
			sb.append( buffer, 0, read );
	}
	while( read >= 0 );
}
catch( UnsupportedEncodingException e )
{
	;
}
catch( IOException e )
{
	;
}
String str = sb.toString();

byte[] to String

String str = EncodingUtils.getString( data, "gb2312");  // 这里的 gb2312 指的是 byte[] 的编码

Or:

String str = null;
try
{
	str = new String( data, "gb2312" );  // 这里 gb2312 指的是 byte[] 的编码,不是产生的 String 的编码
}
catch( UnsupportedEncodingException e )
{
	;
}

String to byte[]

byte[] data = null;
try
{
	data = str.getBytes( "gb2312" );  // byte[] 的最终编码
}
catch( UnsupportedEncodingException e )
{
	;
}

Dialog

在 Activity 里使用 dialog 需要重载两个 method:

@Override
protected Dialog onCreateDialog( int id )
{
	AlertDialog.Builder builder = new AlertDialog.Builder( this );
	//... ...
	AlertDialog dialog = builder.create();
	return( dialog );
}
 
@Override
protected void onPrepareDialog( int id, Dialog dialog )
{
	;
}

在需要显示对话框的地方调用 showDialog( id ),Android 会智能的选择调用 onCreateDialog() 或者直接重用之前已经创建好的 dialog。在 dialog 显示出来之前,Android 会调用 onPrepareDialog() 让你有机会对即将显示出来的 dialog 做初始设定。

关闭(其实只是隐藏)dialog 用 dismissDialog( id )。

Custom Dialog

AlertDialog.Builder builder = new AlertDialog.Builder( this );
builder.setTitle( R.string.title );
builder.setNegativeButton( R.string.ok, this );
 
LayoutInflater inflater = (LayoutInflater)getSystemService( LAYOUT_INFLATER_SERVICE );
View dialogView = inflater.inflate( R.layout.custom_dialog, null );
builder.setView( dialogView );
 
AlertDialog dialog = builder.create();

Touch

boolean ViewGroup::onInterceptTouchEvent( MotionEvent ev );
 
boolean View::onTouchEvent( MotionEvent event );
 
void View::setOnTouchListener( View.OnTouchListener l );
 
View.OnTouchListener
{
	public abstract boolean onTouch( View v, MotionEvent event );
}

onInterceptTouchEvent 和 onTouchEvent 调用时序
http://blog.csdn.net/ddna/article/details/5473293

Accessibility

Eyes-Free

http://code.google.com/p/eyes-free/

可以绕过 Google Market/Play 直接下载 TalkBack。

Custom Attributes

尺寸相关

Screen size: 实际的物理对角线尺寸,分为 small, normal, large, 和 extra large (xlarge)。

Screen density: 根据 dpi(dots per inch)/ppi(pixels per inch) 分为 ldpi (low), mdpi (medium), hdpi (high), 和 xhdpi (extra high)。

dp (Density-independent Pixel): 密度(dpi/ppi)无关的抽象像素。以 160dpi(mdpi) 为基准,在 160dpi 的设备上,1dp = 1px。在其它 dpi 的设备上,dp 跟 px 的换算公式为 px = dp * ( dpi / 160 )。

sp (Scale-independent Pixel): 用于字体。The sp scale factor depends on a user setting and the system scales the size the same as it does for dp.

pt (Points): 屏幕物理长度单位, 表示一个点,是屏幕的物理尺寸。大小为 1/72 英寸。

对于程序中使用的像素化图片,在 ldpi, mdpi, hdpi 和 xhdpi 的屏幕上要遵循 3:4:6:8 的比例来准备。比如在 mdpi 屏幕上用的图片是 48×48 的,那么 ldpi 上就是 36×36,hdpi 上就是 72×72,xhdpi 上就是 96×96。

建议使用 dp 作为空间大小单位,sp 作为和文字相关大小单位。

120dpi ldpi 3
160dpi baseline mdpi 4
240dpi hdpi 6
320dpi xhdpi 8

用 sw600dp 来区分 7" 设备这个做法是怎么来的?

dp 是密度(dpi/ppi)无关的单位,在任何 dpi 的设备上显示出来以后,物理尺寸应该是基本一致的,所以 600dp 的物理尺寸是确定的,可以计算出来的。

160 dp / 1 inch = 600 dp / x inch, 600dp = 3.75”

就是说 7” 的设备上,屏幕的最窄边是 3.75”,就是 600dp。

对于 4:3 的 7” 屏,最窄边是 4.2”。4.2” - 3.75” = 0.45”,0.45” 在 mdpi 的设备上是 72px。这 72 个像素一方面是因为 system bar 会占掉一些空间,另外也要考虑到屏幕不总是 4:3 的,而且 dpi 也不总是 160,所以会有些误差。

对于 16:9 的 7” 屏,最窄边是 3.42”。3.42” 在 mdpi 的设备上是 547px。所以估计 sw600dp 在 16:9 的 7” 平板上不 work,现在有 16:9 的 7” tablet 吗?

Android 官方文档上举例的 7” tablet 是 600×1024,这种屏的比例是 1.71;4:3 的屏幕比例是 1.33;16:9 的屏幕比例是 1.78。所以官方举的例子是介乎 4:3 跟 16:9 之间的一种比例。硬件制造商会跟 Android 保持默契维护 7” 屏设备最小 dp 是 600 吗?

Soft Keyboard

点击回车时做特殊处理(如提交输入内容)

需要给 EditText 设置 OnEditorActionListener:

myEditText.setOnEditorActionListener( new TextView.OnEditorActionListener()
{
	@Override
	public boolean onEditorAction( TextView v, int actionId, KeyEvent event )
	{
		if( event != null &&
			event.getKeyCode() == KeyEvent.KEYCODE_ENTER &&
			event.getAction()  == KeyEvent.ACTION_DOWN )
		{
			// do any thing you want to do here
			return( true );
		}
 
		return( false );
	}
});

需要注意的是:

  • 只有此事件由回车键触发,event 才不为 null
  • 有时候按一次回车键 onEditorAction() 会被调用两次(一次 down,一次 up),但是有时只有 down,为了不做两次重复的处理,最好加以区分

还可以让针对某个 EditText 控件弹出的软键盘上的回车键看起来像其它样子(比如回车键看起来是一个搜索的放大镜图标):

<EditText
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"
	android:imeOptions="actionSearch" />

这样一来的话,onEditorAction() 的 actionId 值就会变成 EditorInfo.IME_ACTION_SEARCH(不做定制的话默认是 EditorInfo.IME_NULL)。不过有些输入法貌似有 bug(比如搜狗拼音),就算定制过,回车键图标也不变,actionId 的值也还是 EditorInfo.IME_NULL。

Alert Dialog 里的 EditText 会自动获取焦点,但是软键盘不显示

在 dialog 创建之后调用:

dialog.getWindow().setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE );

或者还有一种方法是:

dialog.setOnShowListener( new OnShowListener()
{
	@Override
	public void onShow( DialogInterface di )
	{
		InputMethodManager imm = (InputMethodManager)getSystemService( Context.INPUT_METHOD_SERVICE );
		imm.showSoftInput( myEditText, InputMethodManager.SHOW_IMPLICIT );
	}
});

处理未捕获异常

需要实现 Thread.UncaughtExceptionHandler 接口:

public void uncaughtException( Thread thread, Throwable ex )
{
	StackTraceElement[] ste = ex.getStackTrace();
	for( int i = 0; i < ste.length; ++i )
	{
		Log.e( "crash", ste[i].toString() );
	}
 
	android.os.Process.killProcess( android.os.Process.myPid() );  
	System.exit( 1 );
}

然后要把实现了 UncaughtExceptionHandler 的类实例安装成为默认的未捕获异常处理器:

Thread.setDefaultUncaughtExceptionHandler( foo );

如何给 apk 签名

产生 RSA 密钥对和自签名证书

keytool -genkey -v -keystore test.keystore -alias test -keyalg RSA -keysize 2048 -validity 10000

-validity 的单位是天,是证书的有效期。

过程中会提示输入 keystore 的密码和 key 的密码,完成后会在当前目录下生生 test.keystore。里面的内容?

对未签名的 apk 文件进行签名

jarsigner -verbose -keystore test.keystore unsigned.apk test

过程中会提示输入 keystore 的密码和 key 的密码,完成后 apk 包的文件名不变,但是里面多了一个叫 META-INF 的文件夹,里面有如下文件:

  • MANIFEST.MF - 文本文件,里面有 apk 包里每个文件的 SHA-1 数字摘要的 Base64 编码
  • TEST.SF - 文本文件,在 MANIFEST.MF 的基础上,用私钥对每个文件的数字摘要进行数字签名
  • TEST.RSA - 保存公钥和加密算法等信息

对签过名的 apk 包做对齐整理

zipalign -v 4 unaligned.apk ready_to_go.apk

-v 是冗长输出,4 是 4 bytes 对齐。

设备

Samsung Galaxy S II

  • Android: 2.3.4
  • Screen: 4.27”, 480×800, 218.501dpi

Samsung Galaxy Nexus

  • Android: 4.0.2
  • Screen: 4.65”, 720×1280, 320dpi (316dpi, Device uses PenTile technology. Its pixels consist of only two sub-pixels instead of three and the claimed pixel density is only achieved using subpixel rendering. In any case, the ppi numbers are not directly comparable.)

Motorola Xoom

  • Android: 3.2.1
  • Screen: 10.1”, 1280×800, 149dpi

Kindle Fire

  • Android: 2.3
  • Screen: 7”, 1024×600, 169ppi

NDK

Tips

扩展 Application

扩展了 Application 以后要在 AndroidManifest.xml 的 <Application> 里面加上 name,否则在 code 里使用扩展的 Application 的时候会报 java.lang.ClassCastException: android.app.Application:

<application
	android:icon="@drawable/ic_launcher"
	android:label="@string/app_name"
	android:name="MyApplication">

从 AndroidManifest.xml 里取到版本号

String versionName = null;
try
{
	PackageInfo pi = getPackageManager().getPackageInfo( getApplication().getPackageName(), PackageManager.GET_CONFIGURATIONS );
	versionName = pi.versionName;
}
catch( PackageManager.NameNotFoundException e )
{
	Log.e( "Failed to get package info", e.getMessage() );
}

获得屏幕分辨率

// method 1
DisplayMetrics dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics( dm );
Log.i( "", "width = " + dm.widthPixels );
Log.i( "", "height = " + dm.heightPixels );
 
// method 2
Display display = getWindowManager().getDefaultDisplay();
Log.i( "", "width = " + display.getWidth());
Log.i( "", "height = " + display.getHeight());

去掉 Activity 的 title bar,增加可视空间

<activity
	android:name=".HelloGoogleMaps"
	android:label="@string/app_name"
	android:theme="@android:style/Theme.NoTitleBar">

让有 EditView 或者 AutoCompleteTextView 的 Activity/Fragment 不会自动弹出软键盘

在 EditView/AutoCompleteTextView 的前面插入如下内容抢走它的 focus:

<LinearLayout
	android:focusable="true"
	android:focusableInTouchMode="true"
	android:layout_width="0px"
	android:layout_height="0px"/>

取当前系统时间戳

long System.currentTimeMillis(),返回自 1970 年 1 月 1 日 00:00:00 UTC 起的系统时间,单位毫秒。

隐藏软键盘

InputMethodManager imm = (InputMethodManager)context.getSystemService( Context.INPUT_METHOD_SERVICE );
imm.hideSoftInputFromWindow( view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS );

view 是当前拥有输入焦点的那个控件。

URL 编码

URLEncoder.encode( "http://www.google.com?q=foo%bar", "utf-8" );

获取当前的语言设置

Locale.getDefault().getLanguage() 对于中文,返回 zh。

Locale.getDefault().getCountry() 对于中文,返回 CN。

让 ViewPager 给它下面的 View 滕一点位置

如果使用如下布局,会发现 AdView 得不到纵向空间,无法显示:

<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:ads="http://schemas.android.com/apk/lib/com.google.ads"
	android:id="@+id/root"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	android:orientation="vertical" >
 
	<android.support.v4.view.ViewPager
		android:id="@+id/pager"
		android:layout_width="match_parent"
		android:layout_height="wrap_content" />
 
	<com.google.ads.AdView
		android:id="@+id/adView"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		ads:adUnitId="MY_AD_UNIT_ID"
		ads:adSize="BANNER"
		ads:testDevices="TEST_EMULATOR, TEST_DEVICE_ID"
		ads:loadAdOnCreate="true" />
</LinearLayout>

把 ViewPager 改成如下形式就可以让它给其下面的 View 腾出足够的位置(ADT 建议把高度设置成 0dip 以便提高性能):

<android.support.v4.view.ViewPager
	android:id="@+id/pager"
	android:layout_width="match_parent"
	android:layout_height="0dip"
	android:layout_weight="1" />

让 WebView 跟 AdView 共存于同一个 Activity 里

<RelativeLayout
	android:layout_width="match_parent"
	android:layout_height="match_parent">
 
	<!-- layout_above 确定了 WebView 跟 AdView 之间的相互位置;layout_height="match_parent" 能让 web page 在很短的情况下,WebView 也能充满全屏 -->
	<WebView
		android:layout_width="match_parent"
		android:layout_height="match_parent"
		android:layout_above="@id/adView" />
 
	<!-- layout_alignParentBottom 比较重要,它让 AdView 贴紧屏幕下缘,否则在动态创建的情况下 AdView 会跑到屏幕上缘,WebView 就看不见了 -->
	<AdView
		android:layout_above="@+id/adView" 
		android:layout_width="match_parent"
		android:layout_height="wrap_content"
		android:layout_alignParentBottom="true" />
 
</RelativeLayout>

让 WebView 在屏幕方向发生改变以后不会重新加载

修改 AndroidMenifest.xml,在 WebView 所在的 Activity 里加入下面这个属性:

android:configChanges="orientation"

判断网络是否可用

NetworkInfo: type: mobile[UNKNOWN], state: UNKNOWN/IDLE, reason: (unspecified), extra: (none), roaming: false, failover: false, isAvailable: false
NetworkInfo: type: WIFI[HSDPA], state: DISCONNECTED/SCANNING, reason: (unspecified), extra: (none), roaming: false, failover: false, isAvailable: true
NetworkInfo: type: mobile_mms[UNKNOWN], state: UNKNOWN/IDLE, reason: (unspecified), extra: (none), roaming: false, failover: false, isAvailable: false
NetworkInfo: type: mobile_supl[UNKNOWN], state: UNKNOWN/IDLE, reason: (unspecified), extra: (none), roaming: false, failover: false, isAvailable: false
NetworkInfo: type: mobile_dun[UNKNOWN], state: UNKNOWN/IDLE, reason: (unspecified), extra: (none), roaming: false, failover: false, isAvailable: false
NetworkInfo: type: mobile_hipri[UNKNOWN], state: UNKNOWN/IDLE, reason: (unspecified), extra: (none), roaming: false, failover: false, isAvailable: false
NetworkInfo: type: mobile_bip[UNKNOWN], state: UNKNOWN/IDLE, reason: (unspecified), extra: (none), roaming: false, failover: false, isAvailable: false
static boolean isNetworkAvailable( Context context )
{
	ConnectivityManager cm = (ConnectivityManager)context.getSystemService( Context.CONNECTIVITY_SERVICE );
	NetworkInfo[] nis = cm.getAllNetworkInfo();
	for( int i = 0; i < nis.length; ++i )
	{
		if( nis[i].getType() == ConnectivityManager.TYPE_MOBILE ||
			nis[i].getType() == ConnectivityManager.TYPE_WIFI )
		{
			if( nis[i].isConnectedOrConnecting() && nis[i].isAvailable() )
				return( true );
		}
	}
 
	return( false );
}

让屏幕不会关闭

在布局的 layout 里加属性:

android:keepScreenOn="true"

也可以用代码的方法:

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

开源控件

ActionBarSherlock

ViewPagerTabs

  • 模拟 Android Market 的 ViewPager tab 效果的控件,效果比 ViewPagerIndicator 好。内部引用 Support Package,如果跟 ActionBarSherlock 一起使用时,需要注意要用最新版的 ActionBarSherlock 来保证其内含 Support Package 的时效性

TouchList

PullToRefresh - Johan

PullToRefresh - Erik

PullToRefresh - Tim Mahoney

PullToRefresh - guillep

PullToRefresh - Chris Banes

PullToRefresh

PullToRefresh - woozzu

android-drag-and-drop-listview

android_notes.txt · 最后更改: 2014/04/01 06:26 由 2ndboy