《极简笔记》源码分析

0. 介绍

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

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

1. Manifest入手

1.1 权限

1
2
3
4
5
<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层

1
android:name=".App"

项目结构

在Application层发现了一个奇怪的属性,然后又发现项目结构目录中有个继承自Application的类,顿时疑惑。经查阅后又联想到包建强的《App研发录》中提到彻底结束安卓程序进程需要用到继承Application的类来记录已经打开的Activity,然后统一结束它们,如代码所示:

1
2
3
4
5
public class App extends Application {

public List<Activity> activities=new ArrayList<Activity>();

}

Manifest进行注册:

1
2
3
4
<application
android:icon="@drawable/icon"
android:label="@string/app_name"
android:name=".App" >

每个Activity中的做法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//首先: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类

类中定义了以下方法:

1
2
3
4
5
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的使用

第一步: 添加编译和运行库

1
2
3
4
5
dependencies {
apt 'com.google.dagger:dagger-compiler:2.0'
compile 'com.google.dagger:dagger:2.0'
...
}

第二步: 构建依赖

1
2
3
4
5
6
7
@Module
public class ActivityModule {

@Provides UserModel provideUserModel() {
return new UserModel();
}
}

第三步: 构建Injector

1
2
3
4
@Component(modules = ActivityModule.class)
public interface ActivityComponent {
void inject(MainActivity activity);
}

第三步: 完成依赖注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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,如图:
主Activity界面
此外,还有一点值得注意:

1.3.1 搜索功能的使用方法

搜索有两种实现方式,默认搜索框(比如Toolbar上面的)和搜索控件(可以在Layout里面声明的SearchView),一般采用默认的搜索框方式即可,此处也只简单讲讲此方式,如要了解更多可以去阅读官方文档的创建搜索界面

1.3.1.1 创建搜索配置文件

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

1
2
3
4
5
<?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,另一个是搜索框的配置文件地址:

1
2
3
4
5
6
7
8
9
10
<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等展示结果

此处展示接收查询的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
@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代表成功消耗此请求,示例代码如下:

1
2
3
4
5
6
7
@Override 
public boolean onSearchRequested() {
Bundle appData = new Bundle();
appData.putBoolean(SearchableActivity.JARGON, true);
startSearch(null, false, appData, false);
return true;
}

1
2
3
4
5
// startSearch()中
Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA);
if (appData != null) {
boolean jargon = appData.getBoolean(SearchableActivity.JARGON);
}

2. 攻入MainActivity

2.1 ButterKnife

1
2
3
4
5
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 使用方法

  1. 导库
    下载jar包导入或者直接在gradle中加上 compile 'com.jakewharton:butterknife:7.0.1'即可
  2. @BindButterKnife.bind(Activity act);
    看如下一段代码就能明白如何使用:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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 基类和接口

1
2
3
public class MainActivity extends BaseActivity implements MainView{
...
}

从这里,可以进入基类BaseActivity和接口MainView看看。

2.2.1 重写Activity生命周期的BaseActivity

1
2
3
4
5
6
7
8
9
10
11
12
@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过渡动画的使用方法:

1
overridePendingTransition(R.anim.activity_down_up_anim, R.anim.activity_exit_anim);

xml中定义的动画:

1
2
3
4
5
6
7
<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)方法实现的。

1
2
int style = R.style.RedTheme;
activity.setTheme(style);

styles中定义了多种样式:

1
2
3
4
5
<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的状态栏”沉浸模式”

1
2
3
4
5
6
7
8
9
10
@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() 抽象方法。

1
2
@Override
protected int getLayoutView() { return R.layout.activity_main; }

通过这里我们也就又发现了新大陆,哦不,新道路,通往Activity布局文件的道路。

2.2.1.6 Toolbar初始化

由于各Activity中toolbar都一样,所以这里就将其抽取出来了,布局文件中使用 <include> 标签抽取,Activity中抽取出来一个ToolbarUtils类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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切换主题解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
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里的共有方法进行抽象。

1
2
3
4
5
6
7
8
9
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去执行。

1
2
3
4
5
6
7
8
9
10
// 初始化依赖注入
@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()方法,以独具一格。

1
2
3
4
5
6
7
@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多个是可以省略为一个的,并不会影响程序的执行,但为了代码的可读性,还是应该写成多个。

1
2
3
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

1
2
3
4
5
<com.lguipeng.notes.view.FixedRecyclerView
android:id="@+id/recyclerView"
android:padding="4dp"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

这个是作者的一个修正后的RecyclerView控件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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进行了初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@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中自带效果,用法代码如下:

1
2
3
4
5
6
7
8
9
<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了一个阴影效果布局:

1
2
3
4
5
<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:

1
2
3
4
5
6
<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而是通过隐藏和显示模块实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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包含了几个不常用的属性,值得一看。

1
2
3
4
5
6
7
8
<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: 选择模式: 多选和单选,默认不设定,此处单选便于用户知道自己所在的选项卡,如图所示:

None模式单选模式

  • divider: 分隔线
  • dividerHeight: 分隔线高度

分析完主布局,继续回到MainActivity。

2.6 NotesAdapter

首先回到之前提到了RecyclerView,其中的NotesAdapter是一个比较重要的东西,关乎着笔记列表的展示和操作。

2.6.1 承接关系

1
2
3
public class NotesAdapter extends BaseRecyclerViewAdapter<SNote> implements Filterable {
...
}

继承自BaseRecyclerViewAdapter,而BaseRecyclerViewAdapter才继承自真正应该继承的RecyclerView.Adapter,同时实现了Filterable接口,便于笔记列表的过滤操作,笔者猜测是用于搜索功能的过滤操作,那接下来就先分析其父类BaseRecyclerViewAdapter。

2.6.1.1 BaseRecyclerViewAdapter

2.6.1.1.1 增删改方法

在BaseRecyclerViewAdapter中,首先是增加了对传入List的增删改方法,此处只贴上增加的方法:

1
2
3
4
public void add(E e) {
this.list.add(0, e);
notifyItemInserted(0);
}

此处notifyItemInserted(int position)方法是用于通知RecyclerView有新的数据增加,对于不使用notifyDataSetChanged()方法,笔者猜测是为了防止刷新数据时列表跳回到表首。

2.6.1.1.2 内部点击事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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()中获得的所有动画效果:

1
2
3
4
5
6
7
8
9
10
11
12
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进行实现:

1
2
3
4
5
6
7
8
9
10
11
12
@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

1
2
3
4
5
6
@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。
首先看构造函数:

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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 过滤操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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,继承结构如图:
DrawerListAdapter类继承结构图

Android Studio中按快捷键F4查看类继承结构图

2.7.1.1 BaseListAdapter

与BaseRecyclerViewAdapter类似,同样包含需要传入参数进行初始化操作的列表,以及增删改方法,以及回调的点击事件接口。

2.7.1.2 SimpleListAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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

1
2
@Override
protected int getLayout() { return R.layout.drawer_list_item_layout; }
2.7.1.3.1 布局

布局仅用了一个简洁的TextView,但TextView中包含了几个不常见的属性:

1
2
3
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); 为抽屉加上开关抽屉的监听。对监听器的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 设置抽屉遮帘颜色

1
mDrawerLayout.setScrimColor(getCompactColor(R.color.drawer_scrim_color));

此处放上设置遮帘为蓝色后的效果图:
遮帘颜色配置

2.8 PopupMenu

在每个CardView上面需要显示菜单,包含”编辑”和”回收”,显示PopupMenu方法如下:

1
2
3
4
5
6
7
8
@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中新增搜索项:

1
2
3
4
5
6
7
<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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@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 处理菜单事件

1
2
3
4
5
6
7
8
9
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 处理实体按键事件

1
2
3
4
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return mainPresenter.onKeyDown(keyCode) || super.onKeyDown(keyCode, event);
}

返回值注意的是先处理传入MainPresentor里面方案,有代码自左至右运行顺序,如果不满足则按父类方法处理,这样写简直精妙,避免了多重if判断。

2.12 删除对话框的显示

1
2
3
4
5
6
7
8
9
10
@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(…)方法:

1
2
3
4
5
6
7
8
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本身调用方法:

1
SnackbarUtils.show(fab, message);

至此,庞大的MainActivity算是分析得差不多了,那么接下来便啃另一块大骨头——MainPresenter。

3. Presenter —— MVP中的桥梁

3.1 接口

为了将逻辑放到P层中,MainPresenter继承了多个接口。

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

由于其它接口前文已有提及,此处只展示和分析Presenter接口。
Presenter接口将Activity的生命周期抽象出来,并且通过attachView将Activity传入,用于MainPresenter的初始化。

1
2
3
4
5
6
7
8
9
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 构造函数

1
2
3
4
5
6
7
8
9
@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 元注解

  1. @Target 所修饰的对象范围
  2. @Retention 该Annotation被保留的时间长短
  3. @Documented 可以被例如javadoc此类的工具文档化
  4. @Inherited 被标注的类型是被继承的

3.2.1.4 自定义注解

定义注解格式:
  public @interface 注解名 {定义体}
例如:

1
2
3
4
5
6
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.TYPE)
@Documented
public @interface Description {
String value();
}

3.2.1.5 对项目中”ContextLifeCycle”的分析

1
2
3
4
5
6
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface ContextLifeCycle {
String value() default "App";
}

@Qualifier: 限定注释符
因为ContextLifeCycle的实现需要反射,所以我们暂时不能跟踪到它的实现,所以暂时先不作实现分析。

3.2.2 FinalDb参数

FinalDb是一个比较庞大的类,是aFinal第三方库的一个子模块,主要是数据库操作,接下来让我们大致了解一下它。

3.2.2.1 数据库连接池

1
2
3
4
5
6
7
8
9
private static HashMap<String, FinalDb> daoMap = new HashMap<String, FinalDb>();
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主要包含以下属性:

1
2
3
4
5
6
7
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 构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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内部类
1
2
3
class SqliteDbHelper extends SQLiteOpenHelper {
...
}

此类继承SQLiteOpenHelper获取安卓默认的数据库。

3.2.2.3 公有的create()方法

create方法重载了多个,配合使用一个或多个参数调用的情况。例如其中之一:

1
2
3
4
5
6
7
8
9
10
11
12
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类,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    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即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    FinalDb db = FinalDb.create(this);
    User user = new User();
    user.setEmail("...");
    user.setName("...");
    user.setRegisterDate(new Date());
    db.save(user);
    List<User> 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接口

1
2
3
public interface Fun<T> {
T call() throws Exception;
}

内部包含一个Fun接口,即为一个函数,每个需要作为参数的函数被包装在实现该接口的类中。如:

1
2
3
4
5
6
7
8
9
10
private class GetEverNoteUserFun implements Fun<User>{
private EverNoteUtils mEverNoteUtils;
public GetEverNoteUserFun(EverNoteUtils mEverNoteUtils) {
this.mEverNoteUtils = mEverNoteUtils;
}
@Override
public User call() throws Exception{
return mEverNoteUtils.getUser();
}
}

这样函数就能作为一个参数被传递到其它函数中。如:

1
create(new GetEverNoteUserFun(everNoteUtils))

此处将 方法 称作 函数 是为了便于理解。

3.2.5 EverNoteUtils参数

EverNoteUtils就是调用印象笔记的API,包含推送笔记和获取笔记等方法,这里可以看印象笔记官方提供的API,此处不作分析。

3.3 onCreate()方法

3.3.1 Activity崩溃的信息保存

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

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

然后再onCreate中恢复信息:

1
2
3
4
if (savedInstanceState != null){
int value = savedInstanceState.getInt(CURRENT_NOTE_TYPE_KEY);
mCurrentNoteTypePage = SNote.NoteType.mapValueToStatus(value);
}

3.3.2 初始化视图

1
2
3
4
5
view.initToolbar();
initDrawer();
initMenuGravity();
initItemLayoutManager();
initRecyclerView();

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

3.3.2.1 initToolbar

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

3.3.2.2 初始化抽屉

1
2
3
4
5
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中定义数组:

1
2
3
4
<array name="drawer_content">
<item>SNotes</item>
<item>回收站</item>
</array>

3.3.2.3 设置抽屉方向

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

1
2
3
4
5
6
@Override
public void setMenuGravity(int gravity) {
DrawerLayout.LayoutParams params = (DrawerLayout.LayoutParams) drawerRootView.getLayoutParams();
params.gravity = gravity;
drawerRootView.setLayoutParams(params);
}

3.3.2.4 设置RecyclerView线性或网格排列

1
2
3
4
5
6
7
8
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中进行视图操作:

1
recyclerView.setLayoutManager(manager);

3.3.2.5 加载笔记

1
2
3
4
5
6
7
8
9
10
11
12
13
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对象,此处截取了关键部分:

1
2
3
4
5
6
7
8
9
10
11
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():

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

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

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

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

3.3.2.5.2 指定线程
1
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库

1
EventBus.getDefault().register(this);

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

3.3.3.1 介绍

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

3.3.3.2 使用

  • 导入库

    1
    compile 'org.greenrobot:eventbus:3.0.0'
  • 编写消息类,传递的消息是一个对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class FirstEvent {

    private String mMsg;
    public FirstEvent(String msg) {
    mMsg = msg;
    }
    public String getMsg(){
    return mMsg;
    }
    }
  • 注册与反注册
    在onCreate方法中进行注册:

    1
    EventBus.getDefault().register(this);

在onDestroy方法中进行反注册:

1
EventBus.getDefault().unregister(this);

  • 发送消息

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

    1
    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 消息接收——同步笔记

1
2
3
4
5
6
7
8
9
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操作,同时,消息的参数很巧妙——使用了一个枚举,作为参数,可以集合多种情况。

1
2
3
4
5
public enum SyncResult{
START,
...
SUCCESS
}

3.3.3.3.3 消息接收——其他操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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平滑地滚动到指定位置

    1
    2
    3
    4
    @Override
    public void scrollRecyclerViewToTop() {
    recyclerView.smoothScrollToPosition(0);
    }
  • 重建Activity

    1
    2
    3
    4
    @Override
    public void reCreate() {
    super.recreate();
    }

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

3.4 onResume()

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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 返回监听

1
2
3
4
5
6
7
8
9
10
11
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 同步笔记

1
2
3
4
5
6
7
8
9
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()方法设定操作方式。

1
2
3
4
5
6
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initializePresenter();
notePresenter.onCreate(savedInstanceState);
}

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

4.2 布局文件

新建笔记界面

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

  • 焦点拦截
    1
    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 动态修改菜单

1
2
3
4
5
6
7
8
9
10
11
12
13
@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 监听器

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

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

  • 文本改变

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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);
    }
  • 焦点改变

    1
    2
    3
    if (hasFocus){
    view.setToolbarTitle(R.string.edit_note);
    }

4.5 更多细节

4.5.1 键盘显示与隐藏

1
2
3
4
5
6
7
8
9
10
11
12
13
@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 提示用户保存

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

1
2
3
4
5
6
7
8
@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 孤注一掷

1
2
3
4
5
6
7
@Override
public void onDestroy() {
if (event != null){
EventBus.getDefault().post(event);
}
EventBus.getDefault().unregister(this);
}

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

4.6.2 保存笔记

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 时间的格式化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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天前”等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 保存按钮的隐现

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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值得一析:

1
2
3
4
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 承接关系

1
2
3
public class SettingFragment extends PreferenceFragment implements SettingView{
...
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

1
2
3
4
5
6
7
@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 设置项初始化

1
2
3
4
5
6
7
8
9
@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效果:
layout预览
此处代码只展示部分:

1
2
3
4
5
6
7
8
9
<?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。

1
2
3
private void initializePresenter() {
settingPresenter.attachView(this);
}

5.1.1.3.4 findPreference
1
2
3
4
5
6
7
8
9
@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 对外提供方法

1
2
3
4
5
@Override
public void setRightHandModePreferenceChecked(boolean checked) {
rightHandModePreference.setChecked(checked);
}
...

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

5.1.1.5 ListView自定义

1
2
3
4
5
6
7
8
9
10
@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 主题切换对话框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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集合,颜色资源的代码如下,此处只展示红色:

1
2
3
4
<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来进行了初始化,其布局如下:

1
2
3
4
5
6
7
8
9
10
<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>

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

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

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

5.2.3 点击事件

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

1
2
3
4
gridView.setOnItemClickListener((parent, view, position, id) -> {
dialog.dismiss();
settingPresenter.onThemeChoose(position);
});

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

5.3 SettingPresenter

5.3.1 onCreate()

1
2
3
4
5
6
7
8
@Override
public void onCreate(Bundle savedInstanceState) {
EventBus.getDefault().register(this);
view.findPreference();
initOtherPreference();
initFeedbackPreference();
initEverAccountPreference();
}

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

1
2
3
4
5
6
7
8
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 邮件发送

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 备份笔记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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备份笔记。
请求权限的核心代码为:

1
2
3
4
5
6
7
8
9
10
11
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 应用评分

1
2
3
4
5
6
7
8
9
10
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 打赏模块

1
2
3
4
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 布局文件

1
2
3
4
5
6
7
8
9
10
11
12
<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即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<com.balysv.materialripple.MaterialRippleLayout
android:layout_marginTop="120dp"
android:layout_marginLeft="30dp"
android:layout_marginRight="30dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:mrl_rippleOverlay="true"
app:mrl_rippleColor="?attr/colorPrimary">

<Button
android:background="@drawable/white_button_background"
android:id="@+id/blog_btn"
android:textColor="?attr/colorPrimary"
android:text="@string/jianshu_blog"
android:textSize="@dimen/abc_text_size_button_material"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

</com.balysv.materialripple.MaterialRippleLayout>

6.1.2 版本信息

1
2
3
4
5
6
7
8
9
10
private String getVersion(Context ctx){
try {
PackageManager pm = ctx.getPackageManager();
PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES);
return pi.versionName;
}catch (PackageManager.NameNotFoundException e){
e.printStackTrace();
}
return "1.0.0";
}

通过以上代码便可获得版本信息。

6.1.3 连续敲击彩蛋

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@OnClick(R.id.version_text)
void versionClick(View view){
if (clickCount < 3){
if (TimeUtils.getCurrentTimeInLong() - lastClickTime < 500 || lastClickTime <= 0){
clickCount ++;
if (clickCount >= 3){
startViewAction(BuildConfig.ABOUT_APP_URL);
clickCount = 0;
lastClickTime = 0;
return;
}
}else {
clickCount = 0;
lastClickTime = 0;
return;
}
lastClickTime = TimeUtils.getCurrentTimeInLong();
}
}

通过连续地快速点击三次,可以执行彩蛋,就是跳转至作者的博客。

6.2 分享对话框

在笔者看来,关于界面真正的大头是这个分享对话框,如图所示。
分享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private void showShareDialog(){
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.ShareDialog);
builder.setTitle(getString(R.string.share));
final MaterialSimpleListAdapter adapter = new MaterialSimpleListAdapter(this);
String[] array = getResources().getStringArray(R.array.share_dialog_text);
adapter.add(new ShareListItem.Builder(this)
.content(array[0])
.icon(R.drawable.ic_wx_logo)
.build());
...
builder.setAdapter(adapter, (dialog, which) -> {
switch (which) {
case 0:
shareToWeChatSession();
break;
...
default:
share("", null);
}
});
AlertDialog dialog = builder.create();
Window window = dialog.getWindow();
window.setGravity(Gravity.BOTTOM);
WindowManager.LayoutParams lp = window.getAttributes();
Display display = getWindowManager().getDefaultDisplay();
Point out = new Point();
display.getSize(out);
lp.width = out.x;
window.setAttributes(lp);
View decorView = window.getDecorView();
decorView.setBackgroundColor(getResources().getColor(R.color.window_background));
dialog.setOnShowListener((dialog1 -> {
Animator animator = ObjectAnimator
.ofFloat(decorView, "translationY", decorView.getMeasuredHeight() / 1.5F, 0);
animator.setDuration(200);
animator.start();
}));
dialog.show();
}

值得一提的还有对话框的样式:

1
2
3
4
5
6
<style name="ShareDialog" parent="BaseDialogTheme">
<item name="android:windowFrame">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsFloating">true</item>
</style>

但由于分享并不是主要功能,所以这里就不作分析了,感兴趣的读者可以自行深入研究。

总结

到这里,没有煽情的结尾,《极简笔记》应用的源码就基本分析完了,从中看到了作者对MVP、RxJava、依赖注入等运用的淋漓尽致,这里真心感谢作者 lguipeng 提供的此开源项目供我们学习,也感谢读者你的来访和阅读,希望读者也能或多或少从中学到一些东西!最后欢迎读者访问我的其它博客,也都同样精彩!


参考

  1. 项目原地址 lguipeng/Notes
  2. 使用Dagger 2进行依赖注入
  3. 浅谈依赖注入
  4. textAppearance解析
  5. android中xmlns:tools属性详解
  6. 深入浅出Java Annotation(元注解和自定义注解)
  7. 使用android快速开发框架afinal的FinalDb操作android数据库
  8. 给 Android 开发者的 RxJava 详解
  9. EventBus使用详解