一、Android開發初體驗
監聽器使用匿名內部類的好處:1,因為匿名內部類的使用,我們可在同一處實現監聽器方法,代碼更清晰可讀。2,事件監聽器一般只在同一處使用,使用匿名內部類可避免不必要的命名類實現。
二、Android與MVC設計模式
模型對象存儲著應用的數據和業務邏輯。模型類通常用來映射與應用相關的一些事物,如用戶、商店里的商品、服務器上的圖片或者一些段電視節目,抑或GeoQuiz應用里的地理知識問題。模型對象不關心用戶界面,它存在的唯一上的就是存儲和管理應用數據。
視圖對象知道如何在屏幕上繪制自己,以及如何響應用戶的輸入,如觸摸動作等。一個簡單經驗法則是,凡是能夠在屏幕上看見的對象,就是視圖對象。比如xml文件。
控制器對象含有應用的邏輯單元,是視圖與模型對象的聯系紐帶。控制器對象響應視圖對象觸發的種類事件,此外還管理著模型對象與視圖間的數據流動。一般是Activity、Fragment或Service的一個子類。
int question = mQuestonBank[mCurrentIndex].getTextResId();
mQuestionTextView.setText(question); //setText(@StringRes int resid)的另一個重構方法
公共代碼抽取方法:refactor->extract->method。
Button: android:drawableRight="@drawable...
TextView點擊事件
ImageButton:android:src
三、Activity的生命周期
onCreate(內存),onStart(可視),onResume(前臺),onPause(可視),onStop(內存),onDestroy,onRestart
Log快捷鍵:logt、logd、logm
LogCat過濾設置:選擇Edit Filter Configuration選項,單擊+按鈕創建消息過濾器,在Filter Name處輸入QuizActivity,Log Tag處同樣輸入QuizActivity,單擊OK。現在,LogCat窗口僅顯示Tag為QuizActivity的日志信息。
設備處于水平方向時,Android會找到并使用res/layout-land目錄下的布局資源。
FrameLayout里面控件使用android:layout_gravity屬性。
設備旋轉前保存數據:onSaveInstanceState / if(savedInstanceState != null)...
四、Android應用的調試
診斷應用異常(配合邏輯診斷)
記錄棧跟蹤日志(查看方法在哪被調用):Log.d(TAG,"Updating question text ",new Exception());
設置斷點(Debug而不是Run),運行后可檢查對象,變量的值,單擊this可看到超類的值。
使用異常斷點(調試時會定位到異常拋出的代碼行):Run --> View Breakpoints,單擊+,選擇Java Exception Breakpoints,輸入RuntimeException并選擇。異常斷點影響大建議及時清除不需要的斷點。
使用Android Lint(會檢查項目中所有潛在問題):Analyze --> Inspect Code...,選擇Whole project。
R類的問題(資源編譯錯誤):重新檢查資源文件中XML文件的有效性、清理項目、使用Gradle同步項目、運行Android Lint
布局檢查器(查看布局樹):androd monitor --> hierarchy View
內存分配跟蹤:點擊按鈕啟動后,在前臺操作應用時,后臺就開始記錄內存分配狀況,尋找可優化的點。
五、第二個Activity
tools和tools:text命名空間可以覆蓋某個組件的任何屬性。
activity調用startActivity()方法時,調用請求發送給了操作系統的ActivityManager。ActivityManager負責創建Activity實例并調用其onCreate()方法。
//CheatActivity
private static final String EXTRA_ANSWER_IS_TRUE = "com.bignerdranch.android.geoquiz.answer_is_true";
public static Intent newIntent(Context packageContext,boolean answerIsTrue) {
Intent intent = new Intent(packageContext,CheatActivity.class);
intent.putExtra(EXTRA_ANSWER_IS_TRUE,answerIsTrue);
return intent;
}
//QuizActivity
Intent intent = CheatActivity.newIntent(QuizActivity.this,answerIsTrue);
startActivity(intent);
//CheatActivity
mAnswerIsTrue = getIntent().getBooleanExtra(EXTRA_ANSWER_IS_TRUE,false);
啟動應用時,實際上是啟動了應用的lanunch activity。
ActivityManager維護著一個非特定應用獨享的回退棧。所有應用的activity都共享該回退棧。這也是將ActivityManager設計成操作系統級的activity管理器來負責啟動應用activity的原因之一。顯然,回退棧是作為一個整體共享于操作系統及設備,而不單單用于某個應用。
六、Android SDK版本與兼容
使用Android Lint,在老版本的系統上調用新版本代碼時,潛在問題在編譯時就能被發現。也就是說,如果使用了高版本系統API中的代碼,Android Lint會提示編譯錯誤。
根據系統版本編譯:Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLTPOP
Android文檔在SDK安裝目錄中的docs目錄中。
七、UI fragment與fragment管理器
activity托管fragment:activity在其視圖層級里提供一處位置,用來放置fragment視圖。fragment本身沒有在屏幕上顯示視圖的能力。
生命周期:(setContentView方法中調用)onAttach,inCreate,onCreateView,(創建)onActivityCreate,(停止)onStart,(暫停)onResume,(運行)onPause,(暫停)onStop,(停止)onDestroyView,(activity關閉)onDestroy,onDetach,銷毀。
activity托管UI fragment有兩種方式:在activity布局中添加fragment;在activity代碼中添加fragment。一般使用第二種方式。
onCreateView(){ View v = inflater.inflate(R.layout.fragment_crime,container,false); return v;} //CrimeFragment
EditText.addTextChangedListener
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.fragment_container); //看framelayout里面是否已經被添加過fragment。
if(fragment == null) {
fragment = new CrimeFragment();
fm.beginTransaction().add(R.id.fragment_container,fragment).commit();
在activity處于運行狀態時,添加fragment時,FragmentManager立即驅趕fragment,調用從onAttach到onResume方法,追上activity步伐(與activity的最新狀態保持同步)后,托管activity的FragmentManager就會邊接收操作系統的調用指令,邊調用其他生命周期方法,讓fragment與activity的狀態取得一致。
要合理使用fragment。應用單屏最多使用2~3個fragment。在開發中盡量使用fragment,因為后期添加太麻煩。
要使用支持庫版fragment,應用的activity必須繼承FragmentActivity。AppCompatActivity是FragmentActivity子類,FragmentActivity又是Activity的子類。
八、使用RecyclerView顯示列表
單例是特殊的Java類,在創建實例時,一個單例類僅允許創建一個實例。應用能在內存里存活多久,單例就能存活多久。
要創建單例,需創建一個帶有私有構造方法及get()方法的類。如果實例已存在,get()方法就直接返回它;如果實例還不存在,get()方法就會調用構造方法創建它。
使用抽象activity托管fragment,把通用的建立一個fragment的activity設置成超類,寫一個createFragment的抽象方法。
使用RecyclerView:ViewHolder只做一件事,容納View視圖。RecyclerView自身不會創建視圖,它創建的是ViewHolder,而ViewHolder引用著itemView。
Adapter是一個控制器對象,從模型層獲取數據,然后提供給RecyclerView顯示,是溝通的橋梁。它負責創建必要的ViewHolder和綁定ViewHolder至模型層數據。RecyclerView需要顯示視圖對象時,就會去找它的Adapter。
首先調用Adapter的getItemCount方法,詢問數組列表中包含多少個對象;接著RecyclerView調用Adapter的onCreateViewHolder方法創建ViewHolder及其要顯示的視圖;最后,Recycler會傳入ViewHolder及其位置,調用onBindViewHolder方法,Adapter會找到目標位置的數據并將其綁定到ViewHolder的視圖上。所謂綁定,就是使用模型數據 填充視圖。
需要注意的是,相對于onBindViewHoler方法,onCreateViewHolder方法的調用并不頻繁。一旦有了夠用的ViewHolder,RecyclerView就會停止調用onCreateView方法,隨后,它會回收利用舊的ViewHoler以節約時間和內存。
Recycler類不會新版擺放屏幕上的列表項,實際上,擺放的任務被委托給了LayoutManager。LayoutManager還負責定義屏幕滾動行為。
mCrimeRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
列表滾動流暢歸功于onBindViewHolder方法,任何時候,都要確保這個方法輕巧、高效。
private class CrimeHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
private Crime mCrime;
private TextView mTitleTextView;
private TextView mDateTextView;
public CrimeHolder(LayoutInflater inflater,ViewGroup parent) {
super(inflater.inflate(R.layout.list_item_crime,parent,false));
itemView.setOnClickListener(this);
mTitleTextView = (TextView) itemView.findViewById(R.id.crime_title);
mDateTextView = (TextView) itemView.findViewById(R.id.crime_date);
}
public void bind(Crime crime) {
mCrime = crime;
mTitleTextView.setText(mCrime.getTitle());
mDateTextView.setText(mCrime.getDate().toString());
}
@Override
public void onClick(View v) {
Toast.makeText(getActivity(),mCrime.getTitle() + " clicked!",Toast.LENGTH_SHORT).show();
}
}
private class CrimeAdapter extends RecyclerView.Adapter<CrimeHolder> {
private List<Crime> mCrimes;
public CrimeAdapter(List<Crime> crimes) {
mCrimes = crimes;
}
@Override
public CrimeHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
return new CrimeHolder(layoutInflater,parent);
}
@Override
public void onBindViewHolder(CrimeHolder holder, int position) {
Crime crime = mCrimes.get(position);
holder.bind(crime);
}
@Override
public int getItemCount() {
return mCrimes.size();
}
}
RecyclerView可代替ListView和GridView。
單例能方便地存儲和控制模型對象。但是無法做到持久存儲,也不利于單元測試(用依賴注入工具解決)。
RecyclerView ViewType:可在RecyclerView中創建不同類的列表項。
定義三種item的xml視圖,對應的需要定義三種不同的ViewHolder。 然后根據不同的需求,在Adapter里面識別并運用這些ViewHolder,Adapter里面已經定義好了一些方法,只需要重寫getItemViewType(int position)方法,給每個固定的position上的item返回一個固定的類型(ViewType)就能方便的表明每個item需要的ViewHolder:
class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
//User是一個自定義類,代表封裝的數據類型
private List<User> mUsers;
//三種不同的ViewType類型,事先用常量定義好
public static final int VIEW_TYPE_ONE = 1;
public static final int VIEW_TYPE_TWO = 2;
public static final int VIEW_TYPE_THREE = 3;
public MyAdapter(List<User> users) {
mUsers = users;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
RecyclerView.ViewHolder myViewHolder = null;
//根據不同的ViewType類型,來返回不同的ViewHolder
switch (viewType) {
case VIEW_TYPE_ONE:
myViewHolder = new ViewHolderOne
(LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_item_one, parent, false));
break;
case VIEW_TYPE_TWO:
myViewHolder = new ViewHolderTwo
(LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_item_two, parent, false));
break;
case VIEW_TYPE_THREE:
myViewHolder = new ViewHolderThree
(LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_item_three, parent, false));
break;
}
return myViewHolder;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
//根據不同的ViewType類型,進行不同的數據綁定操作
switch (holder.getItemViewType()) {
case VIEW_TYPE_ONE:
ViewHolderOne holderOne = (ViewHolderOne) holder;
holderOne.mUserAvatar.setImageResource(R.mipmap.ic_launcher);
holderOne.mUserName.setText(mUsers.get(position).getUserName());
holderOne.mUserInfo.setText(mUsers.get(position).getInfo());
break;
case VIEW_TYPE_TWO:
ViewHolderTwo holderTwo = (ViewHolderTwo) holder;
holderTwo.mUserAvatar.setImageResource(R.mipmap.user_avatar);
holderTwo.mUserName.setText(mUsers.get(position).getUserName());
holderTwo.mUserInfo.setText(mUsers.get(position).getInfo());
break;
case VIEW_TYPE_THREE:
ViewHolderThree holderThree = (ViewHolderThree) holder;
holderThree.mUserAvatar.setImageResource(R.mipmap.ic_launcher);
holderThree.mUserName.setText(mUsers.get(position).getUserName());
holderThree.mUserInfo.setText(mUsers.get(position).getInfo());
break;
}
}
@Override
public int getItemCount() {
if (null != mUsers) return mUsers.size();
else return 0;
}
//重寫方法,給每個position上的item返回一個固定的ViewType類型
//如果返回的ViewType類型不固定,則會出現各種item布局變化的情況,可能還會觸發bug
@Override
public int getItemViewType(int position) {
return mUsers.get(position).getType();
}
}
九、使用布局與組件創建用戶界面
ConstraintLayout:添加四個方向上的約束可擺放組件位置,組件大小有三個選擇(組件自己決定wrap_content、手動調整、充滿約束布局)。
選中組件,在組件的屬性視圖容器設置大小,有三中:固定大小|--|、包裹內容>>>、動態適應。先把兩個組件全設為wrap_content,然后拖ImageView組件到crime_date下面。在預覽界面,拖住ImageView組件頂部的約束柄,將其拖向ConstraintLayout組件頂部,直到約束柄變綠,并彈出Release to Create Top Constraint這樣的提示時,再松開鼠標,然后依次設置下部、右部約束。在xml中形式為:
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
對兩個TextView設置約束和邊距,寬度為動態適應(0dp),高度為wrap_content
樣式(style)是XML資源文件,含有用來描述組件行為和外觀屬性定義。主題是各種樣式的集合,從結構上說,主題本身也是一種樣式資源,只不過它的屬性指向了其他樣式資源。使用主題引用,可將預定義的應用主題樣式添加給指定組件,并確定組件在應用中擁有正確一致的顯示風格。
邊距屬性的默認使用值是16dp或8dp。
十、使用fragment argument
直接獲取extra信息的缺點:破壞了fragment的封裝,不再是可復用的構建單元。更好的做法是使用fragment argument bundle。
每個fragment實例都可附帶一個Bundle對象。該bundle包含鍵-值對,可以像附加extra到Activity的intent中那樣使用它們。一個鍵-值對即一個argument。
創建fragment argument:先創建Bundle對象,然后使用put方法,將argument添加到bundle中。然后調用Fragment.setArguments方法把argument bundle附加給fragment:
```
public static CrimeFragment newInstance(UUID crimeId) { //Fragment
Bundle args = new Bundle();
args.putSerializable(ARG_CRIME_ID,crimeId);
CrimeFragment fragment = new CrimeFragment();
fragment.setArguments(args);
return fragment;
}
protected Fragment createFragment() { //Activity
UUID crimeId = (UUID) getIntent().getSerializableExtra(EXTRA_CRIME_ID);
return CrimeFragment.newInstance(crimeId);
}
列表數據變化: mAdapter.notifyDataSetChanged();
高效刷新:mAdapter.notifyItemChange(int);
十一、使用ViewPager
ViewPager在某種程度上類似于RecyclerView,不過,相較于RecyclerView與Adapter間的協同工作,ViewPager與PagerAdapter間的配合要復雜得多,所以,一般使用Google提供的PagerAdapter的子類FragmentStatePagerAdapter:
mViewPager.setAdapter(new FragmentStatePagerAdapter(fragmentManager) {
@Override
public Fragment getItem(int position) {
Crime crime = mCrimes.get(position);
return CrimeFragment.newInstance(crime.getId());
}
@Override
public int getCount() {
return mCrimes.size();
}
});
for (int i = 0;i < mCrimes.size();i++) {
if (mCrimes.get(i).getId().equals(crimeId)) {
mViewPager.setCurrentItem(i);
break;
}
}
FragmentStatePagerAdapter在卸載不需要的fragment時會銷毀它。FragmentPagerAdapter會調用事務的detach而不是remove,只是銷毀了fragment的視圖,而實例還保存在FragmentManager中。前者更省內存,后者適合用戶需要少量固定的fragment的情形。
當需要ViewPager托管非fragment視圖時,需要自己實現PagerAdapter接口,
不推薦使用代碼方式創建視圖,不過如果只需一個視圖時,可用代碼創建。
十二、對話框
建議將AlertDialog封裝在DialogFragment實例中使用,使用FragmentManager管理對話框,可以更靈活地顯示對話框,旋轉設備后對話框也不會消失:
public class DatePickerFragment extends DialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(getActivity())
.setTitle(R.string.date_picker_title)
.setPositiveButton(android.R.string.ok,null)
.create();
}
}
FragmentManager manager = getFragmentManager();
DatePickerFragment dialog = new DatePickerFragment();
dialog.show(manager,DIALOG_DATE);
同一個activity托管的fragment間的數據傳遞:
public static DatePickerFragment newInstance(Date date) {
Bundle args = new Bundle();
args.putSerializable(ARG_DATE,date);
DatePickerFragment fragment = new DatePickerFragment();
fragment.setArguments(args);
return fragment;
}
DatePickerFragment dialog = DatePickerFragment.newInstance(mCrime.getDate());
dialog.show(manager,DIALOG_DATE);
dialog.setTargetFragment(CrimeFragment.this,REQUEST_DATE);
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
return;
}
if (requestCode == REQUEST_DATE) {
Date date = (Date) data.getSerializableExtra(DatePickerFragment.EXTRA_DATE);
mCrime.setDate(date);
mDateButton.setText(mCrime.getDate().toString());
}
}
private void sendResult(int resultCode,Date date) {
if (getTargetFragment() == null) {
return;
}
Intent intent = new Intent();
intent.putExtra(EXTRA_DATE,date);
getTargetFragment()
.onActivityResult(getTargetRequestCode(),resultCode,intent);
}
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int year = mDatePicker.getYear();
int month = mDatePicker.getMonth();
int day = mDatePicker.getDayOfMonth();
Date date = new GregorianCalendar(year,month,day).getTime();
sendResult(Activity.RESULT_OK,date);
}
})
編寫同樣的代碼用于全屏fragment或對話框fragment時,可選擇覆蓋DialogFragment.onCreateView方法,而非oncreateDialog方法,以實現不同設備上的信息呈現。
十三、工具欄
使用AppCompat庫;
android:theme="@style/AppTheme"
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
res下new-->android resource file,選擇menu類型,命名為fragment_crime_list。
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/new_crime"
android:icon="@android:drawable/ic_menu_add"
android:title="@string/new_crime"
app:showAsAction="ifRoom|withText" />
</menu>
使用Android Studio內置的Android Asset Studio工具為工具欄創建或定制圖片:右鍵單擊drawable,選擇New-->Image Asset,生成各類圖標。
創建菜單:
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.fragment_crime_list,menu);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.new_crime:
Crime crime = new Crime();
CrimeLab.get(getActivity()).addCrime(crime);
Intent intent = CrimePagerActivity.newIntent(getActivity(),crime.getId());
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
實現層級導航:android:parentActivityName=".CrimeListActivity"
修改標題:
private void updateSubtitle() {
CrimeLab crimeLab = CrimeLab.get(getActivity());
int crimeCount = crimeLab.getCrimes().size();
String subtitle = getString(R.string.subtitle_format,crimeCount);
AppCompatActivity activity = (AppCompatActivity) getActivity();
activity.getSupportActionBar().setSubtitle(subtitle);
}
case R.id.show_subtitle:
updateSubtitle();
return true;
復數字條串資源:
<plurals name="subtitle_plural">
<item quantity="one">%1d crimes</item>
</plurals>
int crimeSize = crimeLab.getCrimes().size();
String subtitle = getResources().getQuantityString(R.plurals.subtitle_plural,crimeSize,crimeSize);
用于RecyclerView的空視圖:在布局中添加一個textview,默認不可見,當數據為零時顯示可見。
十四、SQLite數據庫
應用上下文在應用的全周期存在,而activity不一定存在,所以在單例中使用應用上下文:mContext = context.getApplicationContext();
十五、使用隱式intent
檢查可響應任務的activity:
PackageManager packageManager = getActivity().getPackageManager();
if (packageManager.resolveActivity(pickContact,
PackageManager.MATCH_DEFAULT_ONLY) == null) {
mSuspectButton.setEnabled(false);
}
過濾器驗證代碼:pickContact.addCategory(Intent.CATEGORY_HOME);
ShareCompat類有個內部類IntentBuilder,使用這個類創建發送消息的Intent略微方便一些。
十六、使用intent拍照
使用FileProvider:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.lewanjiang.criminalintent.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/files/"/>
</provider>
<paths>
<files-path
name="crime_photo"
path="."/>
</paths>
使用相機intent:
final Intent captureImage = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
boolean canTakePhoto = mPhotoFile != null &&
captureImage.resolveActivity(packageManager) != null;
mPhotoButton.setEnabled(canTakePhoto);
mPhotoButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Uri uri = FileProvider.getUriForFile(getActivity(),
"com.lewanjiang.criminalintent.fileprovider",mPhotoFile);
List<ResolveInfo> cameraActivities = getActivity()
.getPackageManager().queryIntentActivities(captureImage,
PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo activity:cameraActivities) {
getActivity().grantUriPermission(activity.activityInfo.packageName,
uri,Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
startActivityForResult(captureImage,REQUEST_PHOTO);
}
});
縮放的顯示位圖:
public class PictureUtils {
public static Bitmap getScaledBitmap(String path,int destWidth,int destHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path,options);
float srcWidth = options.outWidth;
float srcHeight = options.outHeight;
int inSampleSize = 1;
if (srcHeight > destHeight || srcWidth > destWidth) {
float heightScale = srcHeight / destHeight;
float widthScale = srcWidth / destWidth;
inSampleSize = Math.round(heightScale > widthScale ? heightScale : widthScale);
}
options = new BitmapFactory.Options();
options.inSampleSize = inSampleSize;
return BitmapFactory.decodeFile(path,options);
}
public static Bitmap getScaledBitmap(String path, Activity activity) {
Point size = new Point();
activity.getWindowManager().getDefaultDisplay().getSize();
return getScaledBitmap(path,size.x,size.y);
}
}
else if (requestCode == REQUEST_PHOTO) {
Uri uri = FileProvider.getUriForFile(getActivity(),
"com.lewanjiang.criminalintent.fileprovider",
mPhotoFile);
getActivity().revokeUriPermission(uri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
updatePhotoView();
}
private void updatePhotoView() {
if (mPhotoFile == null || !mPhotoFile.exists()) {
mPhotoView.setImageDrawable(null);
} else {
Bitmap bitmap = PictureUtils.getScaledBitmap(mPhotoFile.getPath(),getActivity());
mPhotoView.setImageBitmap(bitmap);
}
}
優化縮略圖加載:ViewTreeObserver
十七、雙版面主從用戶界面
@LayoutRes
protected int getLayoutResId() {
return R.layout.activity_fragment;
}
setContentView(getLayoutResId());
別名資源是一種指向其他資源的特殊資源,定義在refs.xml文件中。
十八、應用本地化
十九、Android輔助功能
TalkBack
二十、數據綁定與MVVM
為什么需要用MVVM架構:當應用越來越復雜,fragment和activity開始膨脹,逐漸變得難以理解和擴展。這個時候,控制器層就需要做功能拆分了。
怎么拆?先搞清楚控制器類到底做了哪些工作,再把這些工作拆分到獨立的小類里。讓一個個拆開的小類協同工作。
如何確定控制器類的不同使用呢?你的架構可以給你答案,使用MVC/MVP時它們就是這個答案。
每個視圖模型應控制成多在規模,這要具體情況具體分析。如果 視圖模型過大,你還可以繼續拆分。總之,你的架構你把控。
開啟數據綁定:
dataBinding {
enabled = true
}
然后把一般布局改造為數據綁定布局:把布局定義放入<layout>標簽。
實例化綁定類:
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
FragmentBeatBoxBinding binding = DataBindingUtil
.inflate(inflater,R.layout.fragment_beat_box,container,false);
binding.recyclerView.setLayoutManager(new GridLayoutManager(getActivity(),3));
return binding.getRoot();
}
創建SoundHolder:
private class SoundHolder extends RecyclerView.ViewHolder {
private ListItemSoundBinding mBinding;
private SoundHolder(ListItemSoundBinding binding) {
super(binding.getRoot());
mBinding = binding;
}
}
創建SoundAdapter:
private class SoundAdapter extends RecyclerView.Adapter<SoundHolder> {
@Override
public SoundHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(getActivity());
ListItemSoundBinding binding = DataBindingUtil
.inflate(inflater,R.layout.list_item_sound,parent,false);
return new SoundHolder(binding);
}
@Override
public void onBindViewHolder(SoundHolder holder, int position) {
}
@Override
public int getItemCount() {
return 0;
}
}
使用assets有兩面性:一方面,無需配置管理,可以隨意命名,并按自己的文件結構組織它們;另一方面,沒有配置管理,無法自動響應屏幕顯示密度、語言這樣的設置配置變更,自然也就無法在布局或其他資源里自動使用它們了。
為何使用assets:如果使用resources系統要一個個去處理,效率非常低,因為這些文件不能全放在一個目錄下管理,所以使用assets。assets可以看作是一個微型文件系統,支持任意層次的文件目錄結構,常用來加載大師圖片和違章資源。
二一、音頻播放與單元測試
MMVM架構極大方便了一項關鍵編程工作:單元測試。
利用SoundPool實現音頻播放:
先添加兩個測試依賴:Mockito,Hamcrest。把compile改為testCompile。
在類名上創建一個新測試類(光標移到類名上,按Command+Shift+T,選擇Create New Test...),測試庫選JUnit4,勾選setUp/@before,其他保持默認設置。選擇test目錄進行單元測試
實現測試類,使用虛擬依賴項:
public class SoundViewModelTest {
private BeatBox mBeatBox;
private Sound mSound;
private SoundViewModel mSubject;
@Before
public void setUp() throws Exception {
mBeatBox = mock(BeatBox.class);
mSound = new Sound("assetPath");
mSubject = new SoundViewModel(mBeatBox);
mSubject.setSound(mSound);
}
}
編寫測試方法:
@Test
public void exposesSoundNameAsTitle() {
assertThat(mSubject.getTitle(), is(mSound.getName()));
}
設備旋轉和對象保存:實現Serializable或者Parcelable接口。
保留fragment:setRetainInstance(true);。原理:當設備配置改變時,fragment視圖被銷毀,但fragment本身不會被銷毀。然而,這個功能不推薦使用。第一是用起來更復雜,第二是因系統回收內存而被銷毀時,就會數據丟失。
Esspresso與整合測試,Espresso是Google開發的一個UI測試框架。
二十二、樣式與主題
在resources是定義顏色資源,一處定義,整個應用中引用。colors.xml
樣式是能夠應用于視圖組件的一套屬性。styles.xml
<style name="BeatBoxButton">
<item name="android:background">@color/dark_blue</item>
</style>
style="@style/BeatBoxButton" //xml中Button屬性中
樣式支持繼承:
<style name="BeatBoxButton.Strong">
<item name="android:textStyle">bold</item>
</style>
也可以采用指定父樣式的方式。
主題可以看作是樣式的進貨加強版,會自動應用于整個應用,主題能引用外部和資源和其他樣式:
<style name="AppTheme" parent="Theme.AppCompat.Light">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/red</item>
<item name="colorPrimaryDark">@color/dark_red</item>
<item name="colorAccent">@color/gray</item>
</style>
覆蓋主題屬性:從現有主題往上找父主題,直到找到相關屬性為止
修改按鈕屬性:逐級向上查找父主題,找到buttonSytle樣式,可以在Widget.AppCompat.Button中看到button的各個屬性。修改BeatBosButton的父樣式為Widget.AppCompat.Button。
<style name="BeatBoxButton" parent="Widget.AppCompat.Button">
在主題中覆蓋buttonStyle屬性:
<item name="buttonStyle">@style/BeatBoxButton</item>
如果是繼承自己內部的主題,使用主題名指定父主題即可;如果繼承android操作系統中的樣式或主題,記得使用parent屬性
在主題中引用資源時,使用?符號。
二十三、XML drawable
在android中,凡是要在屏幕上繪制的東西都可以叫drawable,比如抽象圖形、Drawable類的子類代碼、位圖圖像等。
shape drawable可以把按鈕變成各種形狀:
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/dark_blue" />
</shape>
在styles中把上面的drawable作為按鈕背景:
<item name="android:background">@drawable/button_beat_box_normal</item>
state list drawable:狀態改變時變化。
先定義一個用于按鈕按下狀態的shape drawable:
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/red" />
</shape>
然后定義一個state list drawable:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/button_beat_box_pressed"
android:state_pressed="true"/>
<item android:drawable="@drawable/button_beat_box_normal" />
</selector>
最后將上面的state list drawable作為按鈕背景:
<item name="android:background">@drawable/button_beat_box</item>
除了按下狀態,state list drawable還支持禁用、聚集以及激活等狀態。
layer list drawable能讓兩個drawable合二為一。比如為按下狀態的按鈕添加一個深色的圓環:
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/red" />
</shape>
</item>
<item>
<shape android:shape="oval">
<stroke
android:width="4dp"
android:color="@color/dark_red"/>
</shape>
</item>
</layer-list>
drawable用起來靈活方便,不僅用法多樣,還易于更新維護,能做出復雜的背景圖,連圖像編輯器都省了,更改應用的配色也很簡單。
另外,drawable獨立于屏幕像素密度,不需要準備多個版本來適合不同屏幕。
把應用啟動器圖標放在mipmap目錄中,其他圖片都放在drawable目錄中。
二十四、深入學習intent和任務
Intent另一個構造方法:setClassName(String packageName,String className)
任務是一個activity棧,默認情況下,新activity都在當前任務中啟動。
啟動新activity時啟動新任務:addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
進程是操作系統創建的、供應用對象生存以及應用運行的地方。通常會占用操作系統管理著的系統資源,在Android中,每個進程都需要一個虛擬機來運行。
每一個activity實例都存在于一個進程之中,同一個任務關聯。這也是進程與任務的唯一相似之處。任務只包含activity,這些activity通常來自于不同的應用進程;而進程則包含了應用的全部運行代碼和對象
并發文檔(concurrent document),可以為運行的應用動態創建任意數目的任務:android:documentLaunchMode="intoExisting"
二十五、HTTP與后臺任務
線程是個單一招行序列。單個線程中的代碼會逐步招行。所有的Android應用的運行都是從主線程開始的。然后,主線程不是線程那樣的預訂招行序列,相反,它處于一個無限循環的運行狀態,等著用戶或系統觸發事件。一旦有事件觸發,主線程便招行代碼做出響應。
清理AsyncTask:AsyncTask.cancel(true)。如果fragment/activity已銷毀了,不撤銷AsyncTask,可能會引發內存泄漏。
第一個參數是輸入參數類型,第二個參數是指定發送進度更新需要的類型,第三個參數是輸出類型。
相比AsyncTask,推薦使用loader。遇到配置變化 時會自動管理loader和加載的數據 ,而且LoaderManager負責啟動和停止,以及管理生命周期。
分頁:RecyclerView.OnScrollListener
動態調整網格:ViewTreeObserver.OnGlobalLayoutListener
二十六、Looper、Handler和HandlerThread
RecyclerView設置GridLayoutManager時,意味著寬度會變,而高度固定(寬高分別為match_parent和120dp),為最大化利用ImageView的空間,設置scaleType的屬性值為centerCrop。
在應用圖片展示頁面,很多應用通常會選擇僅在需要顯示 圖片時都去下載。顯然,RecyclerView及其adapter應負責實現按需下載。adapter觸發圖片下載就放在onBindViewHolder方法中實現。Async是執行后臺線程的最簡單方式,但它不適用于那些重復且長時間運行的任務。放棄AsyncTask,創建一個專用的后臺線程。這是實現按需下載的最常用方式。
創建并啟動后臺線程:
public class ThumbnailDownloader<T> extends HandlerThread {
private static final String TAG = "ThumbnailDownloader";
private Boolean mHasQuit = false;
public ThumbnailDownloader() {
super(TAG);
}
@Override
public boolean quit() {
mHasQuit = true;
return super.quit();
}
public void queueThunbnail(T target,String url) {
Log.i(TAG, "queueThunbnail: URL " + url);
}
}
圖片下載可用Picasso、Glide、Fresco,Picasso小而類,Glide小巧,Fresco性能好。
StrictMode.enableDefaults方法能發現以下問題:在主線程上發起網絡請求、在主線程上做了磁盤讀寫、Activit未及時銷毀(泄露)、SQLite數據庫游標未關閉、網絡通信使用明文(未使用SSL/TLS加密)。
預加載以及緩存:LruCache。
二十七、搜索
SearchView是個可以嵌入工具欄的操作視圖類(action view),用戶可以輸入查詢關鍵字,提交查詢請求搜索,返回結果將顯示在RecyclerView中。
創建和響應Menu菜單:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/menu_item_search"
android:title="@string/search"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="ifRoom" />
<item android:id="@+id/menu_item_clear"
android:title="@string/clear_search"
app:showAsAction="never" />
</menu>
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.fragment_photo_gallery,menu);
MenuItem searchItem = menu.findItem(R.id.menu_item_search);
final SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
Log.d(TAG, "onQueryTextSubmit: " + query);
updateItems();
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
Log.d(TAG, "onQueryTextChange: " + newText);
return false;
}
});
}
使用SharedPreferences存儲查詢記錄
默認顯示已保存查詢信息:
searchView.setOnSearchClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String query = QueryPreferences.getStoredQuery(getActivity());
searchView.setQuery(query,false);
}
});
二十八、后臺服務
創建IntentService
后臺網絡連接安全:
private boolean isNetworkAvailableAndConnected() {
ConnectivityManager cm =
(ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
boolean isNetworkAvailable = cm.getActiveNetwork() != null;
boolean isNetworkConnected = isNetworkAvailable &&
cm.getActiveNetworkInfo().isConnected();
return isNetworkConnected;
}
使用AlarmManager延遲運行服務:
public static void setServiceAlarm(Context context,boolean isOn) {
Intent i = PollService.newIntent(context);
PendingIntent pi = PendingIntent.getService(context,0,i,0);
AlarmManager alarmManager = (AlarmManager)
context.getSystemService(Context.ALARM_SERVICE);
if (isOn) {
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime(),POLL_INTERVAL_MS,pi);
} else {
alarmManager.cancel(pi);
pi.cancel();
}
}
setRepeting方法是非精準重復計時器。
PendingIntent是一種token對象,將它交給其他應用使用時,它是代表當前應用發送token對象的。另外,它本身存在于操作系統而不是token里,因此實際上是你在控制著它。
可以使用PendingIntent管理定時器,查看定時器激活與否:
public static boolean isServiceAlarmOn(Context context) {
Intent i = PollService.newIntent(context);
PendingIntent pi = PendingIntent.getService(context,0,i,PendingIntent.FLAG_NO_CREATE);
return pi != null;
}
控制定時器:
boolean shouldStartAlarm = !PollService.isServiceAlarmOn(getActivity());
PollService.setServiceAlarm(getActivity(),shouldStartAlarm);
通知信息:Notification notification = new NotificationCompat.Builder(this)
.setTicker(resources.getString(R.string.new_pictures_title))
.setSmallIcon(android.R.drawable.ic_menu_report_image)
.setContentTitle(resources.getString(R.string.new_pictures_title))
.setContentText(resources.getString(R.string.new_pictures_text))
.setContentIntent(pi)
.setAutoCancel(true)
.build();
NotificationManagerCompat notificationManager =
NotificationManagerCompat.from(this);
notificationManager.notify(0,notification);
對于大多數服務任務,推薦使用IntentService。原生服務不能在后臺線程上運行。這也是推薦使用IntentService的最主要原因。大多數重要的服務都需要在后臺線程上運行,而IntentService已提供了一套標準支持方案。
服務生命周期:onCreate,onStartCommand,onDestroy。
IntentService是一種non-sticky服務。
sticky服務適用于長時間運行的服務,如音樂播放器,即使這樣,也就考慮一種使用non-sticky服務的替代架構方案。sticky服務的管理很不方便,因為很難判斷服務是否已啟動。
在API 21 中,為更好地實現后臺服務,引入了JobScheduler的全新API,除了能用來處理種類任務外,還能:發現沒有網絡時,禁止服務啟動;如果請求失敗或網絡連接受限,提供稍后重試機制;控制只在設備充電的時候,才允許檢查是否有新圖片。還支持按場景、按條件運行后臺服務。
還可以使用sync adapter創建常規的polling網絡服務。
二十九、broadcast intent
使用私有權限:在Manifest文件中定義,安全級別有四種
如何用broadcast觸發長時運行任務:將任務交給服務去處理,然后通過broadcast瞬時 啟動服務。第二種是用goAsync,但不夠靈活,不推薦使用。
事件總線:EventBus、RxJava
本地廣播不支持有序,也不支持在獨立線程上發送和接收廣播。
三十、網頁瀏覽
WebChromeClient是一個事件接口,用來響應那些改變瀏覽器中裝飾元素的事件。
自己處理配置更改,資源修改符無法自動工作。
非HTTP鏈接支持:加載URI前,先檢查它的scheme,如果不是HTTP或HTTPS,就發送一個針對目標URI的Intent.ACTION_VIEW。
三十一、定制視圖與觸摸事件
創建定制視圖三大步驟:選擇超類;覆蓋超類的構造方法;覆蓋其他關鍵方法。
public class BoxDrawingView extends View {
public BoxDrawingView(Context context) {
this(context,null);
}
public BoxDrawingView(Context context, AttributeSet attrs) {
super(context,attrs);
}
}
監聽觸摸事件的一種方式是使用setOnTouchListener方法,不過,由于定制視圖是View的子類,可走捷徑覆蓋以下方法:onTouchEvent:
@Override
public boolean onTouchEvent(MotionEvent event) {
PointF current = new PointF(event.getX(),event.getY());
String actioin = "";
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
actioin = "ACTION_DOWN";
break;
case MotionEvent.ACTION_MOVE:
actioin = "ACTION_MOVE";
break;
case MotionEvent.ACTION_UP:
actioin = "ACTION_UP";
break;
case MotionEvent.ACTION_CANCEL:
actioin = "ACTION_CANCEL";
break;
}
Log.i(TAG, "onTouchEvent: x " + current.x + "y " + current.y);
return true;
}
跟蹤運行事件:
public boolean onTouchEvent(MotionEvent event) {
PointF current = new PointF(event.getX(),event.getY());
String actioin = "";
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
actioin = "ACTION_DOWN";
mCurrentBox = new Box(current);
mBoxen.add(mCurrentBox);
break;
case MotionEvent.ACTION_MOVE:
actioin = "ACTION_MOVE";
if (mCurrentBox != null) {
mCurrentBox.setCurrent(current);
invalidate();
}
break;
case MotionEvent.ACTION_UP:
actioin = "ACTION_UP";
mCurrentBox = null;
break;
case MotionEvent.ACTION_CANCEL:
actioin = "ACTION_CANCEL";
mCurrentBox = null;
break;
}
Log.i(TAG, "onTouchEvent: x " + current.x + "y " + current.y);
return true;
}
onDraw方法內的圖形繪制
private Paint mBoxPaint;
private Paint mBackgroundPaint;
public BoxDrawingView(Context context) {
this(context,null);
}
public BoxDrawingView(Context context, AttributeSet attrs) {
super(context,attrs);
mBoxPaint = new Paint();
mBoxPaint.setColor(0x22ff0000);
mBackgroundPaint = new Paint();
mBackgroundPaint.setColor(0xfff8efe0);
}
protected void onDraw(Canvas canvas) {
canvas.drawPaint(mBackgroundPaint);
for (Box box:mBoxen) {
float left = Math.min(box.getOrigin().x,box.getCurrent().x);
float right = Math.max(box.getOrigin().x,box.getCurrent().x);
float top = Math.min(box.getOrigin().y,box.getCurrent().y);
float botton = Math.max(box.getOrigin().y,box.getCurrent().y);
canvas.drawRect(left,top,right,botton,mBoxPaint);
}
}
設備旋轉問題:protected Parcelable onsavedInstanceState(),protected void onRestoreInstanceState(Pracelable state)
三十二、屬性動畫
ObjectAnimator heightAnimator = ObjectAnimator.ofFloat(mSunView,"y",sunYStart,sunYEnd).setDuration(3200);
heightAnimator.setInterpolator(new AccelerateInterpolator());
heightAnimator.start();
三十三、地理位置和Play服務
三十四、使用地圖
三十五、material design
material surface:elevation和Z值、state list animator
動畫工具:circular reveal、shared element transition
新的視圖組件:card、floating action button、snackbar
三十六、編后語