《極簡筆記》原始碼分析(一)
0. 介紹
此文將對Github上lguipeng大神所開發的 極簡筆記 v2.0
(點我下載原始碼)程式碼進行分析學習。
通過此文你將學到:
- 應用原始碼的研讀方法
- MVP架構模式
- Application的應用
- Degger2依賴注入框架
- 搜尋控制元件的使用
- ButterKnife庫的使用
- Material主題
- RecyclerView等新控制元件的用法
- Lambda表示式
- Java自定義註解
- aFinal框架
1. Manifest入手
1.1 許可權
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
聲明瞭網路與儲存讀寫相關許可權,至於網路許可權筆者猜測應該是用於印象筆記的同步吧。
1.2 Application層
android:name=".App"
在Application層發現了一個奇怪的屬性,然後又發現專案結構目錄中有個繼承自Application的類,頓時疑惑。經查閱後又聯想到包建強的《App研發錄》中提到徹底結束安卓程式程序需要用到繼承Application的類來記錄已經開啟的Activity,然後統一結束它們,如程式碼所示:
public class App extends Application {
public List<Activity> activities=new ArrayList<Activity>();
}
Manifest進行註冊:
<application
android:icon="@drawable/icon"
android:label="@string/app_name"
android:name=".App" >
每個Activity中的做法如下:
//首先:onCreate()方法裡邊:
App app = (App) getApplicationContext();// 獲取應用程式全域性的例項引用
app.activities.add(this); // 把當前Activity放入集合中
//然後:onDestroy()方法裡邊做法:
@Override
protected void onDestroy() {
super.onDestroy();
App app = (App) getApplication();// 獲取應用程式全域性的例項引用
app.activities.remove(this); // 把當前Activity從集合中移除
}
//最後:在程式中需要結束時的做法:
List<Activity> activities = app.activities;
for (Activity act : activities) {
act.finish();// 顯式結束
}
我想此處亦是同樣原理。
補充Application相關知識點:
- 建立一個類繼承Application並在manifest的application標籤中進行註冊
- 生命週期等於這個程式的生命週期
- 通常用於資料傳遞、資料共享、資料快取等操作
- onTerminate() 當終止應用程式物件時呼叫 onLowMemory() 當後臺程式已經終止資源還匱乏時會呼叫
1.2.1 探索繼承自Application的App類
類中定義了以下方法:
private void initializeInjector() {
mAppComponent = DaggerAppComponent.builder()
.appModule(new AppModule(this))
.build();
}
通過DaggerAppComponent可以發現使用了Dagger2庫,那麼Dagger庫又是什麼呢?繼續探索…
1.2.1.1 Dagger2介紹
在此之前,需要先了解依賴注入,在本人看來其實就是低階類對高階類的依賴關係,它有以下好處:
- 依賴的注入和配置獨立於元件之外
- 因為物件是在一個獨立、不耦合的地方初始化,所以當注入抽象方法的時候,我們只需要修改物件的實現方法,而不用大改程式碼庫
- 依賴可以注入到一個元件中:我們可以注入這些依賴的模擬實現,這樣使得測試更加簡單
而Dagger2就是Google基於java的依賴注入標準維護的一個庫。
1.2.1.1 Dagger2的使用
第一步: 新增編譯和執行庫
dependencies {
apt 'com.google.dagger:dagger-compiler:2.0'
compile 'com.google.dagger:dagger:2.0'
...
}
第二步: 構建依賴
@Module
public class ActivityModule {
@Provides UserModel provideUserModel() {
return new UserModel();
}
}
第三步: 構建Injector
@Component(modules = ActivityModule.class)
public interface ActivityComponent {
void inject(MainActivity activity);
}
第三步: 完成依賴注入
public class MainActivity extends ActionBarActivity {
private ActivityComponent mActivityComponent;
@Inject UserModel userModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mActivityComponent = DaggerActivityComponent.builder().activityModule(new ActivityModule()).build();
mActivityComponent.inject(this);
((TextView) findViewById(R.id.user_desc_line)).setText(userModel.id + "\n" + userModel.name + "\n" + userModel.gender);
}
...
}
1.3 Activity層
<activity
android:name=".ui.MainActivity"
android:launchMode="singleTop"
android:windowSoftInputMode="adjustResize|stateHidden"
android:screenOrientation="portrait">
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
由標籤裡的內容可以看出該Activity是程式啟動的主Activity,如圖:
此外,還有一點值得注意:
1.3.1 搜尋功能的使用方法
搜尋有兩種實現方式,預設搜尋框(比如Toolbar上面的)和搜尋控制元件(可以在Layout裡面宣告的SearchView),一般採用預設的搜尋框方式即可,此處也只簡單講講此方式,如要了解更多可以去閱讀官方文件的建立搜尋介面
1.3.1.1 建立搜尋配置檔案
主要是對搜尋框樣式的配置,檔案儲存在res/xml/searchable.xml
:
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:label="@string/app_label"
android:hint="@string/search_hint" >
</searchable>
1.3.1.2 建立Activity並註冊
註冊Activity有兩個要點,一個是接收Intent.ACTION_SEARCH,另一個是搜尋框的配置檔案地址:
<application ... >
<activity android:name=".SearchableActivity" >
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data android:name="android.app.searchable"
android:resource="@xml/searchable"/>
</activity>
...
</application>
1.3.1.3 執行搜尋過程
搜尋的執行過程又分為3步:
- 接收查詢: 收到Intent資料獲取到搜尋內容執行搜尋
- 搜尋你的資料: 通過SQLite的FTS3方式搜尋或進行線上搜尋
- 呈現結果: 使用ListView等展示結果
此處展示接收查詢的示例程式碼:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.search);
// Get the intent, verify the action and get the query
Intent intent = getIntent();
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
String query = intent.getStringExtra(SearchManager.QUERY);
doMySearch(query);
}
}
1.3.1.4 進行實時搜尋
如果要進行實時搜尋,需要在Activity中重寫onSearchRequested()方法,返回true代表成功消耗此請求,示例程式碼如下:
@Override
public boolean onSearchRequested() {
Bundle appData = new Bundle();
appData.putBoolean(SearchableActivity.JARGON, true);
startSearch(null, false, appData, false);
return true;
}
// startSearch()中
Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA);
if (appData != null) {
boolean jargon = appData.getBoolean(SearchableActivity.JARGON);
}
2. 攻入MainActivity
2.1 ButterKnife
public class MainActivity extends BaseActivity implements MainView{
@Bind(R.id.toolbar) Toolbar toolbar;
@Bind(R.id.refresher) SwipeRefreshLayout refreshLayout;
...
}
開啟MainActivity。映入眼簾的是熟悉的ButterKnife,此處回顧一下ButterKnife的使用。
2.1.1 使用方法
- 導庫
下載jar包匯入或者直接在gradle中加上compile 'com.jakewharton:butterknife:7.0.1'
即可 @Bind
和ButterKnife.bind(Activity act);
看如下一段程式碼就能明白如何使用:
class ExampleActivity extends Activity {
@Bind(R.id.user) EditText username;
@Bind(R.id.pass) EditText password;
@BindString(R.string.login_error)
String loginErrorMessage;
@OnClick(R.id.submit) void submit() {
// TODO call server...
}
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
// TODO Use fields...
}
}
2.2 基類和介面
public class MainActivity extends BaseActivity implements MainView{
...
}
從這裡,可以進入基類BaseActivity和介面MainView看看。
2.2.1 重寫Activity生命週期的BaseActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
parseIntent(getIntent());
showActivityInAnim();
initTheme();
super.onCreate(savedInstanceState);
initWindow();
initializeDependencyInjector();
setContentView(getLayoutView());
ButterKnife.bind(this);
initToolbar();
}
通過這樣重寫生命週期的方式可以使程式碼更加統一,便於後期管理和維護。
下面就簡單分析幾個方法:
2.2.1.1 處理資料
通過 parseIntent(getIntent());
處理傳遞到Activity的資料,可以進行一些初始化操作。
2.2.1.2 過渡動畫
回顧一下Activity過渡動畫的使用方法:
overridePendingTransition(R.anim.activity_down_up_anim, R.anim.activity_exit_anim);
xml中定義的動畫:
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator" >
<translate
android:duration="@android:integer/config_shortAnimTime"
android:fromYDelta="10%p"
android:toYDelta="0" />
</set>
兩點注意:
- 此處筆者測試了下,即便
android:fromYDelta="100%p"
中為100%p,也不能省略為p。- 窗體過渡動畫不一定要在setContentView之前執行,可以在onCreate()中任意位置執行
2.2.1.3 主題切換
主題切換是通過Activity中繼承自ContextThemeWrapper的setTheme(int resid)方法實現的。
int style = R.style.RedTheme;
activity.setTheme(style);
styles中定義了多種樣式:
<style name="RedTheme" parent="AppBaseTheme.Dark">
<item name="colorPrimary">@color/red</item>
<item name="colorPrimaryDark">@color/dark_red</item>
<item name="colorAccent">@color/accent_red</item>
</style>
2.2.1.4 針對KitKat的狀態列”沉浸模式”
@TargetApi(19)
private void initWindow(){
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT){
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
SystemBarTintManager tintManager = new SystemBarTintManager(this);
tintManager.setStatusBarTintColor(getStatusBarColor());
tintManager.setStatusBarTintEnabled(true);
}
}
針對安卓4.4系統,通過使用 SystemBarTintManager
開源庫實現了狀態列變色功能。
2.2.1.5 檢視初始化
通過 setContentView(getLayoutView());
也巧妙將佈局設定轉移給子類實現 getLayoutView()
抽象方法。
@Override
protected int getLayoutView() { return R.layout.activity_main; }
通過這裡我們也就又發現了新大陸,哦不,新道路,通往Activity佈局檔案的道路。
2.2.1.6 Toolbar初始化
由於各Activity中toolbar都一樣,所以這裡就將其抽取出來了,佈局檔案中使用 <include>
標籤抽取,Activity中抽取出來一個ToolbarUtils類。
public class ToolbarUtils {
public static void initToolbar(Toolbar toolbar, AppCompatActivity activity){
if (toolbar == null || activity == null)
return;
if (activity instanceof BaseActivity){
toolbar.setBackgroundColor(((BaseActivity) activity).getColorPrimary());
}else {
toolbar.setBackgroundColor(activity.getResources().getColor(R.color.toolbar_bg_color));
}
toolbar.setTitle(R.string.app_name);
toolbar.setTitleTextColor(activity.getResources().getColor(R.color.toolbar_title_color));
toolbar.collapseActionView();
activity.setSupportActionBar(toolbar);
if (activity.getSupportActionBar() != null){
activity.getSupportActionBar().setHomeAsUpIndicator(R.drawable.abc_ic_ab_back_mtrl_am_alpha);
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
}
}
2.2.1.7 重啟Activity
BaseActivity中還包含一個reload()方法,用於沒有動畫的重啟自身Activity,以便應用新的主題。關於不重啟應用新樣式主題,讀者感興趣可以去了解知乎的不重啟Activity切換主題解決方案。
public void reload(boolean anim) {
Intent intent = getIntent();
if (!anim) {
overridePendingTransition(0, 0);
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
intent.putExtra(BaseActivity.IS_START_ANIM, false);
}
finish();
if (!anim) {
overridePendingTransition(0, 0);
}
startActivity(intent);
}
至此,BaseActivity分析得差不多了,接下來回到MainActivity。
2.2.2 MainView介面
回到MainActivity再看看MainView介面,此介面主要是對BaseActivity裡的共有方法進行抽象。
public interface MainView extends View {
void initToolbar();
void initDrawerView(List<String> list);
void setToolbarTitle(String title);
void showProgressWheel(boolean visible);
void switchNoteTypePage(List<SNote> notes);
void addNote(SNote note);
...
}
注意View介面是在本專案中的介面,而非android.view.View
2.3 MainPresenter橋樑
大致瀏覽MainActivity,可以看到到處都是MainPresenter的影子,這便是MVP的架構思想,在MainActivity中將邏輯操作轉交給MainPresenter去執行。
// 初始化依賴注入
@Override
protected void initializeDependencyInjector() {
App app = (App) getApplication();
mActivityComponent = DaggerActivityComponent.builder()
.activityModule(new ActivityModule(this))
.appComponent(app.getAppComponent())
.build();
mActivityComponent.inject(this);
}
那麼顯然在MainActivity分析完成後的下一個目標就是MainPresenter了,現在先不急,繼續分析MainActivity。
2.4 onCreate()的重寫
在MainActivity中,並沒有使用BaseActivity重寫的生命週期,而是再次重寫onCreate()方法,以獨具一格。
@Override
protected void onCreate(Bundle savedInstanceState) {
launchWithNoAnim();
super.onCreate(savedInstanceState);
initializePresenter();
mainPresenter.onCreate(savedInstanceState);
}
2.5 主佈局檔案分析
通過 getLayoutView()
可以找到主Activity對應的佈局檔案。主佈局由ToolBar和DrawerLayout組成,DrawerLayout中包含RecyclerView正文介面和ListView側滑介面,為了更好相容低版本安卓系統,使低版本也能夠擁有5.0以上版本的特效,大量使用了第三方庫和自定義控制元件。
2.5.1 頭宣告
此處注意xmlns多個是可以省略為一個的,並不會影響程式的執行,但為了程式碼的可讀性,還是應該寫成多個。
xmlns:fab="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:wheel="http://schemas.android.com/apk/res-auto"
2.5.2 FixedRecyclerView
<com.lguipeng.notes.view.FixedRecyclerView
android:id="@+id/recyclerView"
android:padding="4dp"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
這個是作者的一個修正後的RecyclerView控制元件。
public class FixedRecyclerView extends RecyclerView {
...
@Override
public boolean canScrollVertically(int direction) {
// check if scrolling up
if (direction < 1) {
boolean original = super.canScrollVertically(direction);
return !original && getChildAt(0) != null && getChildAt(0).getTop() < 0 || original;
}
return super.canScrollVertically(direction);
}
}
這段程式碼暫時有些難以理解,此處就不詳細分析了。此處讀者可以去回顧RecyclerView的用法。在MainActivity中,對RecyclerView進行了初始化:
@Override
public void initRecyclerView(List<SNote> notes){
recyclerAdapter = new NotesAdapter(notes, this);
recyclerView.setHasFixedSize(true);
recyclerAdapter.setOnInViewClickListener(R.id.notes_item_root,
new BaseRecyclerViewAdapter.onInternalClickListenerImpl<SNote>() {
@Override
public void OnClickListener(View parentV, View v, Integer position, SNote values) {
super.OnClickListener(parentV, v, position, values);
mainPresenter.onRecyclerViewItemClick(position, values);
}
});
recyclerAdapter.setOnInViewClickListener(R.id.note_more,
new BaseRecyclerViewAdapter.onInternalClickListenerImpl<SNote>() {
@Override
public void OnClickListener(View parentV, View v, Integer position, SNote values) {
super.OnClickListener(parentV, v, position, values);
mainPresenter.showPopMenu(v, position, values);
}
});
recyclerAdapter.setFirstOnly(false);
recyclerAdapter.setDuration(300);
recyclerView.setAdapter(recyclerAdapter);
refreshLayout.setColorSchemeColors(getColorPrimary());
refreshLayout.setOnRefreshListener(mainPresenter);
}
當中,設定了recyclerView的 NotesAdapter
介面卡,設定了SwipeRefreshLayout的主題顏色和重新整理監聽器,當然也傳遞給MainPresenter進行處理。
2.5.3 ProgressWheel
ProgressWheel為 materialish-progress
庫中的一個進度環控制元件,在安卓低版本中實現MaterialDesign中自帶效果,用法程式碼如下:
<com.pnikosis.materialishprogress.ProgressWheel
android:id="@+id/progress_wheel"
android:layout_width="75dp"
android:layout_height="75dp"
android:visibility="visible"
android:layout_gravity="center"
wheel:matProg_spinSpeed="1.2"
wheel:matProg_barColor="?attr/colorPrimary"
wheel:matProg_progressIndeterminate="true" />
2.5.4 Toolbar陰影
如何解決Toolbar在低版本安卓上效果不好,比如沒有陰影效果,作者很機智地include了一個陰影效果佈局:
<View
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="6dp"
android:background="@drawable/toolbar_shadow" />
drawable/toolbar_shadow:
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<gradient
android:startColor="@android:color/transparent"
android:endColor="@color/light_grey"
android:angle="90"/>
</shape>
2.5.5 BetterFab
BetterFab也是作者重寫的一個基於FloatingActionButton的自定義控制元件,主要增加了強制隱藏方法,該功能體現在 回收站
功能中FloatingActionButton被隱藏掉了,也得以猜測到此應用中抽屜切換並非切換Fragment而是通過隱藏和顯示模組實現的。
public class BetterFab extends FloatingActionButton{
private boolean forceHide = false;
...
public void setForceHide(boolean forceHide) {
this.forceHide = forceHide;
if (!forceHide) {
setVisibility(VISIBLE);
}else {
setVisibility(GONE);
}
}
//if hide,disable animation
public boolean canAnimation(){
return !isForceHide();
}
}
2.5.6 抽屜中的ListView
抽屜中的ListView包含了幾個不常用的屬性,值得一看。
<ListView android:id="@+id/left_drawer_listview"
android:layout_width="@dimen/drawer_width"
android:layout_height="0dp"
android:layout_weight="1.0"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:background="?attr/colorPrimary"/>
- choiceMode: 選擇模式: 多選和單選,預設不設定,此處單選便於使用者知道自己所在的選項卡,如圖所示:
- divider: 分隔線
- dividerHeight: 分隔線高度
分析完主佈局,繼續回到MainActivity。
2.6 NotesAdapter
首先回到之前提到了RecyclerView,其中的NotesAdapter是一個比較重要的東西,關乎著筆記列表的展示和操作。
2.6.1 承接關係
public class NotesAdapter extends BaseRecyclerViewAdapter<SNote> implements Filterable {
...
}
繼承自BaseRecyclerViewAdapter,而BaseRecyclerViewAdapter才繼承自真正應該繼承的RecyclerView.Adapter
2.6.1.1 BaseRecyclerViewAdapter
2.6.1.1.1 增刪改方法
在BaseRecyclerViewAdapter中,首先是增加了對傳入List的增刪改方法,此處只貼上增加的方法:
public void add(E e) {
this.list.add(0, e);
notifyItemInserted(0);
}
此處notifyItemInserted(int position)方法是用於通知RecyclerView有新的資料增加,對於不使用notifyDataSetChanged()方法,筆者猜測是為了防止重新整理資料時列表跳回到表首。
2.6.1.1.2 內部點選事件
private void addInternalClickListener(final View itemV, final Integer position, final E valuesMap) {
if (canClickItem != null) {
for (Integer key : canClickItem.keySet()) {
View inView = itemV.findViewById(key);
final onInternalClickListener<E> listener = canClickItem.get(key);
if (inView != null && listener != null) {
inView.setOnClickListener((view) ->
listener.OnClickListener(itemV, view, position,
valuesMap)
);
inView.setOnLongClickListener((view) -> {
listener.OnLongClickListener(itemV, view, position,
valuesMap);
return true;
});
}
}
}
}
這段程式碼邏輯比較複雜,主要是對內部的點選事件進行回撥,暫時先不作詳細分析。
2.6.1.1.3 動畫效果
首先animate方法用於執行getAnimators()中獲得的所有動畫效果:
protected void animate(RecyclerView.ViewHolder holder, int position){
if (!isFirstOnly || position > mLastPosition) {
for (Animator anim : getAnimators(holder.itemView)) {
anim.setDuration(mDuration).start();
anim.setInterpolator(mInterpolator);
}
mLastPosition = position;
} else {
ViewHelper.clear(holder.itemView);
}
}
getAnimators()方法在子類NotesAdapter進行實現:
@Override
protected Animator[] getAnimators(View view) {
if (view.getMeasuredHeight() <=0){
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1.05f, 1.0f);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1.05f, 1.0f);
return new ObjectAnimator[]{scaleX, scaleY};
}
return new Animator[]{
ObjectAnimator.ofFloat(view, "scaleX", 1.05f, 1.0f),
ObjectAnimator.ofFloat(view, "scaleY", 1.05f, 1.0f),
};
}
此處用到了屬性動畫相關知識。
2.6.2 onCreateViewHolder
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
mContext = parent.getContext();
final View view = LayoutInflater.from(mContext).inflate(R.layout.notes_item_layout, parent, false);
return new NotesItemViewHolder(view);
}
在建立單個Item檢視的ViewHolder時,先使用LayoutInflater填充出一個view,再通過NotesItemViewHolder包裝獲得ViewHolder。
2.6.2.1 NotesItemViewHolder
NotesItemViewHolder繼承自RecyclerView.ViewHolder,是一個為了提高效能的ViewHolder。
首先看建構函式:
private final TextView mNoteLabelTextView;
private final TextView mNoteContentTextView;
private final TextView mNoteTimeTextView;
public NotesItemViewHolder(View parent) {
super(parent);
mNoteLabelTextView = (TextView) parent.findViewById(R.id.note_label_text);
mNoteContentTextView = (TextView) parent.findViewById(R.id.note_content_text);
mNoteTimeTextView = (TextView) parent.findViewById(R.id.note_last_edit_text);
}
這裡並沒有使用ButterKnife,也許是因為ButterKnife的使用有需要傳入Activity引數的限制,或是因為成員變數為final型別,需要即時初始化。
類中還包含設定TextView的方法,用於設定每個Item View的文字。
2.6.3 繫結ViewHolder
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
super.onBindViewHolder(viewHolder, position);
NotesItemViewHolder holder = (NotesItemViewHolder) viewHolder;
SNote note = list.get(position);
if (note == null)
return;
String label = "";
if (mContext != null) {
boolean b = TextUtils.equals(mContext.getString(R.string.default_label), note.getLabel());
label = b? "": note.getLabel();
}
holder.setLabelText(label);
holder.setContentText(note.getContent());
holder.setTimeText(TimeUtils.getConciseTime(note.getLastOprTime(), mContext));
animate(viewHolder, position);
}
此方法主要對ViewHolder中的控制元件進行賦值,在載入每個子項時呼叫此方法。
2.6.4 過濾操作
private static class NoteFilter extends Filter{
private final NotesAdapter adapter;
private final List<SNote> originalList;
private final List<SNote> filteredList;
private NoteFilter(NotesAdapter adapter, List<SNote> originalList) {
super();
this.adapter = adapter;
this.originalList = new LinkedList<>(originalList);
this.filteredList = new ArrayList<>();
}
@Override
protected FilterResults performFiltering(CharSequence constraint) {
filteredList.clear();
final FilterResults results = new FilterResults();
if (constraint.length() == 0) {
filteredList.addAll(originalList);
} else {
for ( SNote note : originalList) {
if (note.getContent().contains(constraint) || note.getLabel().contains(constraint)) {
filteredList.add(note);
}
}
}
results.values = filteredList;
results.count = filteredList.size();
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
adapter.list.clear();
adapter.list.addAll((ArrayList<SNote>) results.values);
adapter.notifyDataSetChanged();
}
}
此類主要由搜尋功能呼叫,建構函式對originalList進行賦值,performFiltering(…)方法進行過濾操作,過濾後列表存入filteredList,並且返回FilterResults以便後用,publishResults(…)方法進行展示filteredList的內容。
2.7 DrawerView的初始化
DrawerView檢視比較簡單,只有一個ListView,不過其中包含很多細節值得學習,而且作者為了後期的可拓展性定義了抽象類和介面。
2.7.1 DrawerListAdapter
首先是DrawerListAdapter,繼承自SimpleListAdapter,而SimpleListAdapter又繼承自BaseListAdapter,然後才是繼承自API的BaseAdapter,繼承結構如圖:
Android Studio中按快捷鍵F4檢視類繼承結構圖
2.7.1.1 BaseListAdapter
與BaseRecyclerViewAdapter類似,同樣包含需要傳入引數進行初始化操作的列表,以及增刪改方法,以及回撥的點選事件介面。
2.7.1.2 SimpleListAdapter
@Override
public View bindView(int position, View convertView, ViewGroup parent) {
Holder holder;
if (convertView == null){
convertView = LayoutInflater.from(mContext).inflate(getLayout(), null);
holder = new Holder();
holder.textView = (TextView)convertView.findViewById(R.id.textView);
convertView.setTag(holder);
}else{
holder = (Holder)convertView.getTag();
}
holder.textView.setText(list.get(position));
return convertView;
}
SimpleListAdapter中,實現了抽象方法bindView(…),並且使用了ListView的快取機制,但bindView(…)中填充Item檢視並沒有寫死,而是交給了子類DrawerListAdapter去進行實現。
2.7.1.3 DrawerListAdapter
@Override
protected int getLayout() { return R.layout.drawer_list_item_layout; }
2.7.1.3.1 佈局
佈局僅用了一個簡潔的TextView,但TextView中包含了幾個不常見的屬性:
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="Medium Text"
android:singleLine="true"
- android:textAppearance: 系統文字外觀,’?’代表試探系統是否有此外觀,沒有則使用預設外觀
- tools:text: 告訴Android Studio在執行時忽略該屬性,只在設計佈局時有效
- android:singleLine: 就是單行顯示文字
2.7.2 抽屜開關按鈕
通過 mDrawerLayout.setDrawerListener(mDrawerToggle);
為抽屜加上開關抽屜的監聽。對監聽器的配置如下:
mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, 0, 0){
@Override
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
invalidateOptionsMenu();
mainPresenter.onDrawerOpened();
}
@Override
public void onDrawerClosed(View drawerView) {
super.onDrawerClosed(drawerView);
invalidateOptionsMenu();
mainPresenter.onDrawerClosed();
}
};
mDrawerToggle.setDrawerIndicatorEnabled(true); // 指示器: 用於動畫展示開關操作按鈕變化
2.7.3 設定抽屜遮簾顏色
mDrawerLayout.setScrimColor(getCompactColor(R.color.drawer_scrim_color));
此處放上設定遮簾為藍色後的效果圖:
2.8 PopupMenu
在每個CardView上面需要顯示選單,包含”編輯”和”回收”,顯示PopupMenu方法如下:
@Override
public void showNormalPopupMenu(View view, SNote note) {
PopupMenu popup = new PopupMenu(this, view);
popup.getMenuInflater()
.inflate(R.menu.menu_notes_more, popup.getMenu());
popup.setOnMenuItemClickListener((item -> mainPresenter.onPopupMenuClick(item.getItemId(), note)));
popup.show();
}
2.9 ActionBar上的搜尋框
2.9.1 定義選單
首先在menu.xml中新增搜尋項:
<item
android:id="@+id/action_search"
android:icon="@drawable/abc_ic_search_api_mtrl_alpha"
android:title="@string/search"
app:showAsAction="ifRoom|collapseActionView"
app:actionViewClass="android.support.v7.widget.SearchView">
</item>
2.9.2 初始化SearchView
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
SearchManager searchManager =
(SearchManager) getSystemService(Context.SEARCH_SERVICE);
MenuItem searchItem = menu.findItem(R.id.action_search);
//searchItem.expandActionView(); // 預設展開搜尋框
SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
ComponentName componentName = getComponentName();
searchView.setSearchableInfo(
searchManager.getSearchableInfo(componentName));
searchView.setQueryHint(getString(R.string.search_note));
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String s) {
return true;
}
@Override
public boolean onQueryTextChange(String s) {
recyclerAdapter.getFilter().filter(s); // 文字改變就即時處理搜尋
return true;
}
});
MenuItemCompat.setOnActionExpandListener(searchItem, mainPresenter); // 監聽搜尋框是否開啟,用於隱藏FloatingActionBar和禁用下拉重新整理
return true;
}
2.10 處理選單事件
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if(mDrawerToggle.onOptionsItemSelected(item)) {
return true;
}
if (mainPresenter.onOptionsItemSelected(item.getItemId())){
return true;
}
return super.onOptionsItemSelected(item);
}
第一個if用於判斷是否點選開啟抽屜開關按鈕,第二個才傳入MainPresenter進行選單的處理,返回true當然就表示消耗此事件。
2.11 處理實體按鍵事件
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return mainPresenter.onKeyDown(keyCode) || super.onKeyDown(keyCode, event);
}
返回值注意的是先處理傳入MainPresentor裡面方案,有程式碼自左至右執行順序,如果不滿足則按父類方法處理,這樣寫簡直精妙,避免了多重if判斷。
2.12 刪除對話方塊的顯示
@Override
public void showDeleteForev