Android Training Note
版本适配
Tip:为了能在几个Android版本中都能提供最好的特性和功能,你应该在你的app中使用Android Support Library,它能使你的app能在旧平台上使用最近的几个平台的APIs。
- 指定最小和目标API级别,具体来说,<uses-sdk> 元素中的 minSdkVersion和 targetSdkVersion属性,标明在设计和测试app时,最低兼容API的级别和最高适用的API级别(这个最高的级别是需要通过你的测试的)。随着新版本Android的发布,一些风格和行为可能会改变,为了能使你的app能利用这些变化,而且能适配不同风格的用户的设备,你应该设置 targetSdkVersion 的值去匹配最新的可用Android版本。
- 在运行时检查系统版本。
@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; }
- compiledSdkVersion 是你的应用将要编译的目标Android版本,此处默认为你的SDK已安装的最新Android版本(目前应该是4.1或更高版本,如果你没有安装一个可用Android版本,就要先用SDKManager来完成安装),你仍然可以使用较老的版本编译项目,但把该值设为最新版本,使你可以使用Android的最新特性,同时可以在最新的设备上优化应用来提高用户体验。
- targetSdkVersion 表示你测试过你的应用支持的最高Android版本(同样用API level)表示).当Android发布最新版本后,你应该在最新版本的Android测试你的应用同时更新target sdk到Android最新版本,以便充分利用Android新版本的特性。
布局
- 可选的布局文件:在XML中定义界面布局而不是在运行时去动态生成布局是有多个原因的,其中最重要的一个原因是这样可以使得你为不同大小的屏幕创建不同的布局文件。例如,你可以创建创建2个版本的布局文件,告诉系统在小的屏幕上使用其中一个布局文件,在大的屏幕上使用另外一个布局文件。
- 于字符串 @string/edit_message,该字符串资源与id使用了相同的名称(edit_message)。然而,对于资源的引用是区分类型的(比如id和字符串),因此,使用相同的名称不会引起冲突
- 当你在用户界面定义一个文本的时候,你应该把每一个文本字符串列入资源文件。对于所有字符串值,字符串资源能够单独的修改,在资源文件里你可以很容易的找到并且做出相应的修改。通过选择定义每个字符串,还允许您对不同语言本地化应用程序。
- android:layout_weight,“两份伏特加酒,一份咖啡利口酒”,意思就是这个酒中伏特加酒占三分之二。(请注意,使用权重的前提一般是给View的宽或者高的大小设置为0dp,然后系统根据上面的权重规则来计算View应该占据的空间。但是很多情况下,如果给View设置了match_parent的属性,那么上面计算权重时则不是通常的正比,而是反比,也就是权重值大的反而占据空间小)
使用字符资源:
- 代码中:String hello = getResources().getString(R.string.hello_world);
- 布局中:android:text=”@string/hello_world”
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);
- 当使用你的app的时候,不会因为有来电通话或者切换到其他app而导致程序crash。
- 当用户没有激活某个组件的时候不要消耗宝贵的系统资源。
- 当离开你的app并且一段时间后返回,不要丢失用户的使用进度。
- 当设备发送屏幕旋转的时候,不会crash或者丢失用户的使用进度。
- onCreate里面尽量少做事情,避免程序启动太久都看不到界面
- 通常,你不应该使用onPause()来保存用户改变的数据 (例如填入表格中的个人信息) 到永久存储(File或者DB你确认用户期待那些改变能够被自动保存的时候(例如正在撰写邮件草稿),你可以把那些数据存到永久存储 。应该避免在onPause()时执行CPU-intensive 的工作,例如写数据到DB,因为它会导致切换到下一个activity变应该把那些heavy-load的工作放到onStop()去做)。如果你的activity实际上是要被Stop,那么你应该为了切换的顺畅而减少在OnPause()方法里面的工作量。
- 请注意,系统每次调用这个方法(onResume)时,activity都处于最前台,包括第一次创建的时候。所以,你应该实现onResume()来初始化那些你在onPause方法里面释放掉的组件,并执行那些activity每次进入Resumed state都需要的初始化动作 (例如开始动画与初始化那些只有在获取用户焦点时才需要的组件)
- 因为系统在activity停止时会在内存中保存了Activity实例。有些时候你不需要实现onStop(),onRestart()甚至是onStart()方法.因为大多数的activity相对比较简单,activity会自己停止与重启,你只需要使用onPause()来停止正在运行的动作并断开系统资源链接。
- 通常来说,跳转到其他的activity或者是点击Home都会导致当前的activity执行onSaveInstanceState,因为这种情况下的activity都是有可能会被destory并且是需要保存状态以便后续恢复使用的,而从跳转的activity点击back回到前一个activity,那么跳转前的activity是执行退栈的操作,所以这种情况下是不会执行onSaveInstanceState的,因为这个activity不可能存在需要重建的操作。
- 当系统开始停止你的Activity时,只有在Activity实例会需要重新创建的情况下才会调用到onSaveInstanceState()(1) ,在这个方法里面可以指定额外的状态数据到Bunde中。如果这个Activity被destroyed然后这个实例又需要被重新创建时,系统会传递在 (1) 中的状态数据到 onCreate()与 onRestoreInstanceState().
- onRestoreInstanceState()方法会在 onStart() 方法之后执行. 系统仅仅会在存在需要恢复的状态信息时才会调用 onRestoreInstanceState() ,因此你不需要检查 Bundle 是否为null。
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); }
PackageManager packageManager = getPackageManager(); List<ResolveInfo> activities = packageManager.queryIntentActivities(intent, 0); boolean isIntentSafe = activities.size() > 0;
// 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); }
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,并且指定了标题。
<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 ... } }
- 你可以把fragment想象成activity中一个模块化的部分,它拥有自己的生命周期,接收自己的输入事件,可以在acvitity运行过程中添加或者移除(有点像”子activity”,你可以在不同的activities里面重复使用)可以使用包含action bar的 v7 appcompat library。v7 appcompat library 兼容Android2.1(API level 7),也包含了Fragment APIs。
- 和activity其中一个区别是当你创建Fragment的时候,你必须重写onCreateView()回调方法来定义你的布局。事实上,这是使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" />
- 当你用XML布局文件的方式将Fragment添加进activity时,你的Fragment是不能被动态移除的
- 运用fragment(特别是那些你在运行时添加的)的一个很重要的规则就是在布局中你必须有一个容器view,fragment的layout将会放在这个view里面。
- 用addToBackStack(),当你移除或者替换一个fragment并把它放入返回栈中时,被移除的fragment的生命周期是stopped(不是destoryed).当用户返回重新恢复这个fragment,它的生命周期是restarts。如果你没把fragment放入返回栈中,那么当他被移除或者替换时,它的生命周期是destoryed。
- addToBackStack()方法提供了一个可选的String参数为事务指定了一个唯一的名字。这个名字不是必须的,除非你打算用FragmentManager.BackStackEntry APIs来进行一些高级的fragments操作。
- 经常地,你想fragment之间能相互交互,比如基于用户事件改变fragment的内容。所有fragment之间的交互需要通过他们关联的activity,两个fragment之间不应该直接交互。
Fragments之间的交互
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"); } } ... }
@Override public void onListItemClick(ListView l, View v, int position, long id) { mCallback.onArticleSelected(position); }
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 } }
- 总是可用的
- 这里的文件默认是只能被你的app所访问的。
- 当用户卸载你的app的时候,系统会把internal里面的相关文件都清除干净。
- Internal是在你想确保不被用户与其他app所访问的最佳存储区域。
- 并不总是可用的,因为用户有时会通过USB存储模式挂载外部存储器,当取下挂载的这部分后,就无法对其进行访问了。
- 是大家都可以访问的,因此你可能会失去保存在这里的文件的访问控制权。
- 当用户卸载你的app时,系统仅仅会删除external根目录(getExternalFilesDir())下的相关文件。
- External是在你不需要严格的访问权限并且你希望这些文件能够被其他app所共享或者是允许用户通过电脑访问时的最佳存储区域。
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; }
- Public files :这些文件对与用户与其他app来说是public的,当用户卸载你的app时,这些文件应该保留。例如,那些被你的app拍摄的图片或者下载的文件。
- Private files: 这些文件应该是被你的app所拥有的,它们应该在你的app被卸载时删除掉。尽管由于存储在external storage,那些文件从技术上而言可以被用户与其他app所访问,但实际上那些文件对于其他app是没有意义的。因此,当用户卸载你的app时,系统会删除你的app的private目录。例如,那些被你的app下载的缓存文件。
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; }
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; }
- 所有保存到internal storage的文件。
- 所有使用getExternalFilesDir()方式保存在external storage的文件。
- 移动设备的系统资源有限。Android设备对于单个程序至少需要16MB的内存。程序应该在这个最低内存限制下最优化程序的效率。当然,大多数设备的都有更高的限制需求。
- 在过去, 一个比较流行的内存缓存的实现方法是使用软引用(SoftReference)或弱引用(WeakReference)bitmap缓存, 然而这是不推荐的。从Android 2.3 (API Level 9) 开始,GC变得更加频繁的去释放soft/weak references,这使得他们就显得效率低下。而且在Android 3.0 (API Level 11)之前,备份的bitmap是存放在native memory 中,它不是以可预知的方式被释放,这样可能导致程序超出它的内存限制而崩溃。
- 在上面的例子中, 有1/8的程序内存被作为Cache. 在一个常见的设备上(hdpi),最小大概有4MB (32/8). 如果一个填满图片的GridView组件放置在800×480像素的手机屏幕上,大概会花费1.5MB (800x480x4 bytes), 因此缓存的容量大概可以缓存2.5页的图片内容.、
- 在Android 2.2 (API level 8)以及之前, 当GC发生时, 你的应用的线程是会stopped的. 这导致了一个滞后,它会降低效率. 在Android 2.3上,添加了并发GC的机制, 这意味着在一个bitmap不再被引用到之后,内存会被立即回收.
- 在Android 2.3.3 (API level 10)以及之前, 一个bitmap的像素级数据是存放在native内存中的. 这些数据与bitmap本身是隔离的, bitmap本身是被存放在Dalvik heap中。在native内存中的pixel数据的释放是不可预测的,这意味着有可能导致一个程序容易超过它的内存限制并Crash。 自Android 3.0 (API Level 11)起, pixel数据则是与bitmap本身一起存放在dalvik heap中。
// 获取并缓存系统默认的“短”时长 mShortAnimationDuration = getResources().getInteger( android.R.integer.config_shortAnimTime);
- 对于淡入的 view,设置 alpha 值为 0 并且设置 visibility 为 VISIBLE(要记得他起初被设置成了 GONE)。这让iew 可见了但是它是透明的。
- 对于淡入的 view,把 alpha 值从 0 动态改变到 1。同时,对于淡出的 view,把 alpha 值从 1 动态变到 0。
- 使用 Animator.AnimatorListener 中的 onAnimationEnd(),设置淡出 view 的 visibility 为 GONE。即使 alpha 值为0,也要把 view 的 visibility 设置成 GONE 来防止 view 占据布局空间,还能把它从布局计算中忽略,加速处理过程。
// 设置内容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); } });
@Override public void onBackPressed() { if (mPager.getCurrentItem() == 0) { // 如果用户当前正在看第一步(也就是第一页),那就要让系统来处理返回按钮。 //这个是结束(finish())当前活动并弹出回退栈。 super.onBackPressed(); } else { // 否则,返回前一页 mPager.setCurrentItem(mPager.getCurrentItem() - 1); } }
- ConnectivityManager: 它会回答关于网络连接状态的查询,并在网络连接改变时通知应用程序。
- NetworkInfo: 描述一个给定网络类型(就本节而言是移动网络或Wi-Fi)的网络接口的状态。
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);