参考地址:http://developer.android.com/training/notify-user/index.html
通知(Notification)是Android中使用的非常多的一个事件提示机制。
例子中的Notification是基于Support Library中的NotificationCompat.Builder类。我们使用时要继承这个类,它提供了各个平台最好的Notification支持。
直接上代码:
NotificationCompat.Builder mBuilder =
new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle("My notification")
.setContentText("Hello World!");
其中,三个参数分别指通知的icon,标题和内容。
action不是必需的,但应该为Notification提供至少一个action,它直接从Notification跳转到Activity。在Notification中action是PendingIntent中的Intent定义的。当从Notification启动一个Activity,你必须保证用户的导航体验。在下面的代码片段中,我们没有创建一个人工的Back Stack。
Intent resultIntent = new Intent(this, ResultActivity.class);
...
// Because clicking the notification opens a new ("special") activity, there's // no need to create an artificial back stack. PendingIntent resultPendingIntent = PendingIntent.getActivity( this, 0, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT );
将NotificationCompat.Builder和Pendingintent联系起来,使用一下代码:
PendingIntent resultPendingIntent;
...
mBuilder.setContentIntent(resultPendingIntent);
NotificationCompat.Builder mBuilder;
...
// Sets an ID for the notification
int mNotificationId = 001;
// Gets an instance of the NotificationManager service
NotificationManager mNotifyMgr =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
// Builds the notification and issues it.
mNotifyMgr.notify(mNotificationId, mBuilder.build());
Notification启动Activity,有两种场景:一种是普通的Activity,另一种是特殊的Activity。
1、在manifest中创建一个activity:
<activity android:name=".MainActivity" android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ResultActivity" android:parentActivityName=".MainActivity">
<meta-data android:name="android.support.PARENT_ACTIVITY" android:value=".MainActivity"/>
</activity>
2、创建一个Back Stack:
int id = 1;
...
Intent resultIntent = new Intent(this, ResultActivity.class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
// Adds the back stack
stackBuilder.addParentStack(ResultActivity.class);
// Adds the Intent to the top of the stack
stackBuilder.addNextIntent(resultIntent);
// Gets a PendingIntent containing the entire back stack
PendingIntent resultPendingIntent =
stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
...
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setContentIntent(resultPendingIntent);
NotificationManager mNotificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.notify(id, builder.build());
一个特殊的Activity不需要创建Back Stack,所以不是必须在Manifest中定义Activity层级(设置父Activity),也不需要addParentStack()方法来创建一个Back栈。取而代之的是,在Manifest中设置Activity选项。比如:
<activity
android:name=".ResultActivity"
...
android:launchMode="singleTask"
android:taskAffinity=""
android:excludeFromRecents="true">
</activity>
...
android:taskAffinity=”“:和FLAG_ACTIVITY_NEW_TASK联合起来使用,可以保证Activity不会进入App默认的Stack中。任何存在的有默认affinity的Stack不受影响。
android:excludeFromRecents=”true”:从Recents中排除新的task,这样导航就不会通过Back访问到它。
// Instantiate a Builder object.
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
// Creates an Intent for the Activity
Intent notifyIntent =
new Intent(new ComponentName(this, ResultActivity.class));
// Sets the Activity to start in a new, empty task
notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_CLEAR_TASK);
// Creates the PendingIntent
PendingIntent notifyIntent =
PendingIntent.getActivity(
this,
0,
notifyIntent,
PendingIntent.FLAG_UPDATE_CURRENT
);
// Puts the PendingIntent into the notification builder
builder.setContentIntent(notifyIntent);
// Notifications are issued by sending them to the
// NotificationManager system service.
NotificationManager mNotificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// Builds an anonymous Notification object from the builder, and
// passes it to the NotificationManager
mNotificationManager.notify(id, builder.build());
当你需要对于相同类型的事件多次发出通知,你应该避免发一个全新的Notification。相反,你应该考虑更新之前存在的通知,通过改变它的一些值或添加一些内容,或两者兼而有之。
创建一个可以修改的Notification,需要使用NotificationManager.notify(ID, notification)给Notification定义一个ID,那么后面更新时,使用相同的id进行更新。
mNotificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// Sets an ID for the notification, so it can be updated
int notifyID = 1;
mNotifyBuilder = new NotificationCompat.Builder(this)
.setContentTitle("New Message")
.setContentText("You've received new messages.")
.setSmallIcon(R.drawable.ic_notify_status)
numMessages = 0;
// Start of a loop that processes data and then notifies the user
...
mNotifyBuilder.setContentText(currentText)
.setNumber(++numMessages);
// Because the ID remains unchanged, the existing notification is
// updated.
mNotificationManager.notify(
notifyID,
mNotifyBuilder.build());
...
以下情况之一Notification就不可见了(被删除了):
Notification有两种样式,一种的Normal的,一个是BigView的。BigView的Notification只有当被拉下来时才会显示。
BigView的样式是从Android 4.1才开始引入,之前的老版本不支持。下面介绍如何在Normal的Notification集成进BigView的样式。
图1:notifications-normalview
图2:notifications-bigview
Demo程序中使用IntentService子类(PingService)来创建和发布Notification。
下面的代码片段是在IntentService的onHandleIntent()方法中,创建PendingIntent的:
Intent resultIntent = new Intent(this, ResultActivity.class);
resultIntent.putExtra(CommonConstants.EXTRA_MESSAGE, msg);
resultIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_CLEAR_TASK);
// Because clicking the notification launches a new ("special") activity,
// there's no need to create an artificial back stack.
PendingIntent resultPendingIntent =
PendingIntent.getActivity(
this,
0,
resultIntent,
PendingIntent.FLAG_UPDATE_CURRENT
);
// This sets the pending intent that should be fired when the user clicks the
// notification. Clicking the notification launches a new activity.
builder.setContentIntent(resultPendingIntent);
下面的代码展示如何在big view中显示Button:
// Sets up the Snooze and Dismiss action buttons that will appear in the
// big view of the notification.
Intent dismissIntent = new Intent(this, PingService.class);
dismissIntent.setAction(CommonConstants.ACTION_DISMISS);
PendingIntent piDismiss = PendingIntent.getService(this, 0, dismissIntent, 0);
Intent snoozeIntent = new Intent(this, PingService.class);
snoozeIntent.setAction(CommonConstants.ACTION_SNOOZE);
PendingIntent piSnooze = PendingIntent.getService(this, 0, snoozeIntent, 0);
下面的代码片段展示创建Builder对象。它设置了BigView的big text样式,也设置了它的内容作为提醒消息。它使用addAction增加了Snooze和Dismiss按钮(这两个按钮绑定了PendingIntent)显示在Notification的BigView中。
// Constructs the Builder object.
NotificationCompat.Builder builder =
new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_stat_notification)
.setContentTitle(getString(R.string.notification))
.setContentText(getString(R.string.ping))
.setDefaults(Notification.DEFAULT_ALL) // requires VIBRATE permission
/* * Sets the big view "big text" style and supplies the * text (the user's reminder message) that will be displayed * in the detail area of the expanded notification. * These calls are ignored by the support library for * pre-4.1 devices. */
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(msg))
.addAction (R.drawable.ic_stat_dismiss,
getString(R.string.dismiss), piDismiss)
.addAction (R.drawable.ic_stat_snooze,
getString(R.string.snooze), piSnooze);
Notification中可以显示一个动态的进度条,用来显示正在进行的任务。如何你能计算任务的事件和长度,可以使用“确定”的指示器(进度条),如果不能确定任务长短,则使用“不确定”指示器(活动指示器)。
通过添加setProgress(max, progress, false),然后发布Notification则可以在Notification中增加一个进度条,第三个参数指的就是任务长度是否确定(indeterminate (true) or determinate (false))。
当progress慢慢增长到max时,可以继续显示Notification也可以清除掉。不管怎样,可以通过setProgress(0, 0, false)清掉进度条:
int id = 1;
...
mNotifyManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mBuilder = new NotificationCompat.Builder(this);
mBuilder.setContentTitle("Picture Download")
.setContentText("Download in progress")
.setSmallIcon(R.drawable.ic_notification);
// Start a lengthy operation in a background thread
new Thread(
new Runnable() {
@Override
public void run() {
int incr;
// Do the "lengthy" operation 20 times
for (incr = 0; incr <= 100; incr+=5) {
// Sets the progress indicator to a max value, the
// current completion percentage, and "determinate"
// state
mBuilder.setProgress(100, incr, false);
// Displays the progress bar for the first time.
mNotifyManager.notify(id, mBuilder.build());
// Sleeps the thread, simulating an operation
// that takes time
try {
// Sleep for 5 seconds
Thread.sleep(5*1000);
} catch (InterruptedException e) {
Log.d(TAG, "sleep failure");
}
}
// When the loop is finished, updates the notification
mBuilder.setContentText("Download complete")
// Removes the progress bar
.setProgress(0,0,false);
mNotifyManager.notify(id, mBuilder.build());
}
}
// Starts the thread by calling the run() method in its Runnable
).start();
要显示一个不确定(持续)的进度条,可以设置setProgress(0, 0, true)。
// Sets the progress indicator to a max value, the current completion
// percentage, and "determinate" state
mBuilder.setProgress(100, incr, false);
// Issues the notification
mNotifyManager.notify(id, mBuilder.build());
将上面的代码替换成下面的
// Sets an activity indicator for an operation of indeterminate length
mBuilder.setProgress(0, 0, true);
// Issues the notification
mNotifyManager.notify(id, mBuilder.build());
例子下载:NotificationExample
参考地址:http://developer.android.com/training/swipe/index.html
Android平台提供了下拉刷新(swipe-to-refresh)的组件,让用户可以手动拉动去刷新数据。
注意:这个需要最新的 Android v4 Support Library APIs支持。
Swipe-to-Refresh是通过SwipeRefreshLayout组件实现的。通过用户下拉,提供一个有特色的进度条,然后触发回调刷新数据。可以在ListView或者GridView的布局文件外加一个SwipeRefreshLayout的父元素来实现下拉刷新。
比如在ListView外面加一个SwipeRefreshLayout如下:
<android.support.v4.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swiperefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.v4.widget.SwipeRefreshLayout>
也可以在ListFragment外面套一层SwipeRefreshLayout。SwipeRefreshLayout自动为id为@android:id/list的ListView添加下拉刷新功能。
可以在Action Bar上添加一个刷新按钮,这样用户不用通过下拉手势来刷新数据。比如,可以通过键盘或软键盘来触发刷新事件。
你可以通过android:showAsAction=never设置一个Menu,不要设置成Button,来添加一个刷新“按钮”。
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:id="@+id/menu_refresh" android:showAsAction="never" android:title="@string/menu_refresh"/>
</menu>
响应刷新手势需要实现SwipeRefreshLayout.OnRefreshListener接口下的onRefresh()方法,你应该将真正刷新的动作写在一个独立的方法里,这样不管是下拉刷新还是点击ActionBar上的刷新按钮,都可以复用。
当更新完数据,调用setRefreshing(false),这个将使SwipeRefreshLayout去掉进度条并更新UI,下面就是一个使用onRefresh() 调用 myUpdateOperation()方法更新ListView数据的例子:
/* * Sets up a SwipeRefreshLayout.OnRefreshListener that is invoked when the user * performs a swipe-to-refresh gesture. */
mySwipeRefreshLayout.setOnRefreshListener(
new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
Log.i(LOG_TAG, "onRefresh called from SwipeRefreshLayout");
// This method performs the actual data-refresh operation.
// The method calls setRefreshing(false) when it's finished.
myUpdateOperation();
}
}
);
当用户通过onOptionsItemSelected()方法触发菜单中的刷新行为,需要手工设置setRefreshing(true),来执行刷新操作,然后调用真正刷新数据的方法,在刷新完成时调用setRefreshing(false)。例如:
/* * Listen for option item selections so that we receive a notification * when the user requests a refresh by selecting the refresh action bar item. */
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
// Check if user triggered a refresh:
case R.id.menu_refresh:
Log.i(LOG_TAG, "Refresh menu item selected");
// Signal SwipeRefreshLayout to start the progress indicator
mySwipeRefreshLayout.setRefreshing(true);
// Start the refresh background task.
// This method calls setRefreshing(false) when it's finished.
myUpdateOperation();
return true;
}
// User didn't trigger a refresh, let the superclass handle this action
return super.onOptionsItemSelected(item);
}
例子下载:SwipeRefreshLayoutBasic
SwipeRefreshLayoutListFragment
参考地址:http://developer.android.com/training/search/index.html
Android自带的搜索框可以很方便为用户提供一致的用户体验。有两种实现方法,一种是SearchView(Android3.0提供),另一种为兼容老版本Android系统提供了一个默认的搜索对话框(default search dialog)。
Android3.0以后,系统提供了一个SearchView放在ActionBar作为首选的搜索方式。
注意:后面会提到兼容Android2.1及以前版本,如何使用SearchView.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/search" android:title="@string/search_title" android:icon="@drawable/ic_search" android:showAsAction="collapseActionView|ifRoom" android:actionViewClass="android.widget.SearchView" />
</menu>
在Activity的onCreateOptionsMenu() 方法中,解析菜单资源(res/menu/options_menu.xml)。
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.options_menu, menu);
return true;
}
SearchView的搜索行为配置放在文件res/xml/searchable.xml中,这个文件中最少需要配置一个android:label属性(类似于Manifest中 或 的android:label属性)。我们建议加多一个android:hint 属性提示用户需要在搜索框输入啥。
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android" android:label="@string/app_name" android:hint="@string/search_hint" />
在Manifest文件中指定 属性指向searchable.xml,那么需要如下配置:
<activity ... >
...
<meta-data android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
在onCreateOptionsMenu()方法创建之前,将搜索配置与SearchView通过setSearchableInfo(SearchableInfo)进行关联。
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.options_menu, menu);
// Associate searchable configuration with the SearchView
SearchManager searchManager =
(SearchManager) getSystemService(Context.SEARCH_SERVICE);
SearchView searchView =
(SearchView) menu.findItem(R.id.search).getActionView();
searchView.setSearchableInfo(
searchManager.getSearchableInfo(getComponentName()));
return true;
}
调用getSearchableInfo()方法返回的SearchableInfo对象是由searchable.xml文件生成的。当searchable.xml正确关联到了SearchView,当提交一个查询时SearchView将启动一个带ACTION_SEARCH Intent的Activity。你需要在Activity中处理查询动作。
创建一个处理搜索结果的Activity,需要指定ACTION_SEARCH的Action。例如:
<activity android:name=".SearchResultsActivity" ... >
...
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
...
</activity>
在你的搜索Activity的onCreate()中处理搜索行为。
如果你的搜索Activity的启动模式是android:launchMode=”singleTop”,那么还需要在onNewIntent()中处理搜索动作。在singleTop模式下,只会创建一个Activity实例。后面启动这个Activity,不会创建新的Activity。
代码如下:
public class SearchResultsActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
...
handleIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
...
handleIntent(intent);
}
private void handleIntent(Intent intent) {
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
String query = intent.getStringExtra(SearchManager.QUERY);
//use the query to search your data somehow
}
}
...
}
如果你现在运行App,SearchView将接收用户的查询并启动一个ACTION_SEARCH的Activity处理搜索结果。接下来由你决定如何保存和搜索用户要搜索的内容了。
存数数据有多种方式,有线上的数据库,有本地的数据库SQLite,有txt文本文件,你可以选择最好最合适的方式去存储数据。这里创建SQLite的虚拟表来展示强大的全文本搜索功能。这张表的数据有txt文件中填充,里面的内容是每一行一个单词和他对应的定义。
public class DatabaseTable {
private final DatabaseOpenHelper mDatabaseOpenHelper;
public DatabaseTable(Context context) {
mDatabaseOpenHelper = new DatabaseOpenHelper(context);
}
}
在DatabaseTable类中扩展SQLiteOpenHelper创建一个内部类。SQLiteOpenHelper类定义了抽象方法,你必须重写里面的抽象方法,以便在必要时可以创建表和升级表。例如,下面是在字典App中声明创建数据库表一些代码:
public class DatabaseTable {
private static final String TAG = "DictionaryDatabase";
//The columns we'll include in the dictionary table
public static final String COL_WORD = "WORD";
public static final String COL_DEFINITION = "DEFINITION";
private static final String DATABASE_NAME = "DICTIONARY";
private static final String FTS_VIRTUAL_TABLE = "FTS";
private static final int DATABASE_VERSION = 1;
private final DatabaseOpenHelper mDatabaseOpenHelper;
public DatabaseTable(Context context) {
mDatabaseOpenHelper = new DatabaseOpenHelper(context);
}
private static class DatabaseOpenHelper extends SQLiteOpenHelper {
private final Context mHelperContext;
private SQLiteDatabase mDatabase;
private static final String FTS_TABLE_CREATE =
"CREATE VIRTUAL TABLE " + FTS_VIRTUAL_TABLE +
" USING fts3 (" +
COL_WORD + ", " +
COL_DEFINITION + ")";
DatabaseOpenHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
mHelperContext = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
mDatabase = db;
mDatabase.execSQL(FTS_TABLE_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ newVersion + ", which will destroy all old data");
db.execSQL("DROP TABLE IF EXISTS " + FTS_VIRTUAL_TABLE);
onCreate(db);
}
}
}
下面的代码想你展示如何从txt文件(res/raw/definitions.txt)中读取数据插入到表里,使用了多线程来读取数据,以防阻塞UI线程。(注:你可能需要写一个回调来处理读取完成的动作)
private void loadDictionary() {
new Thread(new Runnable() {
public void run() {
try {
loadWords();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
}
private void loadWords() throws IOException {
final Resources resources = mHelperContext.getResources();
InputStream inputStream = resources.openRawResource(R.raw.definitions);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
try {
String line;
while ((line = reader.readLine()) != null) {
String[] strings = TextUtils.split(line, "-");
if (strings.length < 2) continue;
long id = addWord(strings[0].trim(), strings[1].trim());
if (id < 0) {
Log.e(TAG, "unable to add word: " + strings[0].trim());
}
}
} finally {
reader.close();
}
}
public long addWord(String word, String definition) {
ContentValues initialValues = new ContentValues();
initialValues.put(COL_WORD, word);
initialValues.put(COL_DEFINITION, definition);
return mDatabase.insert(FTS_VIRTUAL_TABLE, null, initialValues);
}
调用loadDictionary()向表里插入数据。这个方法在DatabaseOpenHelper中的onCreate()中创建表的代码之后:
@Override
public void onCreate(SQLiteDatabase db) {
mDatabase = db;
mDatabase.execSQL(FTS_TABLE_CREATE);
loadDictionary();
}
写一些SQL语句进行查询:
public Cursor getWordMatches(String query, String[] columns) {
String selection = COL_WORD + " MATCH ?";
String[] selectionArgs = new String[] {query+"*"};
return query(selection, selectionArgs, columns);
}
private Cursor query(String selection, String[] selectionArgs, String[] columns) {
SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
builder.setTables(FTS_VIRTUAL_TABLE);
Cursor cursor = builder.query(mDatabaseOpenHelper.getReadableDatabase(),
columns, selection, selectionArgs, null, null, null);
if (cursor == null) {
return null;
} else if (!cursor.moveToFirst()) {
cursor.close();
return null;
}
return cursor;
}
调用getWordMatches()方法进行查询,查询结果返回一个Cursor。这个例子在搜索Activity中的handleIntent()中调用getWordMatches()。
DatabaseTable db = new DatabaseTable(this);
...
private void handleIntent(Intent intent) {
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
String query = intent.getStringExtra(SearchManager.QUERY);
Cursor c = db.getWordMatches(query, null);
//process Cursor and display results
}
}
SearchView和ActionBar是Android3.0及以上版本的才有的,为兼容旧版本,需要使用系统的搜索对话框(search dialog)来显示在App的上层。
在Manifest文件中设置最小支持3.0以前,Target是3.0以后的,这样系统会在Android3.0及以上的机器上使用ActionBar而在旧版本机器上使用传统菜单(Menu)。
<uses-sdk android:minSdkVersion="7" android:targetSdkVersion="15" />
<application>
...
在选择搜索菜单时,旧版中调用onSearchRequested()来显示一个搜索对话框。
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.search:
onSearchRequested();
return true;
default:
return false;
}
}
在onCreateOptionsMenu() 中检查Android的版本,判断如果是3.0及以上,则使用SearchView。
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.options_menu, menu);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
SearchManager searchManager =
(SearchManager) getSystemService(Context.SEARCH_SERVICE);
SearchView searchView =
(SearchView) menu.findItem(R.id.search).getActionView();
searchView.setSearchableInfo(
searchManager.getSearchableInfo(getComponentName()));
searchView.setIconifiedByDefault(false);
}
return true;
}
Demo地址:https://github.com/bendeng/SearchView
参考地址:http://developer.android.com/training/keyboard-input/index.html
Android系统提供了屏幕上使用的软键盘和实体键盘两种键盘输入方式。
在 元素中使用android:inputType属性指定输入类型,比如‘phone’,则键盘只能输入电话号码的数字:
<EditText android:id="@+id/phone" android:layout_width="fill_parent" android:layout_height="wrap_content" android:hint="@string/phone_hint" android:inputType="phone" />
<EditText
android:id="@+id/password"
android:hint="@string/password_hint"
android:inputType="textPassword"
... />
textAutoCorrect允许你自动纠正拼写错误。下面例子就是让第一个字母大写以及自动纠错的属性:
<EditText
android:id="@+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType=
"textCapSentences|textAutoCorrect"
... />
如果你的TextView不允许多行(多行属性这样设置android:inputType=”textMultiLine”),那么软键盘右下角的按钮显示Next或Done。你可以设置更多的Action比如Send或者Go。设置键盘Action的Button,设置android:imeOptions属性即可:
<EditText android:id="@+id/search" android:layout_width="fill_parent" android:layout_height="wrap_content" android:hint="@string/search_hint" android:inputType="text" android:imeOptions="actionSend" />
通过设置TextView.OnEditorActionListener 的监听,监听action的事件,比如Send:
EditText editText = (EditText) findViewById(R.id.search);
editText.setOnEditorActionListener(new OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
boolean handled = false;
if (actionId == EditorInfo.IME_ACTION_SEND) {
sendMessage();
handled = true;
}
return handled;
}
});
在Manifest中设置Activity的属性android:windowSoftInputMode即可:
<application ... >
<activity
android:windowSoftInputMode="stateVisible" ... >
...
</activity>
...
</application>
下面的例子时在一个View中输入某些内容,调用requestFocus()先获取焦点,然后使用showSoftInput()打开输入法:
public void showSoftKeyboard(View view) {
if (view.requestFocus()) {
InputMethodManager imm = (InputMethodManager)
getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
}
}
注意:一旦输入法显示后,就不能通过代码将其隐藏。只能通过比如实体返回键隐藏。
<activity
android:windowSoftInputMode="stateVisible|adjustResize" ... >
...
</activity>
adjustResize很重要,它会让输入法显示挡住Activity下面的部分时自动上移,露出输入框。
定义focus顺序使用android:nextFocusForward属性。下面的例子,焦点从button1 到editText1 再到button2:
<RelativeLayout ...>
<Button
android:id="@+id/button1"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:nextFocusForward="@+id/editText1"
... />
<Button
android:id="@+id/button2"
android:layout_below="@id/button1"
android:nextFocusForward="@+id/button1"
... />
<EditText
android:id="@id/editText1"
android:layout_alignBottom="@+id/button2"
android:layout_toLeftOf="@id/button2"
android:nextFocusForward="@+id/button2"
... />
...
</RelativeLayout>
如下例子:
<Button
android:id="@+id/button1"
android:nextFocusRight="@+id/button2"
android:nextFocusDown="@+id/editText1"
... />
<Button
android:id="@id/button2"
android:nextFocusLeft="@id/button1"
android:nextFocusDown="@id/editText1"
... />
<EditText
android:id="@id/editText1"
android:nextFocusUp="@id/button1"
... />
可以通过实现KeyEvent.Callback接口,拦截或直接处理键盘的事件,而不是默认的让系统处理键盘输入。每一个Activity和View都实现了KeyEvent.Callback接口,我们只需要覆盖其中的方法即可:
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_D:
moveShip(MOVE_LEFT);
return true;
case KeyEvent.KEYCODE_F:
moveShip(MOVE_RIGHT);
return true;
case KeyEvent.KEYCODE_J:
fireMachineGun();
return true;
case KeyEvent.KEYCODE_K:
fireMissile();
return true;
default:
return super.onKeyUp(keyCode, event);
}
}
响应加了Shift 或Control键的按键事件。简单的处理是使用 isShiftPressed() 和isCtrlPressed()判断Shift或Ctrl键是否按下了,以此来做逻辑处理:
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch (keyCode) {
...
case KeyEvent.KEYCODE_J:
if (event.isShiftPressed()) {
fireLaser();
} else {
fireMachineGun();
}
return true;
case KeyEvent.KEYCODE_K:
if (event.isShiftPressed()) {
fireSeekingMissle();
} else {
fireMissile();
}
return true;
default:
return super.onKeyUp(keyCode, event);
}
}
参考地址:http://developer.android.com/training/articles/memory.html
内存(RAM)在任何软件开发中都是非常有价值的资源,在移动开发中尤甚。在Android中不存在交换分区(但使用paging和 memory-mapping(mmapped)管理内存),尽管Dalvik虚拟机有内存回收机制,但关于内存的分配和释放必须重视的事情,每个App都有限制的可使用内存。
为了让垃圾回收器能及时回收内存,要避免引起内存泄漏(Memory Leak)。
Android如何彻底地清理App内存呢?答案是只能通过解除对象的引用,让其可以被垃圾回收器回收。
为了满足一切需要,Android尝试使用多进程共享内存,它采用下面的方式:
Android为每个App设置了一个严格的heap大小的限制。这个精确的heap大小不同的设备不一样,它决定了app可以使用的堆内存大小。一旦达到这个限制,就会抛出OutOfMemoryError(OOM).异常。我们可以通过getMemoryClass()方法获取app可以使用的最大内存大小。
Android不使用交换分区,但保存那些不在前台(不可见)的进程到最近最少使用(least-recent used LRU)的cache中。这样,在一个App离开后,不会退出,再次启动时,可以快速的切换到这个app。
LRU中的进程cache会影响系统的整体性能,所以在系统监测到剩余内存很低时,会将LRU中的进程进行杀死,但也会考虑那些特别占用内存的进程。
在 Android 2.3.x (API 10)及以前版本,bitmap对象的像素数据是放在本地内存中的而不是堆内存,这样很难去debug,大部分的堆内存分析工具不能看到本地内存分配。然而,在Android3.0(API 11)开始,bitmap的像素数据分配到了Dalvik heap中,这样提高了垃圾回收的几率以及更易调试。
Android framework中提供了优化的数据容器,比如SparseArray、SparseBooleanArray和 LongSparseArray。通用的HashMap在内存效率上较低是因为要为每一个映射准备一个单独的Entry对象。SparseArray更高效是因为它避免了key的自动装箱操作。
一般,开发人员将抽象简单地作为一个“良好的编程实践“,因为抽象可以提高代码的灵活性和维护性。然而,抽象代价巨大:通常需要大量更多的需要执行的代码,需要更多的时间和更多的RAM代码映射到内存中。如果你使用抽象没有明显的好处,你应该避免使用他们。
Protocol buffers
是语言中立的,平台无关的,可扩展的机制,由谷歌设计,用于序列化结构化数据。相对于XML,更小,更快,更简单。如果采用protobufs,那么整个客户端都要使用这种数据结构。常规的protobufs会生成冗长的代码,会引发各种问题,并很占用内存空间
这些框架往往执行的过程中初始化扫描代码的注释,从而需要大量的代码映射到内存,即使你不需要它。这些映射page分配到clean RAM中,所以Android可以删除它们,但直到页面在内存中已经离开很长一段时间才会发生。
剩下的全是理论:
1、小心使用外部lib,鱼目混杂
2、优化整体性能
3、使用ProGuard剔除无用代码
4、最后生成APK时使用zipalign 。
5、分析你的内存使用情况
6、使用多进程。在Service中设置process属性将其放到单独的进程中运行:<service android:name=".PlaybackService"
android:process=":background" />
参考地址:http://developer.android.com/training/articles/perf-tips.html
编写高质量的代码有2点基本原则:
1、没必要做的工作尽量不做
2、能不用分配内存的地方尽量不分配
有一些棘手的问题是Android版本分化严重,有时在设备A上运行性能良好,在设备B上效果较差,甚至各种设备上的表现总不一致。我们需要让自己的代码在各种设备上都性能较好,需要在很多细节上下功夫。
大部分Andriod程序是Java开发,和C/C++手动分配和释放内存不一样,,Java中有垃圾回收器。但这并不意味着我们可以肆意的创建对象到内存中,而不管这些对象的生命周期。每一次的对象创建,都对垃圾回收器进行垃圾回收是一次触动,我们的原则是尽量不需要创建的对象就不要创建。比如:
1、我们需要拼接一个Sting字符串,很多人习惯使用多个String就进行相加“+”,这其实会造成多次的String对象创建,这是完全没必要的。我们使用StringBuffer或者StirngBuilder(非线程安全)进行append,则完全可以避免这种情况发生。
2、平行的一维数组比一个多维数组效率更高。
❶int数组比Integer数组要好很多,这适用于其它基本数据类型
❷当存储元组类型(foo,bar)对象时,使用foo[]和bar[]两个数组要比(foo,bar)好很多。当然设计API给别人访问时要做出让步,自己内部使用时,最好使用平行的一维数组
当不需要访问一个对象的内部属性时,尝试使用静态方法。静态方法比普通方法的调用快15-20%。
static int intVal = 42;
static String strVal = "Hello, world!";
编译器生成一个类的初始化方法,要调用方法。这个方法在第一次使用时调用,它储存42到intVal变量中,并从类文件string常量表中提取一个String引用赋给strVal。当这些值在后面被引用时,他们通过字段查找进行访问。我们使用final关键词改善这种情况:
static final int intVal = 42;
static final String strVal = "Hello, world!";
这样类不用再调用方法,因为这些常量进入了dex文件的静态域初始化中。代码访问intVal,直接使用42,而对strVal的访问也相对廉价的string常量命令,因为不用查找字段来获取了。
注意:这种优化只针对基本数据类型和String常量,不是任意的数据类型。然而,不管何时,使用final static都是好的实践。
很多语言都习惯使用get和set方法来访问对象属性,这是一个好的习惯。但在Android中,这种方式比直接访问属性成员要昂贵很多。在public的接口中,使用get和set时合理的,但在类的内部,直接访问成员变量比使用简单的get方法要快很多。不使用JIT,快3倍;使用JIT,快7倍。
下面是遍历一个数组的代码:
static class Foo {
int mSplat;
}
Foo[] mArray = ...
public void zero() {
int sum = 0;
for (int i = 0; i < mArray.length; ++i) {
sum += mArray[i].mSplat;
}
}
public void one() {
int sum = 0;
Foo[] localArray = mArray;
int len = localArray.length;
for (int i = 0; i < len; ++i) {
sum += localArray[i].mSplat;
}
}
public void two() {
int sum = 0;
for (Foo a : mArray) {
sum += a.mSplat;
}
}
zero()方法是最慢的,因为JIT不能避免循环中每一次取数组length的开销。one()快一点。它将数组赋给一个本地变量,并取了长度。这样避免了每一次循环都要查找外部变量。two()最快,并且不需要JIT(one()需要JIT)。你应该默认使用增强的循环来遍历。
public class Foo {
private class Inner {
void stuff() {
Foo.this.doStuff(Foo.this.mValue);
}
}
private int mValue;
public void run() {
Inner in = new Inner();
mValue = 27;
in.stuff();
}
private void doStuff(int value) {
System.out.println("Value is " + value);
}
}
这里定义了一个私有的内部类(Foo Inner),并在外部类中调用内部类中的成员和方法,这种情况是合理的。问题在于,虚拟机从Foo Inner中访问其外部类Foo的私有成员是非法的,因为Foo和Foo$Inner是不同的类,尽管Java允许这样访问。编译器做了下面的操作:
/*package*/ static int Foo.access$100(Foo foo) {
return foo.mValue;
}
/*package*/ static void Foo.access$200(Foo foo, int value) {
foo.doStuff(value);
}
作为经验,在Android设备上,浮点型数据比整型数据慢2倍。
从速度上来讲,float和double在一般现代机器上看不出区别。但在空间占用上,double要多2倍多。
使用系统自带的lib,原因就不用多说了。比如String.indexOf()方法及相关的API,Dalvik 虚拟机有了固有的内联。同样,使用System.arraycopy()方法比在Nexus one中运行手写的循环方法要快9倍。
使用NDK开发app不一定比Java开发应用更有效率。首先,从java->native有一个过渡,是一个开销,另外JIT不能跨越这个边界取优化。如果在native分配各种内存和资源,垃圾回收器是无法回收的。使用native你也需要为各个架构的CPU编译so库。
native方法的主要用处是当你有了一个native的代码库想移植到Android时才用,而不是为了“加速”某些java代码用它。
在没有JIT(Android在2.2上引入的JIT)的设备上,通过一个明确的变量类型的变量来调用方法比使用接口变量更有效率(比如,使用Hashmap map比Map map调用方法要更高效,尽管两个对象都是Hashmap)。实际上不是2倍的慢,只是6%的慢。此外,JIT使这两者效率没有区别。
在没有JIT的设备上,缓存成员访问大约比重复直接访问成员快20%,在JIT模式下,成员访问开销和本地访问差不多相同,所以,不用再去优化这一块了,没有价值,除非你感觉它是你的代码更好读了。(这也适合final、static、以及final static修饰的成员)
Profiling with Traceview and dmtracedump
Analyzing UI Performance with Systrace
参考地址:http://developer.android.com/training/articles/perf-anr.html
ANR是Application Not Responding的简称,意思是应用程序无响应。在Android系统中,UI操作超过5秒,BroadcastReceiver中操作超过10秒,都会导致ANR,系统会弹出ANR的对话框,app会崩溃。
Android应用程序默认运行在UI线程(主线程)中,UI线程上的长时间操作很可能导致ANR,因此在UI线程上的操作,要尽可能的轻(结束)。在Activity中的onCreate()和onResume()方法中尽可能进行少的工作。网络请求、数据库操作、或者解析bitmap等繁重的计算任务等任务,务必放在子线程中进行。
最有效的方式就是使用AsyncTask来创建工作线程,实现其中的doInBackground()方法。
private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
// Do the long-running work in here
protected Long doInBackground(URL... urls) {
int count = urls.length;
long totalSize = 0;
for (int i = 0; i < count; i++) {
totalSize += Downloader.downloadFile(urls[i]);
publishProgress((int) ((i / (float) count) * 100));
// Escape early if cancel() is called
if (isCancelled()) break;
}
return totalSize;
}
// This is called each time you call publishProgress()
protected void onProgressUpdate(Integer... progress) {
setProgressPercent(progress[0]);
}
// This is called when doInBackground() is finished
protected void onPostExecute(Long result) {
showNotification("Downloaded " + result + " bytes");
}
}
//这样执行
new DownloadFilesTask().execute(url1, url2, url3);
你可能想使用Thread或HandlerThread来做,尽管它们比AsyncTask复杂。如果使用它们的话,你需要调用Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND)来设置后台线程的优先级。如果不设置这个优先级的话,这个线程可能会让app运行换慢,因为这个线程默认是和Ui线程时一样的优先级。
当实现Thread或HandlerThread时,在等待工作线程来完成任务时请确保你的UI线程不会阻塞-不要调用Thread.wait()或Thread.sleep()。我们使用Handler在多线程中通信。
尽管 BroadcastReceiver中的时间要到10秒才会ANR,我们也要避免在其中做耗时的工作。IntentService是个很好的方案。
提示:可以使用StrictMode 来帮助查找在主线程中潜在的耗时操作,如网络和数据库操作。
一般,100-200ms是用户感觉到慢和卡的临界值。这里一些建议帮助你避免ANR,让你的app看起来响应快: