Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 《極簡筆記》源碼分析(一)

《極簡筆記》源碼分析(一)

編輯:關於Android編程

0. 介紹

此文將對Github上lguipeng大神所開發的 極簡筆記 v2.0 (點我下載源碼)代碼進行分析學習。
通過此文你將學到:

應用源碼的研讀方法 MVP架構模式 Application的應用 Degger2依賴注入框架搜索控件的使用 ButterKnife庫的使用 Material主題 RecyclerView等新控件的用法 Lambda表達式 Java自定義注解 aFinal框架

1. Manifest入手

1.1 權限





聲明了網絡與儲存讀寫相關權限,至於網絡權限筆者猜測應該是用於印象筆記的同步吧。

1.2 Application層

android:name=".App"

項目結構

在Application層發現了一個奇怪的屬性,然後又發現項目結構目錄中有個繼承自Application的類,頓時疑惑。經查閱後又聯想到包建強的《App研發錄》中提到徹底結束安卓程序進程需要用到繼承Application的類來記錄已經打開的Activity,然後統一結束它們,如代碼所示:

public class App extends Application {

    public List activities=new ArrayList();

}

Manifest進行注冊:

每個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 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是程序啟動的主Activity,如圖:
主Activity界面
此外,還有一點值得注意:

1.3.1 搜索功能的使用方法

搜索有兩種實現方式,默認搜索框(比如Toolbar上面的)和搜索控件(可以在Layout裡面聲明的SearchView),一般采用默認的搜索框方式即可,此處也只簡單講講此方式,如要了解更多可以去閱讀官方文檔的創建搜索界面

1.3.1.1 創建搜索配置文件

主要是對搜索框樣式的配置,文件保存在res/xml/searchable.xml:



1.3.1.2 創建Activity並注冊

注冊Activity有兩個要點,一個是接收Intent.ACTION_SEARCH,另一個是搜索框的配置文件地址:


    
        
            
        
        
    
    ...

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的使用。
ButterKnife

2.1.1 使用方法

導庫
下載jar包導入或者直接在gradle中加上 compile 'com.jakewharton:butterknife:7.0.1'即可 @BindButterKnife.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...
  }
}

更多使用方法詳見官方介紹,JakeWharton/butterknife

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中定義的動畫:


    

兩點注意:

此處筆者測試了下,即便 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中定義了多種樣式:

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都一樣,所以這裡就將其抽取出來了,布局文件中使用 標簽抽取,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 list);
    void setToolbarTitle(String title);
    void showProgressWheel(boolean visible);
    void switchNoteTypePage(List 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

這個是作者的一個修正後的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 notes){
    recyclerAdapter = new NotesAdapter(notes, this);
    recyclerView.setHasFixedSize(true);
    recyclerAdapter.setOnInViewClickListener(R.id.notes_item_root,
            new BaseRecyclerViewAdapter.onInternalClickListenerImpl() {
                @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() {
                @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中自帶效果,用法代碼如下:

2.5.4 Toolbar陰影

如何解決Toolbar在低版本安卓上效果不好,比如沒有陰影效果,作者很機智地include了一個陰影效果布局:

drawable/toolbar_shadow:


    

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包含了幾個不常用的屬性,值得一看。

choiceMode: 選擇模式: 多選和單選,默認不設定,此處單選便於用戶知道自己所在的選項卡,如圖所示:

None模式

 

單選模式

divider: 分隔線 dividerHeight: 分隔線高度

分析完主布局,繼續回到MainActivity。

2.6 NotesAdapter

首先回到之前提到了RecyclerView,其中的NotesAdapter是一個比較重要的東西,關乎著筆記列表的展示和操作。

2.6.1 承接關系

public class NotesAdapter extends BaseRecyclerViewAdapter 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 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 originalList;

    private final List filteredList;

    private NoteFilter(NotesAdapter adapter, List 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) 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,繼承結構如圖:
DrawerListAdapter類繼承結構圖

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中新增搜索項:


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 showDeleteForeverDialog(final SNote note) {
    AlertDialog.Builder builder = DialogUtils.makeDialogBuilder(this);
    builder.setTitle(R.string.delete_tip);
    DialogInterface.OnClickListener listener = (DialogInterface dialog, int which) ->
            mainPresenter.onDeleteForeverDialogClick(note, which);
    builder.setPositiveButton(R.string.sure, listener);
    builder.setNegativeButton(R.string.cancel, listener);
    builder.show();
}

這個邏輯沒什麼問題,主要想說的就是用lambda表達式的寫法真的很好,不過筆者很好奇作者如何這麼順暢地寫出lambda表達式,畢竟沒有智能提示。

2.13 SnackbarUtils

SnackbarUtils是作者封裝的快速顯示Snackbar消息的,這個要學習的是如何通過傳入Activity或Fab本身來執行Snackbar.make(…)方法:

public static void show(View view, int message) {
    Snackbar.make(view, message, Snackbar.LENGTH_SHORT)
            .show();
}
public static void show(Activity activity, int message) {
    View view = activity.getWindow().getDecorView();
    show(view, message);
}

可以傳入FloatActionBar本身調用方法:

SnackbarUtils.show(fab, message);

至此,龐大的MainActivity算是分析得差不多了,那麼接下來便啃另一塊大骨頭——MainPresenter。

3. Presenter —— MVP中的橋梁

3.1 接口

為了將邏輯放到P層中,MainPresenter繼承了多個接口。

public class MainPresenter implements Presenter, android.view.View.OnClickListener, SwipeRefreshLayout.OnRefreshListener,
        PopupMenu.OnMenuItemClickListener, MenuItemCompat.OnActionExpandListener {

由於其它接口前文已有提及,此處只展示和分析Presenter接口。
Presenter接口將Activity的生命周期抽象出來,並且通過attachView將Activity傳入,用於MainPresenter的初始化。

public interface Presenter {
    void onCreate (Bundle savedInstanceState);
    void onResume();
    void onStart ();
    void onPause();
    void onStop ();
    void onDestroy();
    void attachView(View v);
}

注意View是本項目中所定義的,並非安卓API。

3.2 構造函數

@Inject
public MainPresenter(@ContextLifeCycle ("Activity")Context context, FinalDb finalDb, PreferenceUtils preferenceUtils,
                     ObservableUtils mObservableUtils, EverNoteUtils everNoteUtils) {
    this.mContext = context;
    this.mFinalDb = finalDb;
    this.mPreferenceUtils = preferenceUtils;
    this.mEverNoteUtils = everNoteUtils;
    this.mObservableUtils = mObservableUtils;
}

構造函數的第一個參數前有一個 @ContextLifeCycle ("Activity") ,不知道讀者是否對此感到疑惑,要分析這個首先要了解java中的自定義注解。

3.2.1 Java Annotation自定義注解

此處只提及幾個關鍵點,讀者感興趣可以參考深入淺出Java Annotation(元注解和自定義注解)。

3.2.1.1 Annotation概述

Annontation是Java5開始引入的新特征。中文名稱一般叫注解。它提供了一種安全的類似注釋的機制,用來將任何的信息或元數據(metadata)與程序元素(類、方法、成員變量等)進行關聯。

3.2.1.2 定義Annotation

使用關鍵字@interface而不是interface 方法定義Annotation的成員,方法返回值類型必須為primitive類型、Class類型、枚舉類型、annotation類型或者由前面類型之一作為元素的一維數組,方法的後面可以使用 default和一個默認數值來聲明成員的默認值,null不能作為成員默認值

3.2.1.3 元注解

@Target 所修飾的對象范圍 @Retention 該Annotation被保留的時間長短 @Documented 可以被例如javadoc此類的工具文檔化 @Inherited 被標注的類型是被繼承的

3.2.1.4 自定義注解

定義注解格式:
  public @interface 注解名 {定義體}
例如:

@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.TYPE)  
@Documented  
public @interface Description {  
    String value();
}  

3.2.1.5 對項目中”ContextLifeCycle”的分析

@Qualifier
@Documented
@Retention(RUNTIME)
public @interface ContextLifeCycle {
    String value() default "App";
}

@Qualifier: 限定注釋符
因為ContextLifeCycle的實現需要反射,所以我們暫時不能跟蹤到它的實現,所以暫時先不作實現分析。

3.2.2 FinalDb參數

FinalDb是一個比較龐大的類,是aFinal第三方庫的一個子模塊,主要是數據庫操作,接下來讓我們大致了解一下它。

3.2.2.1 數據庫連接池

private static HashMap daoMap = new HashMap();
private synchronized static FinalDb getInstance(DaoConfig daoConfig) {
    FinalDb dao = daoMap.get(daoConfig.getDbName());
    if (dao == null) {
        dao = new FinalDb(daoConfig);
        daoMap.put(daoConfig.getDbName(), dao);
    }
    return dao;
}

由HashMap類型的daoMap成員變量和getInstance(…),可以看出將daoMap作為數據庫連接池使用,可以提高數據庫連接的復用率,不過注意getInstance()也是私有的,獲取FinalDb要通過create()方法。

3.2.2.1.1 數據庫信息配置類DaoConfig

DaoConfig主要包含以下屬性:

private Context mContext = null; // android上下文
private String mDbName = "notes.db"; // 數據庫名字
private int dbVersion = 1; // 數據庫版本
private boolean debug = true; // 是否是調試模式(調試模式 增刪改查的時候顯示SQL語句)
private DbUpdateListener dbUpdateListener;
// private boolean saveOnSDCard = false;//是否保存到SD卡
private String targetDirectory;// 數據庫文件在sd卡中的目錄

3.2.2.2 構造函數

private FinalDb(DaoConfig config) {
    ...
    if (config.getTargetDirectory() != null
            && config.getTargetDirectory().trim().length() > 0) {
        this.db = createDbFileOnSDCard(config.getTargetDirectory(),
                config.getDbName());
    } else {
        this.db = new SqliteDbHelper(config.getContext()
                .getApplicationContext(), config.getDbName(),
                config.getDbVersion(), config.getDbUpdateListener())
                .getWritableDatabase();
    }
    this.config = config;
}

此部分判斷如果配置中指定了文件目錄,則在指定的文件目錄創建數據庫文件,否則使用SqliteDbHelper獲取軟件Data目錄下的數據庫。

3.2.2.2.1 createDbFileOnSDCard方法

通過SQLiteDatabase.openOrCreateDatabase(file, null)在SD卡創建數據庫文件(*.db)。

3.2.2.2.2 SqliteDbHelper內部類
class SqliteDbHelper extends SQLiteOpenHelper {
    ...
}

此類繼承SQLiteOpenHelper獲取安卓默認的數據庫。

3.2.2.3 公有的create()方法

create方法重載了多個,配合使用一個或多個參數調用的情況。例如其中之一:

public static FinalDb create(Context context, String targetDirectory,
        String dbName, boolean isDebug, int dbVersion,
        DbUpdateListener dbUpdateListener) {
    DaoConfig config = new DaoConfig();
    config.setContext(context);
    config.setTargetDirectory(targetDirectory);
    config.setDbName(dbName);
    config.setDebug(isDebug);
    config.setDbVersion(dbVersion);
    config.setDbUpdateListener(dbUpdateListener);
    return create(config);
}

後面便是對數據庫CRUD操作和數據庫關系操作,筆者能力有限,便不繼續研讀,接下來談談此框架的使用方法。

3.2.2.4 用法

首先創建相關Entity類,如:
public class User {
    private int id;
    private String name;
    private String email;
    private Date registerDate;
    private Double money;

    /////////////getter and setter 不能省略哦///////////////
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public Date getRegisterDate() {
        return registerDate;
    }
    public void setRegisterDate(Date registerDate) {
        this.registerDate = registerDate;
    }
    public Double getMoney() {
        return money;
    }
    public void setMoney(Double money) {
        this.money = money;
    }
}
接下來使用aFinal即可
FinalDb db = FinalDb.create(this);
User user = new User();
user.setEmail("...");
user.setName("...");
user.setRegisterDate(new Date());
db.save(user);
List userList = db.findAll(User.class); //查詢所有的用戶
Log.e("AfinalOrmDemoActivity", "用戶數量:"+ (userList!=null?userList.size():0));
textView.setText(userList.get(0).getName()+":"+user.getRegisterDate());

至此,相信讀者對FinalDb有了一個初步的認識。

3.2.3 PreferenceUtils參數

這是一個對SharePreference的封裝類,比較簡單,就不做分析了。

3.2.4 ObservableUtils參數

ObservableUtils采用了RxJava,作為被觀察者,這其中有一點比較重要——將函數作為參數來傳遞,這是C#的一個特性——委托,亦或是代理設計模式。

3.2.4.1 Fun接口

public interface Fun {
    T call() throws Exception;
}

內部包含一個Fun接口,即為一個函數,每個需要作為參數的函數被包裝在實現該接口的類中。如:

private class GetEverNoteUserFun implements Fun{
    private EverNoteUtils mEverNoteUtils;
    public GetEverNoteUserFun(EverNoteUtils mEverNoteUtils) {
        this.mEverNoteUtils = mEverNoteUtils;
    }
    @Override
    public User call() throws Exception{
        return mEverNoteUtils.getUser();
    }
}

這樣函數就能作為一個參數被傳遞到其它函數中。如:

create(new GetEverNoteUserFun(everNoteUtils))

此處將 方法 稱作 函數 是為了便於理解。

3.2.5 EverNoteUtils參數

EverNoteUtils就是調用印象筆記的API,包含推送筆記和獲取筆記等方法,這裡可以看印象筆記官方提供的API,此處不作分析。

待續

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved