《极简笔记》源码分析

Posted 吴豪杰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《极简笔记》源码分析相关的知识,希望对你有一定的参考价值。

0. 介绍

此文将对Github上lguipeng大神所开发的 极简笔记 v2.0 (点我下载源码)代码进行分析学习。
通过此文你将学到:

  • 应用源码的研读方法
  • MVP架构模式
  • Application的应用
  • Degger2依赖注入框架
  • 搜索控件的使用
  • ButterKnife库的使用
  • Material主题
  • RecyclerView等新控件的用法
  • Lambda表达式
  • Java自定义注解
  • aFinal框架
  • RxJava框架
  • EventBus消息框架
  • 布局文件常用技巧
  • PreferenceFragment
  • 动态申请权限

此为系列文章,传送门: 《极简笔记》源码分析(一)

3.3 onCreate()方法

3.3.1 Activity崩溃的信息保存

想要在Activity崩溃的时候,其实主要是屏幕发生旋转时保存信息,以便重启Activity后能够恢复信息,需要怎么做呢?很简单,重写 onSaveInstanceState(Bundle outState) 方法,在内部实现信息的保存,并在onCreate方法中对信息进行恢复即可,如:

// 信息的保存: 笔者猜测是用于保存当前界面,保存在"Normal"模式还是"回收站"模式
public void onSaveInstanceState(Bundle outState)
    outState.putInt(CURRENT_NOTE_TYPE_KEY, mCurrentNoteTypePage.getValue());

然后再onCreate中恢复信息:

if (savedInstanceState != null)
    int value = savedInstanceState.getInt(CURRENT_NOTE_TYPE_KEY);
    mCurrentNoteTypePage = SNote.NoteType.mapValueToStatus(value);

3.3.2 初始化视图

view.initToolbar();
initDrawer();
initMenuGravity();
initItemLayoutManager();
initRecyclerView();

在onCreate方法中还进行了各视图的初始化。

3.3.2.1 initToolbar

调用所有继承自MainView接口的Activity的初始化Toolbar方法以初始化。

3.3.2.2 初始化抽屉

drawerList = Arrays.asList(mContext.getResources()
        .getStringArray(R.array.drawer_content));
view.initDrawerView(drawerList);
view.setDrawerItemChecked(mCurrentNoteTypePage.getValue());
view.setToolbarTitle(drawerList.get(mCurrentNoteTypePage.getValue()));

这里值得留意的是在strings.xml中定义数组:

<array name="drawer_content">
    <item>SNotes</item>
    <item>回收站</item>
</array>

3.3.2.3 设置抽屉方向


通过view.setMenuGravity(Gravity.END)和view.setMenuGravity(Gravity.START)设置抽屉的方向,由MVP模式,具体的实现放在MainActivity中。

@Override
public void setMenuGravity(int gravity) 
    DrawerLayout.LayoutParams params = (DrawerLayout.LayoutParams) drawerRootView.getLayoutParams();
    params.gravity = gravity;
    drawerRootView.setLayoutParams(params);

3.3.2.4 设置RecyclerView线性或网格排列

private void switchItemLayoutManager(boolean card)
    if (card)
        view.setLayoutManager(new StaggeredGridLayoutManager(2, LinearLayoutManager.VERTICAL));
    else 
        view.setLayoutManager(new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false));
    
    isCardItemLayout = card;

在MainActivity中进行视图操作:

recyclerView.setLayoutManager(manager);

3.3.2.5 加载笔记

public void initRecyclerView()
    view.showProgressWheel(true);
    mObservableUtils.getLocalNotesByType(mFinalDb, mCurrentNoteTypePage.getValue())
            .subscribeOn(Schedulers.computation())
            .observeOn(androidSchedulers.mainThread())
            .subscribe((notes) -> 
                view.initRecyclerView(notes);
                view.showProgressWheel(false);
            , (e) -> 
                e.printStackTrace();
                view.showProgressWheel(false);
            );

这段代码又用到RxJava,所以显得比较复杂,我们来慢慢分析。

3.3.2.5.1 getLocalNotesByType

那么首先来看ObservableUtils里的getLocalNotesByType方法,它将返回一个Observable对象,此处截取了关键部分:

return Observable.create(new Observable.OnSubscribe<T>() 
    @Override
    public void call(Subscriber<? super T> subscriber) 
        try 
            T t = fun.call();
            subscriber.onNext(t);
        catch (Exception e)
            subscriber.onError(e);
        
    
);

下面是fun.call():

@Override
public List<SNote> call() throws Exception 
    return mFinalDb.findAllByWhere(SNote.class, "type = " + type, "lastOprTime", true);

其中type为mCurrentNoteTypePage.getValue(),标志是否为回收站的内容。
findAllByWhere方法原型为:

List<T> findAllByWhere(Class<T> clazz, String strWhere, String orderBy, boolean desc);

它是aFinal库中FinalDb中的方法,功能为依据条件查找所有元素并反序列化为对象List。

3.3.2.5.2 指定线程
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())

此处的代码用于指定观察者和被观察者所在的线程,由于加载笔记属于重大任务,所以指定在computation计算线程,另外subscribeOn可指定的参数如图:

此处回顾一下RxJava的调度器:

在RxJava 中,Scheduler ——调度器,相当于线程控制器,RxJava 通过它来指定每一段代码应该运行在什么样的线程。RxJava 已经内置了几个 Scheduler ,它们已经适合大多数的使用场景:

  • Schedulers.immediate(): 直接在当前线程运行,相当于不指定线程。这是默认的 Scheduler。
  • Schedulers.newThread(): 总是启用新线程,并在新线程执行操作。
  • Schedulers.io(): I/O 操作(读写文件、读写数据库、网络信息交互等)所使用的 Scheduler。行为模式和 newThread() 差不多,区别在于 io() 的内部实现是是用一个无数量上限的线程池,可以重用空闲的线程,因此多数情况下 io() 比 newThread() 更有效率。不要把计算工作放在 io() 中,可以避免创建不必要的线程。
  • Schedulers.computation(): 计算所使用的 Scheduler。这个计算指的是 CPU 密集型计算,即不会被 I/O 等操作限制性能的操作,例如图形的计算。这个 Scheduler 使用的固定的线程池,大小为 CPU 核数。不要把 I/O 操作放在 computation() 中,否则 I/O 操作的等待时间会浪费 CPU。
  • 另外, Android 还有一个专用的 AndroidSchedulers.mainThread(),它指定的操作将在 Android 主线程运行。
3.3.2.5.3 订阅

subscribe(onNext, onError)方法需要两个参数,一个是接下来需要执行的操作,一个是执行错误的回调操作,均需要实现Action1接口,此处使用lambda表达式简化了代码。

3.3.3 EventBus库

EventBus.getDefault().register(this);

接下来使用到了 EventBus 库,所以让我们先来学习一下EventBus,这个库真可以说是发现的一块新大陆。

3.3.3.1 介绍

EventBus是一款针对Android优化的发布/订阅事件总线。主要功能是替代Intent,Handler,BroadCast在Fragment,Activity,Service,线程之间传递消息.优点是开销小,代码更优雅。以及将发送者和接收者解耦。

3.3.3.2 使用

  • 导入库
compile 'org.greenrobot:eventbus:3.0.0'
  • 编写消息类,传递的消息是一个对象
public class FirstEvent 

    private String mMsg;  
    public FirstEvent(String msg) 
        mMsg = msg;  
      
    public String getMsg()
        return mMsg;  
      
  
  • 注册与反注册
    在onCreate方法中进行注册:
EventBus.getDefault().register(this); 

在onDestroy方法中进行反注册:

EventBus.getDefault().unregister(this);
  • 发送消息
EventBus.getDefault().post(new FirstEvent("FirstEvent btn clicked"));
  • 接收消息
    在接受消息的Activity中重写事件接收方法,如onEventMainThread:
public void onEventMainThread(FirstEvent event);

注参数一定要匹配,否则该方法将接收不到数据。消息的接收是通过判断参数是否匹配来的,它将调用四种接收方法中所有匹配该参数的方法。

3.3.3.3 4种接收消息方法

前文在接受消息使用到是onEventMainThread方法,那么各方法有什么区别呢。

  • onEvent:如果使用onEvent作为订阅函数,那么该事件在哪个线程发布出来的,onEvent就会在这个线程中运行,也就是说发布事件和接收事件线程在同一个线程。使用这个方法时,在onEvent方法中不能执行耗时操作,如果执行耗时操作容易导致事件分发延迟。
  • onEventMainThread:如果使用onEventMainThread作为订阅函数,那么不论事件是在哪个线程中发布出来的,onEventMainThread都会在UI线程中执行,接收事件就会在UI线程中运行,这个在Android中是非常有用的,因为在Android中只能在UI线程中跟新UI,所以在onEvnetMainThread方法中是不能执行耗时操作的。
  • onEventBackground:如果使用onEventBackgrond作为订阅函数,那么如果事件是在UI线程中发布出来的,那么onEventBackground就会在子线程中运行,如果事件本来就是子线程中发布出来的,那么onEventBackground函数直接在该子线程中执行。
  • onEventAsync:使用这个函数作为订阅函数,那么无论事件在哪个线程发布,都会创建新的子线程在执行onEventAsync.

至此,EventBus的使用已基本介绍完毕,接下来分析EventBus在本项目中的使用。

3.3.3.3 本项目中EventBus使用

3.3.3.3.1 注册与反注册

首先在onCreate和onDestroy中进行了注册与反注册。

3.3.3.3.2 消息接收——同步笔记

public void onEventMainThread(EverNoteUtils.SyncResult result)
    if (result != EverNoteUtils.SyncResult.START)
        view.stopRefresh();
    switch (result)
        case ERROR_NOT_LOGIN: view.showGoBindEverNoteSnackbar(R.string.unbind_ever_note_tip, R.string.go_bind);break;
        ...
        case SUCCESS:view.showSnackbar(R.string.sync_success);refreshNoteTypePage();break;
    

消息接收方法处于主线程中,可以进行UI操作,同时,消息的参数很巧妙——使用了一个枚举,作为参数,可以集合多种情况。

public enum SyncResult
    START,
    ...
    SUCCESS

3.3.3.3.3 消息接收——其他操作

public void onEventMainThread(NotifyEvent event)
    switch (event.getType())
        case NotifyEvent.REFRESH_LIST:
            view.startRefresh();
            onRefresh();
            break;
        case NotifyEvent.CREATE_NOTE:
            if (event.getData() instanceof SNote)
                SNote note = (SNote)event.getData();
                view.addNote(note);
                view.scrollRecyclerViewToTop();
                pushNote(note);
            
            break;
        case NotifyEvent.UPDATE_NOTE:
            if (event.getData() instanceof SNote)
                SNote note = (SNote)event.getData();
                view.updateNote(note);
                view.scrollRecyclerViewToTop();
                pushNote(note);
            
            break;
        case NotifyEvent.CHANGE_THEME:
            view.reCreate();
            break;
    

消息接收的其它操作主要是调用Activity下实现的对笔记的操作方法,此处有几点值得学习:

  • 首先是RecyclerView的滚动,通过下面的方法可以使RecyclerView平滑地滚动到指定位置
@Override
public void scrollRecyclerViewToTop() 
    recyclerView.smoothScrollToPosition(0);
  • 重建Activity
@Override
public void reCreate() 
    super.recreate();

关于消息的发送就放到消息发送消息代码处进行分析。

3.4 onResume()

onResume比较简单,主要是对已加载配置文件并应用设置,这也告诉我们,通常加载配置文件放在依赖注入处,也就是onCreate中,而应用设置放在onResume中。

@Override
public void onResume() 
    if (isRightHandMode != mPreferenceUtils.getBooleanParam(mContext
            .getString(R.string.right_hand_mode_key)))
        isRightHandMode = !isRightHandMode;
        if (isRightHandMode)
            view.setMenuGravity(Gravity.END);
        else
            view.setMenuGravity(Gravity.START);
        
    
    if (isCardItemLayout != mPreferenceUtils.getBooleanParam(mContext
            .getString(R.string.card_note_item_layout_key), true))
        switchItemLayoutManager(!isCardItemLayout);
    

switchItemLayoutManager方法最终通过recyclerView.setLayoutManager(manager)设置显示方式。

3.5 返回监听

public boolean onKeyDown(int keyCode)
    if (keyCode == KeyEvent.KEYCODE_BACK)
        if (view.isDrawerOpen())
            view.closeDrawer();
        else 
            view.moveTaskToBack();
        
        return true;
    
    return false;

这段代码的逻辑是,如果按的不是返回键,则返回false交给系统处理,如果按的是返回键且抽屉未关,则关闭抽屉,否则调用 super.moveTaskToBack(true) 手动隐藏Activity,保持栈结构,将任务移到后台,不会调用onDestroy方法。

3.6 同步笔记

private void sync(EverNoteUtils.SyncType type, boolean silence)
    //mEverNoteUtils.sync();
    mObservableUtils.sync(mEverNoteUtils, type)
            .subscribeOn(Schedulers.newThread())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe((result -> 
                if (!silence)
                    onEventMainThread(result);
            ));

主要进行笔记的同步。

3.6 启动其它Activity

private void startNoteActivity(int type, SNote value)
    Intent intent = new Intent(mContext, NoteActivity.class);
    Bundle bundle = new Bundle();
    bundle.putInt(NotePresenter.OPERATE_NOTE_TYPE_KEY, type);
    EventBus.getDefault().postSticky(value);
    intent.putExtras(bundle);
    mContext.startActivity(intent);

public void startSettingActivity()
    Intent intent = new Intent(mContext, SettingActivity.class);
    mContext.startActivity(intent);

public void startAboutActivity()
    Intent intent = new Intent(mContext, AboutActivity.class);
    mContext.startActivity(intent);

在MainPresenter的最后,可以找到其它Activity的入口,在下文中,我们将对这些Activity进行分析。
先分析Activity还是先看看这两段代码,首先是EventBus的postSticky(value)方法,该方法用于会多次传递的参数,可以通过getStickyEvent(ClasseventType)来获取最新发布的对象。还有传递的Bundle数据,笔者猜测是将”新建”或”编辑”操作参数传递过去。

4. 笔记编辑界面

4.1 界面初始化

首先是界面的初始化操作,也就是onCreate()方法,笔者对此处很好奇,因为没有对Bundle数据进行操作,那么是如何区分编辑模式和新建模式呢,答案就是通过Activity下的getIntent()方法可以获取Intent,再通过重写自BaseActivity的parseIntent()方法设定操作方式。

@Override
protected void onCreate(Bundle savedInstanceState) 
    super.onCreate(savedInstanceState);
    initializePresenter();
    notePresenter.onCreate(savedInstanceState);

其中两个增加的函数分别进行了Presenter的初始化和EventBus的注册。当然此处的super.onCreate(savedInstanceState)调用了父类BaseActivity的onCreate方法。

4.2 布局文件

布局比较简单,这里仅挑选几个要点:

  • 焦点拦截
android:focusable="true"
android:focusableInTouchMode="true"

在界面顶部的ListView中捕获焦点,可以焦点拦截使得EditText不会抢先获取焦点。

  • ScrollView属性

    1. android:fadingEdge="none" : 用于配置边界效果
    2. android:cacheColorHintandroid:scrollingCache : 用于消除缓存机制,使得这种情况下更加流畅
    3. android:overScrollMode : 越界模式
  • com.rengwuxian.materialedittext.MaterialEditText : 第三方材质风格控件

  • android:lineSpacingExtra : 行距属性,单位dp

4.3 菜单管理

4.3.1 动态修改菜单

@Override
public boolean onPrepareOptionsMenu(Menu menu) 
    doneMenuItem = menu.getItem(0);
    notePresenter.onPrepareOptionsMenu();
    return super.onPrepareOptionsMenu(menu);


@Override
public boolean onOptionsItemSelected(MenuItem item) 
    if (notePresenter.onOptionsItemSelected(item))
        return true;
    return super.onOptionsItemSelected(item);

要实现动态修改菜单,关键点在于:
onCreateOptionsMenu(Menu menu)方法只在Activity创建时调用一次,而onPrepareOptionsMenu(Menu menu)在每次访问菜单的时候调用。此界面中具体实现逻辑为: 首次通过inflater填充菜单,之后一旦访问菜单即设置为不可见。

菜单的add()方法是追加式的,需要先调用clear()方法。

4.4 初始化视图

三个方法分别初始化为编辑模式、查看模式、创建模式。其中几点值得注意:

4.4.1 焦点请求

对于EditText等View,可以调用labelEditText.requestFocus()捕获焦点。

4.4.2 监听器

contentEditText.setOnFocusChangeListener(notePresenter);
labelEditText.addTextChangedListener(notePresenter);

此处对文本改变和焦点改变设置了监听,分别用于控制菜单显示和Toolbar标题:

  • 文本改变
 String content = contentSrc.replaceAll("\\\\s*|\\t|\\r|\\n", "");
 if (!TextUtils.isEmpty(content))
     if (TextUtils.equals(labelSrc, note.getLabel()) && TextUtils.equals(contentSrc, note.getContent()))
         view.setDoneMenuItemVisible(false);
         return;
     
     view.setDoneMenuItemVisible(true);
 else
     view.setDoneMenuItemVisible(false);
 
  • 焦点改变
if (hasFocus)
    view.setToolbarTitle(R.string.edit_note);

4.5 更多细节

4.5.1 键盘显示与隐藏

@Override
public void hideKeyBoard()
    hideKeyBoard(labelEditText);

@Override
public void showKeyBoard()
    InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
    inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);

private void hideKeyBoard(EditText editText)
    InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
    inputMethodManager.hideSoftInputFromWindow(editText.getWindowToken(), 0);

通过重写方法,再调用InputMethodManager实现了键盘的显示与隐藏。

4.5.2 提示用户保存

通过显示对话框可以友好地提示用户将笔记保存。

@Override
public void showNotSaveNoteDialog()
    AlertDialog.Builder builder = DialogUtils.makeDialogBuilder(this);
    builder.setTitle(R.string.not_save_note_leave_tip);
    builder.setPositiveButton(R.string.sure, notePresenter);
    builder.setNegativeButton(R.string.cancel, notePresenter);
    builder.show();

4.6 NotePresenter

NotePresenter是笔记操作界面与笔记操作逻辑之间的桥梁,一如既往,使用Dagger2点@Inject注解标记构造函数表示此构造函数需要被注入构造所需要的参数,相信到了这里,读者对Dagger有了一定初步的认识。同时要注意的是使用Dagger2依赖注入中,我们不需要对Presenter进行实例化,框架会自动替我们完成这些操作的。

4.6.1 孤注一掷

@Override
public void onDestroy() 
    if (event != null)
        EventBus.getDefault().post(event);
    
    EventBus.getDefault().unregister(this);

在Activity销毁时,使用了EventBus将最后的消息发送出去,消息包含新建的笔记的类型(新建类型或修改类型),以及笔记的具体内容(笔记对象)。这时MainPresenter中的EventBus将会收到消息,并且将收到的消息放到主Activity中进行展示。

4.6.2 保存笔记

刚刚说到将最后的消息发出去,那么最后的消息是怎么得来的呢?这就回到保存笔记方法的逻辑实现了:

private void saveNote()
    view.hideKeyBoard();
    if (TextUtils.isEmpty(view.getLabelText()))
        note.setLabel(mContext.getString(R.string.default_label));
    else 
        note.setLabel(view.getLabelText());
    
    note.setContent(view.getContentText());
    note.setLastOprTime(TimeUtils.getCurrentTimeInLong());
    note.setStatus(SNote.Status.NEED_PUSH.getValue());
    event = new MainPresenter.NotifyEvent<>();
    switch (operateMode)
        case CREATE_NOTE_MODE:
            note.setCreateTime(TimeUtils.getCurrentTimeInLong());
            event.setType(MainPresenter.NotifyEvent.CREATE_NOTE);
            mFinalDb.saveBindId(note);
            break;
        default:
            event.setType(MainPresenter.NotifyEvent.UPDATE_NOTE);
            mFinalDb.update(note);
            break;
    
    event.setData(note);
    view.finishView();

代码比较简单易懂,没有逐行分析的必要。此处注意的是aFinalDb中的方法 boolean saveBindId(Object entity) 可以直接保存对象到数据库。

4.6.3 时间的格式化

private String getOprTimeLineText(SNote note)
    if (note == null || note.getLastOprTime() == 0)
        return "";
    String create = mContext.getString(R.string.create);
    String edit = mContext.getString(R.string.last_update);
    StringBuilder sb = new StringBuilder();
    if (note.getLastOprTime() <= note.getCreateTime() || note.getCreateTime() == 0)
        sb.append(mContext.getString(R.string.note_log_text, create, TimeUtils.getTime(note.getLastOprTime())));
        return sb.toString();
    
    sb.append(mContext.getString(R.string.note_log_text, edit, TimeUtils.getTime(note.getLastOprTime())));
    sb.append("\\n");
    sb.append(mContext.getString(R.string.note_log_text, create, TimeUtils.getTime(note.getCreateTime())));
    return sb.toString();

这段代码的逻辑就是传入笔记对象,如果没有创建时间(笔者猜想或许因为从备份恢复笔记内容或是其它原因会导致笔记创建的时间消失)就只显示最后一次的修改时间,否则就都进行显示,这里用到了TimeUtils。

4.6.3.1 TimeUtils

TimeUtils是对时间相关操作的一个封装,其中有一个有意思的方法,就是将时间做一个人性化的格式化,即格式化为”刚刚”、”半小时前”、”3天前”等:

public static String getConciseTime(long timeInMillis, long nowInMillis, Context context) 
    if (context == null)
        return "";
    long diff = nowInMillis - timeInMillis;
    if (diff >= YEAR_Millis)
        int year = (int)(diff / YEAR_Millis);
        return context.getString(R.string.before_year, year);
    
    ...
    if (diff >= HALF_HOUR_Millis)
        return context.getString(R.string.before_half_hour);
    
    return context.getString(R.string.just_now);

4.6.4 保存按钮的隐现

对于保存按钮的自动隐现,实现思路就是判断文本是否改变即可:

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) 
    if (view.isDoneMenuItemNull())
        return;
    String labelSrc = view.getLabelText();
    String contentSrc = view.getContentText();
    //String label = labelSrc.replaceAll("\\\\s*|\\t|\\r|\\n", "");
    String content = contentSrc.replaceAll("\\\\s*|\\t|\\r|\\n", "");
    if (!TextUtils.isEmpty(content))
        if (TextUtils.equals(labelSrc, note.getLabel()) && TextUtils.equals(contentSrc, note.getContent()))
            view.setDoneMenuItemVisible(false);
            return;
        
        view.setDoneMenuItemVisible(true);
    else
        view.setDoneMenuItemVisible(false);

不过要注意的是要过滤掉转义字符,此处用的正则表达式进行替换过滤。

5. 设置界面

转眼就来到了设置界面,还是老套路(这世间多点真诚,少点套路可好),初始化注射器,不过这次没有立马就发现Presenter在哪里,先不急,继续往下读。

5.1 初始化视图

SettingActivity中的代码少的可怜,看来看去唯有SettingFragment值得一析:

private void init()
    SettingFragment settingFragment = SettingFragment.newInstance();
    getFragmentManager().beginTransaction().replace(R.id.fragment_content, settingFragment).commit();

注意id fragment_content 是布局文件中仅存的FrameLayout的id。

5.1.1 碎片

初始化视图方法中,将FrameLayout替换为了SettingFragment,那我们现在就看看SettingFragment到底是怎么一回事。

5.1.1.1 承接关系

public class SettingFragment extends PreferenceFragment implements SettingView
    ...

SettingFragment继承自SettingView,SettingView中包含有对每个设置项进行设置的方法定义:

public interface SettingView extends View 
    void findPreference();
    void setRightHandModePreferenceChecked(boolean checked);
    void setCardLayoutPreferenceChecked(boolean checked);
    void setFeedbackPreferenceSummary(CharSequence c);
    void setFeedbackPreferenceClickListener(Preference.OnPreferenceClickListener l);
    void setEverNoteAccountPreferenceSummary(CharSequence c);
    void setEverNoteAccountPreferenceTitle(CharSequence c);
    void initPreferenceListView(android.view.View view);
    void showSnackbar(@StringRes int message);
    void showThemeChooseDialog();
    boolean isResume();
    void showUnbindEverNoteDialog();
    void toast(@StringRes int message);
    void reload();

5.1.1.2 绑定Activity

@Override
public void onAttach(Activity activity) 
    super.onAttach(activity);
    if (getActivity() != null && getActivity() instanceof SettingActivity)
        this.activity = (SettingActivity)getActivity();
    

在Fragment绑定中,对Fragment中的Activity成员变量进行初始化,防止每次调用getActivity造成性能损失。

5.1.1.3 设置项初始化

@Override
public void onCreate(Bundle savedInstanceState) 
    super.onCreate(savedInstanceState);
    initializeDependencyInjector();
    addPreferencesFromResource(R.xml.prefs);
    getPreferenceManager().setSharedPreferencesName(PREFERENCE_FILE_NAME);
    initializePresenter();
    settingPresenter.onCreate(savedInstanceState);

在onCreate()中,对菜单项进行了初始化,

5.1.1.3.1 PreferenceScreen

xml文件中使用了 com.jenzz.materialpreference 第三方库,同样是为了让低版本同样拥有Material效果:

此处代码只展示部分:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<com.jenzz.materialpreference.PreferenceCategory android:title="@string/general_settings">
    <com.jenzz.materialpreference.CheckBoxPreference
        android:key="@string/right_hand_mode_key"
        android:title="@string/right_hand_mode"
        android:summary="@string/right_hand_mode_summary"/>
    </com.jenzz.materialpreference.PreferenceCategory>
</PreferenceScreen>
5.1.1.3.2 设定文件

通过PreferenceManager中的setSharedPreferencesName(PREFERENCE_FILE_NAME)方法可以设定配置文件的储存位置。

5.1.1.3.3 Presenter

此处终于见到了设置界面的Presenter。

private void initializePresenter() 
    settingPresenter.attachView(this);
5.1.1.3.4 findPreference
@Override
public void findPreference() 
    rightHandModePreference = (CheckBoxPreference)findPreference(getString(R.string.right_hand_mode_key));
    cardLayoutPreference = (CheckBoxPreference)findPreference(getString(R.string.card_note_item_layout_key));
    feedbackPreference = (Preference)findPreference(getString(R.string.advice_feedback_key));
    everAccountPreference = (Preference)findPreference(getString(R.string.ever_note_account_key));
    payMePreference = (Preference)findPreference(getString(R.string.pay_for_me_key));
    giveFavorPreference = (Preference)findPreference(getString(R.string.give_favor_key));

与Activity中的findViewById()类似,Preference也类似,需要查找到每个控件,只不过此处通过 key 属性来进行查找。

5.1.1.4 对外提供方法

@Override
public void setRightHandModePreferenceChecked(boolean checked) 
    rightHandModePreference.setChecked(checked);

...

之后,对外提供了设置各个设置项的方法。

5.1.1.5 ListView自定义

@Override
public void initPreferenceListView(View view) 
    ListView listView = (ListView)view.findViewById(android.R.id.list);
    listView.setHorizontalScrollBarEnabled(false);
    listView.setVerticalScrollBarEnabled(false);
    listView.setDivider(new ColorDrawable(getResources().getColor(R.color.grey)));
    listView.setDividerHeight((int) getResources().getDimension(R.dimen.preference_divider_height));
    listView.setFooterDividersEnabled(false);
    listView.setHeaderDividersEnabled(false);

之后,通过view.findViewById(android.R.id.list)获取ListView,可以对该ListView进行自定义。如图为笔者加入垂直滚动条后的效果:

view为重写SettingFragment下的onViewCreated(View view, Bundle savedInstanceState)方法中的参数。

5.2 主题切换对话框

@Override
public void showThemeChooseDialog()
    AlertDialog.Builder builder = DialogUtils.makeDialogBuilder(activity);
    builder.setTitle(R.string.change_theme);
    Integer[] res = new Integer[]R.drawable.red_round, R.drawable.brown_round, R.drawable.blue_round,
            R.drawable.blue_grey_round, R.drawable.yellow_round, R.drawable.deep_purple_round,
            R.drawable.pink_round, R.drawable.green_round;
    List<Integer> list = Arrays.asList(res);
    ColorsListAdapter adapter = new ColorsListAdapter(getActivity(), list);
    adapter.setCheckItem(ThemeUtils.getCurrentTheme(activity).getIntValue());
    GridView gridView = (GridView)LayoutInflater.from(activity).inflate(R.layout.colors_panel_layout, null);
    gridView.setStretchMode(GridView.STRETCH_COLUMN_WIDTH);
    gridView.setCacheColorHint(0);
    gridView.setAdapter(adapter);
    builder.setView(gridView);
    final AlertDialog dialog = builder.show();
    gridView.setOnItemClickListener((parent, view, position, id) -> 
        dialog.dismiss();
        settingPresenter.onThemeChoose(position);
    );

5.2.1 主题颜色子项

我们来仔细分析一下此段代码,首先将每个颜色的资源id存放入了list集合,颜色资源的代码如下,此处只展示红色:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="oval">
    <solid android:color="@color/red"/>
</shape>

接下来,使用了ColorsListAdapter作为GridView的适配器,之后很巧妙地使用两张图片重合的FrameLayout作为一个子项实现了主题选择的效果:

在打开对话框时,还需要通过读取配置文件来设置当前所应用的主题。

5.2.2 网格视图

网格视图使用LayoutInflater来进行了初始化,其布局如下:

<GridView xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:numColumns="4"
    android:padding="@dimen/md_dialog_frame_margin"
    android:background="@android:color/transparent"
    android:listSelector="@android:color/transparent"
    android:horizontalSpacing="20dp"
    android:verticalSpacing="20dp"
    android:layout_height="match_parent">
</GridView>

要注意设置背景和所选项的颜色为透明

gridView.setStretchMode(GridView.STRETCH_COLUMN_WIDTH);
gridView.setCacheColorHint(0);
gridView.setAdapter(adapter);

接下来对gridView设置了伸展模式等。

5.2.3 点击事件

最后对其设定点击事件,实现主题切换。

gridView.setOnItemClickListener((parent, view, position, id) -> 
    dialog.dismiss();
    settingPresenter.onThemeChoose(position);
);

其中切换后会调用reload()方法,以实现无动画地重启Activity,之前BaseActivity中已有提及。

5.3 SettingPresenter

5.3.1 onCreate()

@Override
public void onCreate(Bundle savedInstanceState) 
    EventBus.getDefault().register(this);
    view.findPreference();
    initOtherPreference();
    initFeedbackPreference();
    initEverAccountPreference();

onCreate()中调用了之前提及的findPreference操作,并对设置项进行了初始化,此处展示部分初始化:

private void initOtherPreference()
    isCardLayout = mPreferenceUtils.getBooleanParam(getString(mContext,
            R.string.card_note_item_layout_key), true);
    isRightHandMode = mPreferenceUtils.getBooleanParam(getString(mContext,
            R.string.right_hand_mode_key));
    view.setCardLayoutPreferenceChecked(isCardLayout);
    view.setRightHandModePreferenceChecked(isRightHandMode);

5.3.2 邮件发送

关于意见反馈功能,此应用只是简单使用了发送邮件作为反馈方式,代码不难:

private void initFeedbackPreference()
    Uri uri = Uri.parse("mailto:lgpszu@163.com");
    final Intent intent = new Intent(Intent.ACTION_SENDTO, uri);
    PackageManager pm = mContext.getPackageManager();
    List<ResolveInfo> infos = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
    if (infos == null || infos.size() <= 0)
        view.setFeedbackPreferenceSummary(mContext.getString(R.string.no_email_app_tip));
        return;
    
    Preference.OnPreferenceClickListener l = (preference -> 
        mContext.startActivity(intent);
        return true;
    );
    view.setFeedbackPreferenceClickListener(l);

值得学习的是通过PackageManager查询符合要求的Intent应用。

5.3.3 备份笔记

private void backupLocal() 
    //已经备份中,直接返回
    if (backuping)
        return;
    backuping = true;
    PermissionRequester.getInstance(mContext).
            request(new PermissionRequester.RequestPermissionsResultCallBackImpl() 
                @Override
                public void onRequestPermissionsResult(String[] permission, int[] grantResult) 
                    if (grantResult[0] != PackageManager.PERMISSION_GRANTED) 
                        view.showSnackbar(R.string.backup_local_fail);
                        return;
                    
                    mObservableUtils.backNotes(mContext, mFinalDb, mFileUtils)
                            .subscribeOn(Schedulers.io())
                            .observeOn(AndroidSchedulers.mainThread())
                            .subscribe((success) -> 
                                view.showSnackbar(R.string.backup_local_done);
                                backuping = false;
                            , (e) -> 
                                view.showSnackbar(R.string.backup_local_fail);
                                backuping = false;
                            );
                
            , Manifest.permission.WRITE_EXTERNAL_STORAGE);

首先作者使用到了自己封装的一个权限申请类,用于Android 6.0的动态申请权限,在申请成功后再通过RxJava备份笔记。
请求权限的核心代码为:

private void handleIntent(Intent intent) 
    String[] permissions = intent.getStringArrayExtra("permissions");
    ActivityCompat.requestPermissions(this, permissions, 0x44);

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) 
    if (requestCode == 0x44)
        PermissionRequester.getInstance(this).onRequestPermissionsResult(permissions, grantResults);
    
    finish();

5.3.4 应用评分

private void giveFavor() 
    try 
        Uri uri = Uri.parse("market://details?id=" + mContext.getPackageName());
        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        mContext.startActivity(intent);
     catch (ActivityNotFoundException e) 
        e.printStackTrace();
    

通过以上代码可以打开应用商店搜索此应用,便于用户对此应用评分。

5.3.5 打赏模块

if (TextUtils.equals(key, getString(mContext, R.string.pay_for_me_key)))
    Intent intent = new Intent(mContext, PayActivity.class);
    mContext.startActivity(intent);

在onPreferenceTreeClick(Preference preference)方法中进行了操作判断,此处还有一项是PayActivity,即支付。
比较简单,就是放一个支付宝二维码图片和文本框,不过这里要说的它的文本框文字是居中的,是因为使用了 android:textAppearance="?android:attr/textAppearanceMedium" 属性。

6. AboutActivity


如图展示的就是关于界面,关于界面终于没有使用Presenter了,所以代码看起来也不那么绕了,不过要承认的是代码的逻辑性确实不如之前那么强了。

6.1 初始化视图

视图初始化其实没什么说的,先看布局文件。

6.1.1 布局文件

<ScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scroll_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fadingEdge="none"
    android:scrollbars="none"
    android:cacheColorHint="@android:color/transparent"
    android:scrollingCache="false"
    android:overScrollMode="never">
...
</ScrollView>

线性布局下就是一个ScrollView,可以兼容小屏幕手机,同时还能默认隐藏版权说明。还有就是涟漪按钮的实现,其实是使用第三方容器包裹住Button即可:

<com.balysv.materialripple.MaterialRippleLayout
    android:layout_marginTop="120dp"
    andr

以上是关于《极简笔记》源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Nav2极简笔记01-安装与试用

Nav2极简笔记03-启动文件launch

Nav2极简笔记02-导航概念

群体智能中的联邦学习算法综述---简笔记(除安全类)

群体智能中的联邦学习算法综述---简笔记(除安全类)

死磕 java集合之TreeMap源码分析-内含彩蛋