google为了让广大Android开发者能够高效地创建优质的app,专门针对开发者提供了Training板块,这个板块的学习资料是最一手的,来自google android工程师之手的。这个资料是每一个Android开发者都应该学习的手册,并且它是不断更新的。链接:? https://developer.android.com/training/index.html 中文版: http://hukai.me/android-training-course-in-chinese/index.html
版本适配
Tip:为了能在几个Android版本中都能提供最好的特性和功能,你应该在你的app中使用Android Support Library,它能使你的app能在旧平台上使用最近的几个平台的APIs。
适配不同的系统版本俩种方法:
@SuppressLint("NewApi")
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
// 如果运行的环境 (部署到什么版本的手机 )大于3.0
if (android.os.Build.VERSION.SDK_INT > 11) {
SearchView searchView = (SearchView) menu.findItem(
R.id.action_search).getActionView();
searchView.setOnQueryTextListener(this);// 搜索的监听
}
return true;
}
Note:当解析XML资源时,Android会忽略当前设备不支持的XML属性。所以你可以安全地使用较新版本的XML属性
而不需要担心旧版本Android遇到这些代码时会崩溃。
国际化
为了支持多国语言,在 res/ 中创建一个额外的 values 目录以连字符和ISO国家代码结尾命名,比如 values-es/是为语言代码为"es"的区域设置的简单的资源文件的目录。Android会在运行时根据设备的区域设置,加载相应的资源。详见Providing
app/build.gradle
Android Studio使用Gradle 编译运行Android工程. 工程的每个模块以及整个工程都有一个build.gradle文件。通常你只需要关
注模块的build.gradle文件,该文件存放编译依赖设置,包括defaultConfig设置:
布局
使用字符资源:
屏幕适配
有4种普遍尺寸:小(small),普通(normal),大(large),超大(xlarge)
4种普遍分辨率:低精度(ldpi), 中精度(mdpi), 高精度(hdpi), 超高精度(xhdpi)
res/layout-large/
xhdpi: 2.0
hdpi: 1.5
mdpi: 1.0 (基准)
ldpi: 0.75
Note:低密度(ldpi)资源是非必要的,当你提供了hdpi的图像,系统会把hdpi的图像按比例缩小一半,去适配ldpi的屏幕。
Activity
为让新启动的activity能查询,定义key为一个public型的常量,通常使用应用程序包名作为前缀来定义意图键是很好的做法。在应用程序与其他应用程序进行交互时仍可以确保意图键唯一。
public final static String EXTRA_MESSAGE ="com.mycompany.myfirstapp.MESSAGE";
Intent intent = new Intent(this, DisplayMessageActivity.class);
EditText editText = (EditText) findViewById(R.id.edit_message);
String message = editText.getText().toString();
intent.putExtra(EXTRA_MESSAGE,message);
TextView textView = new TextView(this);
textView.setTextSize(40);
textView.setText(message);
// Set the text view as the activity layout
setContentView(textView);
使你的activity有一个透明背景:<activity android:theme="@android:style/Theme.Translucent">
不像其他编程范式一样:程序从 main() 方法开始启动。Android系统根据生命周期的不同阶段唤起对应的回调函数来执行代码。系统存在启动与销毁一个activity的一套有序的回调函数。
如何实现一个符合用户期待的app,你需要注意下面几点:
activity生命周期
重新创建Activity
public void onSaveInstanceState(Bundle savedInstanceState) {
// Save the user's current game state
savedInstanceState.putInt(STATE_SCORE, mCurrentScore);
savedInstanceState.putInt(STATE_LEVEL, mCurrentLevel);
super.onSaveInstanceState(savedInstanceState);
}
public void onRestoreInstanceState(Bundle savedInstanceState) {
// Always call the superclass so it can restore the view hierarchy
super.onRestoreInstanceState(savedInstanceState);
// Restore state members from saved instance
mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
mCurrentLevel = savedInstanceState.getInt(STATE_LEVEL);
}
建立隐式的Intent
尽管Android系统会确保每一个确定的intent会被系统内置的app(such as the Phone, Email, or Calendar app)之一接收,但是你还是应该在触发一个intent之前做验证是否有App接受这个intent的步骤。
Caution: 如果你触发了一个intent,而且没有任何一个app会去接收这个intent,那么你的app会crash。
为了验证是否有合适的activity会响应这个intent,需要执行queryIntentActivities() 来获取到能够接收这个intent的所有activity的list。如果返回的List非空,那么你才可以安全的使用这个intent。例如:
PackageManager packageManager = getPackageManager();
List<ResolveInfo> activities = packageManager.queryIntentActivities(intent, 0);
boolean isIntentSafe = activities.size() > 0;
如果 isIntentSafe 是 true , 那么至少有一个app可以响应这个intent。如果是 false 则说明没有app可以handle这个intent。Note:你必须在第一次使用之前做这个检查,若是不可行,则应该关闭这个功能。如果你知道某个确切的app能够handle这个intent,你也应该提供给用户去下载这个app的链接。?
一个完整的例子,演示了如何创建一个intent来查看地图,验证有app可以handle这个intent,然后启动它。
// Build the intent
Uri location = Uri.parse("geo:0,0?q=1600+Amphitheatre+Parkway,+Mountain+View,+California");
Intent mapIntent = new Intent(Intent.ACTION_VIEW, location);
// Verify it resolves
PackageManager packageManager = getPackageManager();
List<ResolveInfo> activities = packageManager.queryIntentActivities(mapIntent, 0);
boolean isIntentSafe = activities.size() > 0;
// Start an activity if it's safe
if (isIntentSafe) {
startActivity(mapIntent);
}
如果用户希望每次都弹出选择界面,而且每次都不确定会选择哪个app启动,例如分享功能,用户选择分享到哪个app都是不确定的,这个时候,需要强制弹出选择的对话框。(这种情况下用户不能选择默认启动的app)。为了显示chooser, 需要使用createChooser()来创建Intent
Intent intent = new Intent(Intent.ACTION_SEND);
...
// Always use string resources for UI text. This says something like "Share this photo with"
String title = getResources().getText(R.string.chooser_title);
// Create and start the chooser
Intent chooser = Intent.createChooser(intent, title);
startActivity(chooser);
这样就列出了可以响应 createChooser() 中Intent的app,并且指定了标题。
被别的应用启动
如果你的app的功能对别的app也有用,那么你的app应该做好响应的准备。例如,如果你创建了一个social app,它可以分享messages 或者 photos 给好友,那么最好你的app能够接收 ACTION_SEND ?的intent,这样当用户在其他app触发分享功能的时候,你的app能够出现在待选对话框。
为了使得其他的app能够启动你的activity,你需要在你的manifest文件的 <activity> 标签下添加 <intent-filter> 的属性。
例如,这个有intent filter的activity,当数据类型为文本或图像时会处理 ACTION_SEND 的intent。
<activity android:name="ShareActivity">
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
<data android:mimeType="image/*"/>
</intent-filter>
</activity>
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Get the intent that started this activity
Intent intent = getIntent();
Uri data = intent.getData();
// Figure out what to do based on the intent type
if (intent.getType().indexOf("image/") != -1) {
// Handle intents with image data ...
} else if (intent.getType().equals("text/plain")) {
// Handle intents with text ...
}
}
详情看:Intent过滤(92P)
Fragment
<fragment android:name="com.example.android.fragments.ArticleFragment"
android:id="@+id/article_fragment"
android:layout_weight="2"
android:layout_width="0dp"
android:layout_height="match_parent" />
Fragments之间的交互
定义一个接口
为了让fragment与activity交互,你可以在Fragment 类中定义一个接口,并且在activity中实现这个接口。Fragment在他们生命周期的onAttach()方法中捕获接口的实现,然后调用接口的方法来与Activity交互。
public class HeadlinesFragment extends ListFragment {
OnHeadlineSelectedListener mCallback;
// Container Activity must implement this interface
public interface OnHeadlineSelectedListener {
public void onArticleSelected(int position);
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mCallback = (OnHeadlineSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnHeadlineSelectedListener");
}
}
...
}
现在Fragment就可以通过调用 OnHeadlineSelectedListener 接口实例的 ??mCallback 中的 onArticleSelected() (也可以是其它方法)方法与activity传递消息。举个例子,在fragment中的下面的方法在用户点击列表条目时被调用,fragment 用回调接口来传递事件给父Activity.
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
mCallback.onArticleSelected(position);
}
实现接口
为了接收回调事件,宿主activity必须实现在Fragment中定义的接口。举个例子,下面的activity实现了上面例子中的接口。
public static class MainActivity extends Activity
implements HeadlinesFragment.OnHeadlineSelectedListener {
...
public void onArticleSelected(int position) {
// The user selected the headline of an article from the HeadlinesFragment
// Do something here to display that article
}
}
数据存储
写Shared Preference:通过类似putInt()与putString()方法来传递keys与values。然后执行commit() 来提交改变. (后来有建议除非是出于线程同步的需要,否则请使用apply()方法来替代commit(),因为后者有可能会卡到UI Thread.)
存储在内部还是外部
所有的Android设备都有两个文件存储区域:"internal" 与 "external" 存储。 那两个名称来自于早先的Android系统中,当时的大多设备都内置了不可变的内存(internal storage),然后再加上一个类似SD card(external storage)这样可以卸载的存储部件。后来有一些设备把"internal" 与"external" 的部分都做成不可卸载的内置存储了,虽然如此,但是这一整块还是从逻辑上有被划分为"internal"与"external"的。只是现在不再以是否可以卸载来区分了。 下面列出了两者的区别:
Internal storage:
External storage:
Tip: 尽管app是默认被安装到internal storage的,你还是可以通过在程序的manifest文件中声明android:installLocation 属性来指定程序也可以被安装到external storage。当某个程序的安装文件很大且用户的internal storage空间大于external storage时,他们会倾向与将该程序安装到external storage。
你可以执行openFileOutput() 来获取一个 FileOutputStream 用于写文件到internal目录
String filename = "myfile";
String string = "Hello world!";
FileOutputStream outputStream;
try{
outputStream = openFileOutput(filename, Context.MODE_PRIVATE);
outputStream.write(string.getBytes());
outputStream.close();
} catch(Exception e){
e.printStackTrace();
}
如果,你需要缓存一些文件,你可以使用createTempFile()。例如:下面的方法从URL中抽取了一个文件名,然后再在程序的internal缓存目录下创建了一个以这个文件名命名的文件。
public File getTempFile(Context context, String url) {
File file;
try {
String fileName = Uri.parse(url).getLastPathSegment();
file = File.createTempFile(fileName, null, context.getCacheDir());
catch (IOException e) {
// Error while creating file
}
return file;
}
你使用MODE_PRIVATE ,那么这些文件就不可能被其他app所访问
尽管external storage对于用户与其他app是可修改的,那么你可能会保存下面两种类型的文件。
如果你想要保存文件为public形式的,请使用getExternalStoragePublicDirectory()方法来获取一个 File 对象来表示存储在external storage的目录。这个方法会需要你带有一个特定的参数来指定这些public的文件类型,以便于与其他public文件进行分类。参数类型包括DIRECTORY_MUSIC 或者 DIRECTORY_PICTURES. 如下:
public File getAlbumStorageDir(String albumName) {
// Get the directory for the user's public pictures directory.
File file = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
如果你想要保存文件为私有的方式,你可以通过执行getExternalFilesDir() 来获取相应的目录,并且传递一个指示文件类型的参数。每一个以这种方式创建的目录都会被添加到external storage封装你的app目录下的参数文件夹下(如下则是albumName)。这下面的文件会在用户卸载你的app时被系统删除。如下示例:
public File getAlbumStorageDir(Context context, String albumName) {
// Get the directory for the app's private pictures directory.
File file = new File(context.getExternalFilesDir(
Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
请记住,getExternalFilesDir() 方法会创建的目录会在app被卸载时被系统删除。如果你的文件想在app被删除时仍然保留,请使用getExternalStoragePublicDirectory().
不管你是使用 getExternalStoragePublicDirectory() 来存储可以共享的文件,还是使用 getExternalFilesDir() 来储存那些对与你的app来说是私有的文件,有一点很重要,那就是你要使用那些类似 DIRECTORY_PICTURES ?的API的常量。那些目录类型参数可以确保那些文件被系统正确的对待。例如,那些以 DIRECTORY_RINGTONES ?类型保存的文件就会被系统的media scanner认为是ringtone而不是音乐。
查询剩余空间
如果你事先知道你想要保存的文件大小,你可以通过执行getFreeSpace() or getTotalSpace() 来判断是否有足够的空间来保存文件,从而避免发生IOException。
那些方法提供了当前可用的空间还有存储系统的总容量。然而,系统并不能保证你可以写入通过 getFreeSpace() 查询到的容量文件, 如果查询的剩余容量比你的文件大小多几MB,或者说文件系统使用率还不足90%,这样则可以继续进行写的操作,否则你最好不要写进去。
Note:你并没有被强制要求在写文件之前一定有要去检查剩余容量。你可以尝试先做写的动作,然后通过捕获IOException 。这种做法仅适合于你事先并不知道你想要写的文件的确切大小。例如,如果在把PNG图片转换成JPEG之前,你并不知道最终生成的图片大小是多少。
删除文件
你应该在不需要使用某些文件的时候,删除它。删除文件最直接的方法是直接执行文件的 delete() 方法。
myFile.delete();
如果文件是保存在internal storage,你可以通过 Context 来访问并通过执行 deleteFile() 进行删除
myContext.deleteFile(fileName);
Note: 当用户卸载你的app时,android系统会删除以下文件:
然而,通常来说,你应该手动删除所有通过 getCacheDir() 方式创建的缓存文件,以及那些不会再用到的文件。
数据库
就像保存文件到设备的internal storage 一样,Android会保存db到你的程序的private的空间上。你的数据是受保护的,因为那些区域默认是私有的,不可被其他程序所访问。
Note:因为那些操作可能是很耗时的,请确保你在background thread(AsyncTask or IntentService)里面去执行 getWritableDatabase() 或者 getReadableDatabase() 。
和查询信息一样,删除数据同样需要提供一些删除标准。DB的API提供了一个防止SQL注入的机制来创建查询与删除标准。
SQL Injection:(随着B/S模式应用开发的发展,使用这种模式编写应用程序的程序员也越来越多。但是由于程序员的水平及经验也参差不齐,相当大一部分程序员在编写代码的时候,没有对用户输入数据的合法性进行判断,使应用程序存在安全隐患。用户可以提交一段数据库查询代码,根据程序返回的结果,获得某些他想得知的数据,这就是所谓的SQL Injection,即SQL注入)
这个机制把查询语句划分为选项条款与选项参数两部分。条款部分定义了查询的列的特征,参数部分用来测试是否符合前面的条款。因为处理的结果与通常的SQL语句不同,这样可以避免SQL注入问题。
Android图像和动画
高效显示Bitmap
磁盘缓存可以用来保存那些已经处理好的位图,并且在那些图片在内存缓存中不可用时减少加载的次数。当然从磁盘读取图片会比从内存要慢,而且读取操作需要在后台线程中处理,因为磁盘读取操作是不可预期的。
Note:如果图片被更频繁的访问到,也许使用 ContentProvider 会更加的合适,比如在Gallery程序中。
内存缓存的检查是可以在UI线程中进行的,磁盘缓存的检查需要在后台线程中处理。磁盘操作永远都不应该在UI线程中发生。当图片处理完成后,最后的位图需要添加到内存缓存与磁盘缓存中,方便之后的使用。
处理配置改变(Handle Configuration Changes):详情请看P162
为了测试上面的效果,尝试在保留Fragment与没有这样做的情况下旋转屏幕。你会发现当你保留缓存时,从内存缓存中重新绘制几乎没有延迟的现象. 内存缓存中没有的图片可能在存在磁盘缓存中.如果两个缓存中都没有,则图像会像平时一样被处理。
Android管理bitmap memory的演变进程
在Android 3.0及以上版本管理内存:详情请看P165
在Android 3.0 (API Level 11) 引进了BitmapFactory.Options.inBitmap. 如果这个值被设置了,decode方法会在加载内容的时候去重用已经存在的bitmap. 这意味着bitmap的内存是被重新利用的,这样可以提升性能, 并且减少了内存的分配与回收。然而,使用inBitmap有一些限制。特别是在Android 4.4 (API level 19)之前,只支持同等大小的位图。详情请查看inBitmap文档.
后面的不是很懂,日后再看
添加动画
滥用动画或者在错误时机使用动画也是有害的,例如:他们造成了延迟。这节课程告诉你如何应用常用动画类型来提升易用性,在不给用户用户增加烦恼的前提下提升性能。
俩个view间渐变
渐变动画(也叫消失)通常指渐渐的淡出某个 UI 组件,同时同步地淡入另一个。在你 App 想切换内容或 view的情况下,这种动画很有用。渐变简短不易察觉,它也能提供从一个界面到下一个之间流畅的转换。当你不使用它们,不管怎么样转换经常感到生硬而仓促。
将 config_shortAnimTime 系统属性暂存到一个成员变量里。这个属性为动画定义了一个标准的“短”持续时间。对于微妙或者快速发生的动画,这是个很理想的时间段。config_longAnimTime 和 config_mediumAnimTime 也行,如果你想用的话。
// 获取并缓存系统默认的“短”时长
mShortAnimationDuration = getResources().getInteger(
android.R.integer.config_shortAnimTime);
// 设置内容View为0%的不透明度,但是状态为“可见”,
// 因此在动画过程中是一直可见的(但是为全透明)。
mContentView.setAlpha(0f);
mContentView.setVisibility(View.VISIBLE);
// 开始动画内容View到100%的不透明度,然后清除所有设置在View上的动画监听器。
mContentView.animate()
.alpha(1f)
.setDuration(mShortAnimationDuration)
.setListener(null);
// 加载View开始动画逐渐变为0%的不透明度,
// 动画结束后,设置可见性为GONE(消失)作为一个优化步骤
//(它将不再参与布局的传递等过程)
mLoadingView.animate()
.alpha(0f)
.setDuration(mShortAnimationDuration)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mLoadingView.setVisibility(View.GONE);
}
});
viewpager
@Override
public void onBackPressed() {
if (mPager.getCurrentItem() == 0) {
// 如果用户当前正在看第一步(也就是第一页),那就要让系统来处理返回按钮。
//这个是结束(finish())当前活动并弹出回退栈。
super.onBackPressed();
} else {
// 否则,返回前一页
mPager.setCurrentItem(mPager.getCurrentItem() - 1);
}
}
viewpager滑动动画用PageTransformer自定义动画
Android网络连接和云服务
管理网络
在执行网络操作之前检查设备当前连接的网络连接信息是个好习惯。这样可以防止你的程序在无意间连接使用了非意向的网络频道。如果网络连接不可用,你的应用应该优雅的做出响应。为了检测网络连接,我们需要使用到下面两个类:
public boolean isOnline() {
ConnectivityManager connMgr = (ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
return (networkInfo != null && networkInfo.isConnected());
}
private static final String DEBUG_TAG = "NetworkStatusExample";
...
ConnectivityManager connMgr = (ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
boolean isWifiConn = networkInfo.isConnected();
networkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
boolean isMobileConn = networkInfo.isConnected();
Log.d(DEBUG_TAG, "Wifi connected: " + isWifiConn);
Log.d(DEBUG_TAG, "Mobile connected: " + isMobileConn);