《极简笔记》源码分析(一)
2016-04-13 13:27
246 查看
0. 介绍
此文将对Github上lguipeng大神所开发的极简笔记 v2.0(点我下载源码)代码进行分析学习。
通过此文你将学到:
应用源码的研读方法
MVP架构模式
Application的应用
Degger2依赖注入框架
搜索控件的使用
ButterKnife库的使用
Material主题
RecyclerView等新控件的用法
Lambda表达式
Java自定义注解
aFinal框架
1. Manifest入手
1.1 权限
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
声明了网络与储存读写相关权限,至于网络权限笔者猜测应该是用于印象笔记的同步吧。
1.2 Application层
android:name=".App"
![](http://7xqdz8.com1.z0.glb.clouddn.com/160408Project.png)
在Application层发现了一个奇怪的属性,然后又发现项目结构目录中有个继承自Application的类,顿时疑惑。经查阅后又联想到包建强的《App研发录》中提到彻底结束安卓程序进程需要用到继承Application的类来记录已经打开的Activity,然后统一结束它们,如代码所示:
public class App extends Application { public List<Activity> activities=new ArrayList<Activity>(); }
Manifest进行注册:
<application android:icon="@drawable/icon" android:label="@string/app_name" android:name=".App" >
每个Activity中的做法如下:
//首先:onCreate()方法里边: App app = (App) getApplicationContext();// 获取应用程序全局的实例引用 app.activities.add(this); // 把当前Activity放入集合中 //然后:onDestroy()方法里边做法: @Override protected void onDestroy() { super.onDestroy(); App app = (App) getApplication();// 获取应用程序全局的实例引用 app.activities.remove(this); // 把当前Activity从集合中移除 } //最后:在程序中需要结束时的做法: List<Activity> activities = app.activities; for (Activity act : activities) { act.finish();// 显式结束 }
我想此处亦是同样原理。
补充Application相关知识点:
创建一个类继承Application并在manifest的application标签中进行注册
生命周期等于这个程序的生命周期
通常用于数据传递、数据共享、数据缓存等操作
onTerminate() 当终止应用程序对象时调用 onLowMemory() 当后台程序已经终止资源还匮乏时会调用
1.2.1 探索继承自Application的App类
类中定义了以下方法:private void initializeInjector() { mAppComponent = DaggerAppComponent.builder() .appModule(new AppModule(this)) .build(); }
通过DaggerAppComponent可以发现使用了Dagger2库,那么Dagger库又是什么呢?继续探索…
1.2.1.1 Dagger2介绍
在此之前,需要先了解依赖注入,在本人看来其实就是低级类对高级类的依赖关系,它有以下好处:- 依赖的注入和配置独立于组件之外
- 因为对象是在一个独立、不耦合的地方初始化,所以当注入抽象方法的时候,我们只需要修改对象的实现方法,而不用大改代码库
- 依赖可以注入到一个组件中:我们可以注入这些依赖的模拟实现,这样使得测试更加简单
而Dagger2就是Google基于java的依赖注入标准维护的一个库。
1.2.1.1 Dagger2的使用
第一步: 添加编译和运行库dependencies { apt 'com.google.dagger:dagger-compiler:2.0' compile 'com.google.dagger:dagger:2.0' ... }
第二步: 构建依赖
@Module public class ActivityModule { @Provides UserModel provideUserModel() { return new UserModel(); } }
第三步: 构建Injector
@Component(modules = ActivityModule.class) public interface ActivityComponent { void inject(MainActivity activity); }
第三步: 完成依赖注入
public class MainActivity extends ActionBarActivity { private ActivityComponent mActivityComponent; @Inject UserModel userModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mActivityComponent = DaggerActivityComponent.builder().activityModule(new ActivityModule()).build(); mActivityComponent.inject(this); ((TextView) findViewById(R.id.user_desc_line)).setText(userModel.id + "\n" + userModel.name + "\n" + userModel.gender); } ... }
1.3 Activity层
<activity android:name=".ui.MainActivity" android:launchMode="singleTop" android:windowSoftInputMode="adjustResize|stateHidden" android:screenOrientation="portrait"> <meta-data android:name="android.app.searchable" android:resource="@xml/searchable" /> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> <intent-filter> <action android:name="android.intent.action.SEARCH" /> </intent-filter>
由标签里的内容可以看出该Activity是程序启动的主Activity,如图:
![](http://7xqdz8.com1.z0.glb.clouddn.com/160409sc_main.png)
此外,还有一点值得注意:
1.3.1 搜索功能的使用方法
搜索有两种实现方式,默认搜索框(比如Toolbar上面的)和搜索控件(可以在Layout里面声明的SearchView),一般采用默认的搜索框方式即可,此处也只简单讲讲此方式,如要了解更多可以去阅读官方文档的创建搜索界面1.3.1.1 创建搜索配置文件
主要是对搜索框样式的配置,文件保存在res/xml/searchable.xml:
<?xml version="1.0" encoding="utf-8"?> <searchable xmlns:android="http://schemas.android.com/apk/res/android" android:label="@string/app_label" android:hint="@string/search_hint" > </searchable>
1.3.1.2 创建Activity并注册
注册Activity有两个要点,一个是接收Intent.ACTION_SEARCH,另一个是搜索框的配置文件地址:<application ... > <activity android:name=".SearchableActivity" > <intent-filter> <action android:name="android.intent.action.SEARCH" /> </intent-filter> <meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/> </activity> ... </application>
1.3.1.3 执行搜索过程
搜索的执行过程又分为3步:接收查询: 收到Intent数据获取到搜索内容执行搜索
搜索你的资料: 通过SQLite的FTS3方式搜索或进行在线搜索
呈现结果: 使用ListView等展示结果
此处展示接收查询的示例代码:
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.search); // Get the intent, verify the action and get the query Intent intent = getIntent(); if (Intent.ACTION_SEARCH.equals(intent.getAction())) { String query = intent.getStringExtra(SearchManager.QUERY); doMySearch(query); } }
1.3.1.4 进行实时搜索
如果要进行实时搜索,需要在Activity中重写onSearchRequested()方法,返回true代表成功消耗此请求,示例代码如下:@Override public boolean onSearchRequested() { Bundle appData = new Bundle(); appData.putBoolean(SearchableActivity.JARGON, true); startSearch(null, false, appData, false); return true; }
// startSearch()中 Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA); if (appData != null) { boolean jargon = appData.getBoolean(SearchableActivity.JARGON); }
2. 攻入MainActivity
2.1 ButterKnife
public class MainActivity extends BaseActivity implements MainView{ @Bind(R.id.toolbar) Toolbar toolbar; @Bind(R.id.refresher) SwipeRefreshLayout refreshLayout; ... }
打开MainActivity。映入眼帘的是熟悉的ButterKnife,此处回顾一下ButterKnife的使用。
![](http://7xqdz8.com1.z0.glb.clouddn.com/160409ButterKnife.png)
2.1.1 使用方法
导库下载jar包导入或者直接在gradle中加上
compile 'com.jakewharton:butterknife:7.0.1'即可
@Bind和
ButterKnife.bind(Activity act);
看如下一段代码就能明白如何使用:
class ExampleActivity extends Activity { @Bind(R.id.user) EditText username; @Bind(R.id.pass) EditText password; @BindString(R.string.login_error) String loginErrorMessage; @OnClick(R.id.submit) void submit() { // TODO call server... } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.simple_activity); ButterKnife.bind(this); // TODO Use fields... } }
更多使用方法详见官方介绍,JakeWharton/butterknife
2.2 基类和接口
public class MainActivity extends BaseActivity implements MainView{ ... }
从这里,可以进入基类BaseActivity和接口MainView看看。
2.2.1 重写Activity生命周期的BaseActivity
@Override protected void onCreate(Bundle savedInstanceState) { parseIntent(getIntent()); showActivityInAnim(); initTheme(); super.onCreate(savedInstanceState); initWindow(); initializeDependencyInjector(); setContentView(getLayoutView()); ButterKnife.bind(this); initToolbar(); }
通过这样重写生命周期的方式可以使代码更加统一,便于后期管理和维护。
下面就简单分析几个方法:
2.2.1.1 处理数据
通过parseIntent(getIntent());处理传递到Activity的数据,可以进行一些初始化操作。
2.2.1.2 过渡动画
回顾一下Activity过渡动画的使用方法:overridePendingTransition(R.anim.activity_down_up_anim, R.anim.activity_exit_anim);
xml中定义的动画:
<set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/decelerate_interpolator" > <translate android:duration="@android:integer/config_shortAnimTime" android:fromYDelta="10%p" android:toYDelta="0" /> </set>
两点注意:
此处笔者测试了下,即便
android:fromYDelta="100%p"中为100%p,也不能省略为p。
窗体过渡动画不一定要在setContentView之前执行,可以在onCreate()中任意位置执行
2.2.1.3 主题切换
主题切换是通过Activity中继承自ContextThemeWrapper的setTheme(int resid)方法实现的。int style = R.style.RedTheme; activity.setTheme(style);
styles中定义了多种样式:
<style name="RedTheme" parent="AppBaseTheme.Dark"> <item name="colorPrimary">@color/red</item> <item name="colorPrimaryDark">@color/dark_red</item> <item name="colorAccent">@color/accent_red</item> </style>
2.2.1.4 针对KitKat的状态栏”沉浸模式”
@TargetApi(19) private void initWindow(){ if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT){ getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); SystemBarTintManager tintManager = new SystemBarTintManager(this); tintManager.setStatusBarTintColor(getStatusBarColor()); tintManager.setStatusBarTintEnabled(true); } }
针对安卓4.4系统,通过使用
SystemBarTintManager开源库实现了状态栏变色功能。
2.2.1.5 视图初始化
通过setContentView(getLayoutView());也巧妙将布局设置转移给子类实现
getLayoutView()抽象方法。
@Override protected int getLayoutView() { return R.layout.activity_main; }
通过这里我们也就又发现了新大陆,哦不,新道路,通往Activity布局文件的道路。
2.2.1.6 Toolbar初始化
由于各Activity中toolbar都一样,所以这里就将其抽取出来了,布局文件中使用<include>标签抽取,Activity中抽取出来一个ToolbarUtils类。
public class ToolbarUtils { public static void initToolbar(Toolbar toolbar, AppCompatActivity activity){ if (toolbar == null || activity == null) return; if (activity instanceof BaseActivity){ toolbar.setBackgroundColor(((BaseActivity) activity).getColorPrimary()); }else { toolbar.setBackgroundColor(activity.getResources().getColor(R.color.toolbar_bg_color)); } toolbar.setTitle(R.string.app_name); toolbar.setTitleTextColor(activity.getResources().getColor(R.color.toolbar_title_color)); toolbar.collapseActionView(); activity.setSupportActionBar(toolbar); if (activity.getSupportActionBar() != null){ activity.getSupportActionBar().setHomeAsUpIndicator(R.drawable.abc_ic_ab_back_mtrl_am_alpha); activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); } } }
2.2.1.7 重启Activity
BaseActivity中还包含一个reload()方法,用于没有动画的重启自身Activity,以便应用新的主题。关于不重启应用新样式主题,读者感兴趣可以去了解知乎的不重启Activity切换主题解决方案。public void reload(boolean anim) { Intent intent = getIntent(); if (!anim) { overridePendingTransition(0, 0); intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); intent.putExtra(BaseActivity.IS_START_ANIM, false); } finish(); if (!anim) { overridePendingTransition(0, 0); } startActivity(intent); }
至此,BaseActivity分析得差不多了,接下来回到MainActivity。
2.2.2 MainView接口
回到MainActivity再看看MainView接口,此接口主要是对BaseActivity里的共有方法进行抽象。public interface MainView extends View { void initToolbar(); void initDrawerView(List<String> list); void setToolbarTitle(String title); void showProgressWheel(boolean visible); void switchNoteTypePage(List<SNote> notes); void addNote(SNote note); ... }
注意View接口是在本项目中的接口,而非android.view.View
2.3 MainPresenter桥梁
大致浏览MainActivity,可以看到到处都是MainPresenter的影子,这便是MVP的架构思想,在MainActivity中将逻辑操作转交给MainPresenter去执行。// 初始化依赖注入 @Override protected void initializeDependencyInjector() { App app = (App) getApplication(); mActivityComponent = DaggerActivityComponent.builder() .activityModule(new ActivityModule(this)) .appComponent(app.getAppComponent()) .build(); mActivityComponent.inject(this); }
那么显然在MainActivity分析完成后的下一个目标就是MainPresenter了,现在先不急,继续分析MainActivity。
2.4 onCreate()的重写
在MainActivity中,并没有使用BaseActivity重写的生命周期,而是再次重写onCreate()方法,以独具一格。@Override protected void onCreate(Bundle savedInstanceState) { launchWithNoAnim(); super.onCreate(savedInstanceState); initializePresenter(); mainPresenter.onCreate(savedInstanceState); }
2.5 主布局文件分析
通过getLayoutView()可以找到主Activity对应的布局文件。主布局由ToolBar和DrawerLayout组成,DrawerLayout中包含RecyclerView正文界面和ListView侧滑界面,为了更好兼容低版本安卓系统,使低版本也能够拥有5.0以上版本的特效,大量使用了第三方库和自定义控件。
2.5.1 头声明
此处注意xmlns多个是可以省略为一个的,并不会影响程序的执行,但为了代码的可读性,还是应该写成多个。xmlns:fab="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:wheel="http://schemas.android.com/apk/res-auto"
2.5.2 FixedRecyclerView
<com.lguipeng.notes.view.FixedRecyclerView android:id="@+id/recyclerView" android:padding="4dp" android:layout_width="match_parent" android:layout_height="match_parent"/>
这个是作者的一个修正后的RecyclerView控件。
public class FixedRecyclerView extends RecyclerView { ... @Override public boolean canScrollVertically(int direction) { // check if scrolling up if (direction < 1) { boolean original = super.canScrollVertically(direction); return !original && getChildAt(0) != null && getChildAt(0).getTop() < 0 || original; } return super.canScrollVertically(direction); } }
这段代码暂时有些难以理解,此处就不详细分析了。此处读者可以去回顾RecyclerView的用法。在MainActivity中,对RecyclerView进行了初始化:
@Override public void initRecyclerView(List<SNote> notes){ recyclerAdapter = new NotesAdapter(notes, this); recyclerView.setHasFixedSize(true); recyclerAdapter.setOnInViewClickListener(R.id.notes_item_root, new BaseRecyclerViewAdapter.onInternalClickListenerImpl<SNote>() { @Override public void OnClickListener(View parentV, View v, Integer position, SNote values) { super.OnClickListener(parentV, v, position, values); mainPresenter.onRecyclerViewItemClick(position, values); } }); recyclerAdapter.setOnInViewClickListener(R.id.note_more, new BaseRecyclerViewAdapter.onInternalClickListenerImpl<SNote>() { @Override public void OnClickListener(View parentV, View v, Integer position, SNote values) { super.OnClickListener(parentV, v, position, values); mainPresenter.showPopMenu(v, position, values); } }); recyclerAdapter.setFirstOnly(false); recyclerAdapter.setDuration(300); recyclerView.setAdapter(recyclerAdapter); refreshLayout.setColorSchemeColors(getColorPrimary()); refreshLayout.setOnRefreshListener(mainPresenter); }
当中,设置了recyclerView的
NotesAdapter适配器,设置了SwipeRefreshLayout的主题颜色和刷新监听器,当然也传递给MainPresenter进行处理。
2.5.3 ProgressWheel
ProgressWheel为materialish-progress库中的一个进度环控件,在安卓低版本中实现MaterialDesign中自带效果,用法代码如下:
<com.pnikosis.materialishprogress.ProgressWheel android:id="@+id/progress_wheel" android:layout_width="75dp" android:layout_height="75dp" android:visibility="visible" android:layout_gravity="center" wheel:matProg_spinSpeed="1.2" wheel:matProg_barColor="?attr/colorPrimary" wheel:matProg_progressIndeterminate="true" />
2.5.4 Toolbar阴影
如何解决Toolbar在低版本安卓上效果不好,比如没有阴影效果,作者很机智地include了一个阴影效果布局:<View xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="6dp" android:background="@drawable/toolbar_shadow" />
drawable/toolbar_shadow:
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <gradient android:startColor="@android:color/transparent" android:endColor="@color/light_grey" android:angle="90"/> </shape>
2.5.5 BetterFab
BetterFab也是作者重写的一个基于FloatingActionButton的自定义控件,主要增加了强制隐藏方法,该功能体现在回收站功能中FloatingActionButton被隐藏掉了,也得以猜测到此应用中抽屉切换并非切换Fragment而是通过隐藏和显示模块实现的。
public class BetterFab extends FloatingActionButton{ private boolean forceHide = false; ... public void setForceHide(boolean forceHide) { this.forceHide = forceHide; if (!forceHide) { setVisibility(VISIBLE); }else { setVisibility(GONE); } } //if hide,disable animation public boolean canAnimation(){ return !isForceHide(); } }
2.5.6 抽屉中的ListView
抽屉中的ListView包含了几个不常用的属性,值得一看。<ListView android:id="@+id/left_drawer_listview" android:layout_width="@dimen/drawer_width" android:layout_height="0dp" android:layout_weight="1.0" android:choiceMode="singleChoice" android:divider="@android:color/transparent" android:dividerHeight="0dp" android:background="?attr/colorPrimary"/>
choiceMode: 选择模式: 多选和单选,默认不设定,此处单选便于用户知道自己所在的选项卡,如图所示:
![](http://7xqdz8.com1.z0.glb.clouddn.com/160410none.png)
![](http://7xqdz8.com1.z0.glb.clouddn.com/160410single.png)
divider: 分隔线
dividerHeight: 分隔线高度
分析完主布局,继续回到MainActivity。
2.6 NotesAdapter
首先回到之前提到了RecyclerView,其中的NotesAdapter是一个比较重要的东西,关乎着笔记列表的展示和操作。2.6.1 承接关系
public class NotesAdapter extends BaseRecyclerViewAdapter<SNote> implements Filterable { ... }
继承自BaseRecyclerViewAdapter,而BaseRecyclerViewAdapter才继承自真正应该继承的RecyclerView.Adapter
2.6.1.1 BaseRecyclerViewAdapter
2.6.1.1.1 增删改方法在BaseRecyclerViewAdapter中,首先是增加了对传入List的增删改方法,此处只贴上增加的方法:
public void add(E e) { this.list.add(0, e); notifyItemInserted(0); }
此处notifyItemInserted(int position)方法是用于通知RecyclerView有新的数据增加,对于不使用notifyDataSetChanged()方法,笔者猜测是为了防止刷新数据时列表跳回到表首。
2.6.1.1.2 内部点击事件
private void addInternalClickListener(final View itemV, final Integer position, final E valuesMap) { if (canClickItem != null) { for (Integer key : canClickItem.keySet()) { View inView = itemV.findViewById(key); final onInternalClickListener<E> listener = canClickItem.get(key); if (inView != null && listener != null) { inView.setOnClickListener((view) -> listener.OnClickListener(itemV, view, position, valuesMap) ); inView.setOnLongClickListener((view) -> { listener.OnLongClickListener(itemV, view, position, valuesMap); return true; }); } } } }
这段代码逻辑比较复杂,主要是对内部的点击事件进行回调,暂时先不作详细分析。
2.6.1.1.3 动画效果
首先animate方法用于执行getAnimators()中获得的所有动画效果:
protected void animate(RecyclerView.ViewHolder holder, int position){ if (!isFirstOnly || position > mLastPosition) { for (Animator anim : getAnimators(holder.itemView)) { anim.setDuration(mDuration).start(); anim.setInterpolator(mInterpolator); } mLastPosition = position; } else { ViewHelper.clear(holder.itemView); } }
getAnimators()方法在子类NotesAdapter进行实现:
@Override protected Animator[] getAnimators(View view) { if (view.getMeasuredHeight() <=0){ ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1.05f, 1.0f); ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1.05f, 1.0f); return new ObjectAnimator[]{scaleX, scaleY}; } return new Animator[]{ ObjectAnimator.ofFloat(view, "scaleX", 1.05f, 1.0f), ObjectAnimator.ofFloat(view, "scaleY", 1.05f, 1.0f), }; }
此处用到了属性动画相关知识。
2.6.2 onCreateViewHolder
@Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { mContext = parent.getContext(); final View view = LayoutInflater.from(mContext).inflate(R.layout.notes_item_layout, parent, false); return new NotesItemViewHolder(view); }
在创建单个Item视图的ViewHolder时,先使用LayoutInflater填充出一个view,再通过NotesItemViewHolder包装获得ViewHolder。
2.6.2.1 NotesItemViewHolder
NotesItemViewHolder继承自RecyclerView.ViewHolder,是一个为了提高性能的ViewHolder。首先看构造函数:
private final TextView mNoteLabelTextView; private final TextView mNoteContentTextView; private final TextView mNoteTimeTextView; public NotesItemViewHolder(View parent) { super(parent); mNoteLabelTextView = (TextView) parent.findViewById(R.id.note_label_text); mNoteContentTextView = (TextView) parent.findViewById(R.id.note_content_text); mNoteTimeTextView = (TextView) parent.findViewById(R.id.note_last_edit_text); }
这里并没有使用ButterKnife,也许是因为ButterKnife的使用有需要传入Activity参数的限制,或是因为成员变量为final类型,需要即时初始化。
类中还包含设置TextView的方法,用于设置每个Item View的文字。
2.6.3 绑定ViewHolder
@Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { super.onBindViewHolder(viewHolder, position); NotesItemViewHolder holder = (NotesItemViewHolder) viewHolder; SNote note = list.get(position); if (note == null) return; String label = ""; if (mContext != null) { boolean b = TextUtils.equals(mContext.getString(R.string.default_label), note.getLabel()); label = b? "": note.getLabel(); } holder.setLabelText(label); holder.setContentText(note.getContent()); holder.setTimeText(TimeUtils.getConciseTime(note.getLastOprTime(), mContext)); animate(viewHolder, position); }
此方法主要对ViewHolder中的控件进行赋值,在加载每个子项时调用此方法。
2.6.4 过滤操作
private static class NoteFilter extends Filter{ private final NotesAdapter adapter; private final List<SNote> originalList; private final List<SNote> filteredList; private NoteFilter(NotesAdapter adapter, List<SNote> originalList) { super(); this.adapter = adapter; this.originalList = new LinkedList<>(originalList); this.filteredList = new ArrayList<>(); } @Override protected FilterResults performFiltering(CharSequence constraint) { filteredList.clear(); final FilterResults results = new FilterResults(); if (constraint.length() == 0) { filteredList.addAll(originalList); } else { for ( SNote note : originalList) { if (note.getContent().contains(constraint) || note.getLabel().contains(constraint)) { filteredList.add(note); } } } results.values = filteredList; results.count = filteredList.size(); return results; } @Override protected void publishResults(CharSequence constraint, FilterResults results) { adapter.list.clear(); adapter.list.addAll((ArrayList<SNote>) results.values); adapter.notifyDataSetChanged(); } }
此类主要由搜索功能调用,构造函数对originalList进行赋值,performFiltering(…)方法进行过滤操作,过滤后列表存入filteredList,并且返回FilterResults以便后用,publishResults(…)方法进行展示filteredList的内容。
2.7 DrawerView的初始化
DrawerView视图比较简单,只有一个ListView,不过其中包含很多细节值得学习,而且作者为了后期的可拓展性定义了抽象类和接口。2.7.1 DrawerListAdapter
首先是DrawerListAdapter,继承自SimpleListAdapter,而SimpleListAdapter又继承自BaseListAdapter,然后才是继承自API的BaseAdapter,继承结构如图:![](http://7xqdz8.com1.z0.glb.clouddn.com/160411class.png)
Android Studio中按快捷键F4查看类继承结构图
2.7.1.1 BaseListAdapter
与BaseRecyclerViewAdapter类似,同样包含需要传入参数进行初始化操作的列表,以及增删改方法,以及回调的点击事件接口。2.7.1.2 SimpleListAdapter
@Override public View bindView(int position, View convertView, ViewGroup parent) { Holder holder; if (convertView == null){ convertView = LayoutInflater.from(mContext).inflate(getLayout(), null); holder = new Holder(); holder.textView = (TextView)convertView.findViewById(R.id.textView); convertView.setTag(holder); }else{ holder = (Holder)convertView.getTag(); } holder.textView.setText(list.get(position)); return convertView; }
SimpleListAdapter中,实现了抽象方法bindView(…),并且使用了ListView的缓存机制,但bindView(…)中填充Item视图并没有写死,而是交给了子类DrawerListAdapter去进行实现。
2.7.1.3 DrawerListAdapter
@Override protected int getLayout() { return R.layout.drawer_list_item_layout; }
2.7.1.3.1 布局
布局仅用了一个简洁的TextView,但TextView中包含了几个不常见的属性:
android:textAppearance="?android:attr/textAppearanceMedium" tools:text="Medium Text" android:singleLine="true"
android:textAppearance: 系统文字外观,’?’代表试探系统是否有此外观,没有则使用默认外观
tools:text: 告诉Android Studio在运行时忽略该属性,只在设计布局时有效
android:singleLine: 就是单行显示文字
2.7.2 抽屉开关按钮
通过mDrawerLayout.setDrawerListener(mDrawerToggle);为抽屉加上开关抽屉的监听。对监听器的配置如下:
mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, 0, 0){ @Override public void onDrawerOpened(View drawerView) { super.onDrawerOpened(drawerView); invalidateOptionsMenu(); mainPresenter.onDrawerOpened(); } @Override public void onDrawerClosed(View drawerView) { super.onDrawerClosed(drawerView); invalidateOptionsMenu(); mainPresenter.onDrawerClosed(); } }; mDrawerToggle.setDrawerIndicatorEnabled(true); // 指示器: 用于动画展示开关操作按钮变化
2.7.3 设置抽屉遮帘颜色
mDrawerLayout.setScrimColor(getCompactColor(R.color.drawer_scrim_color));
此处放上设置遮帘为蓝色后的效果图:
![](http://7xqdz8.com1.z0.glb.clouddn.com/160411scrim.png)
2.8 PopupMenu
在每个CardView上面需要显示菜单,包含”编辑”和”回收”,显示PopupMenu方法如下:@Override public void showNormalPopupMenu(View view, SNote note) { PopupMenu popup = new PopupMenu(this, view); popup.getMenuInflater() .inflate(R.menu.menu_notes_more, popup.getMenu()); popup.setOnMenuItemClickListener((item -> mainPresenter.onPopupMenuClick(item.getItemId(), note))); popup.show(); }
2.9 ActionBar上的搜索框
2.9.1 定义菜单
首先在menu.xml中新增搜索项:<item android:id="@+id/action_search" android:icon="@drawable/abc_ic_search_api_mtrl_alpha" android:title="@string/search" app:showAsAction="ifRoom|collapseActionView" app:actionViewClass="android.support.v7.widget.SearchView"> </item>
2.9.2 初始化SearchView
@Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); MenuItem searchItem = menu.findItem(R.id.action_search); //searchItem.expandActionView(); // 默认展开搜索框 SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem); ComponentName componentName = getComponentName(); searchView.setSearchableInfo( searchManager.getSearchableInfo(componentName)); searchView.setQueryHint(getString(R.string.search_note)); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String s) { return true; } @Override public boolean onQueryTextChange(String s) { recyclerAdapter.getFilter().filter(s); // 文字改变就即时处理搜索 return true; } }); MenuItemCompat.setOnActionExpandListener(searchItem, mainPresenter); // 监听搜索框是否打开,用于隐藏FloatingActionBar和禁用下拉刷新 return true; }
2.10 处理菜单事件
@Override public boolean onOptionsItemSelected(MenuItem item) { if(mDrawerToggle.onOptionsItemSelected(item)) { return true; } if (mainPresenter.onOptionsItemSelected(item.getItemId())){ return true; } return super.onOptionsItemSelected(item); }
第一个if用于判断是否点击打开抽屉开关按钮,第二个才传入MainPresenter进行菜单的处理,返回true当然就表示消耗此事件。
2.11 处理实体按键事件
@Override public boolean onKeyDown(int keyCode, KeyEvent event) { return mainPresenter.onKeyDown(keyCode) || super.onKeyDown(keyCode, event); }
返回值注意的是先处理传入MainPresentor里面方案,有代码自左至右运行顺序,如果不满足则按父类方法处理,这样写简直精妙,避免了多重if判断。
2.12 删除对话框的显示
@Override public void showDeleteForeverDialog(final SNote note) { AlertDialog.Builder builder = DialogUtils.makeDialogBuilder(this); builder.setTitle(R.string.delete_tip); DialogInterface.OnClickListener listener = (DialogInterface dialog, int which) -> mainPresenter.onDeleteForeverDialogClick(note, which); builder.setPositiveButton(R.string.sure, listener); builder.setNegativeButton(R.string.cancel, listener); builder.show(); }
这个逻辑没什么问题,主要想说的就是用lambda表达式的写法真的很好,不过笔者很好奇作者如何这么顺畅地写出lambda表达式,毕竟没有智能提示。
2.13 SnackbarUtils
SnackbarUtils是作者封装的快速显示Snackbar消息的,这个要学习的是如何通过传入Activity或Fab本身来执行Snackbar.make(…)方法:public static void show(View view, int message) { Snackbar.make(view, message, Snackbar.LENGTH_SHORT) .show(); } public static void show(Activity activity, int message) { View view = activity.getWindow().getDecorView(); show(view, message); }
可以传入FloatActionBar本身调用方法:
SnackbarUtils.show(fab, message);
至此,庞大的MainActivity算是分析得差不多了,那么接下来便啃另一块大骨头——MainPresenter。
3. Presenter —— MVP中的桥梁
3.1 接口
为了将逻辑放到P层中,MainPresenter继承了多个接口。public class MainPresenter implements Presenter, android.view.View.OnClickListener, SwipeRefreshLayout.OnRefreshListener, PopupMenu.OnMenuItemClickListener, MenuItemCompat.OnActionExpandListener {
由于其它接口前文已有提及,此处只展示和分析Presenter接口。
Presenter接口将Activity的生命周期抽象出来,并且通过attachView将Activity传入,用于MainPresenter的初始化。
public interface Presenter { void onCreate (Bundle savedInstanceState); void onResume(); void onStart (); void onPause(); void onStop (); void onDestroy(); void attachView(View v); }
注意View是本项目中所定义的,并非安卓API。
3.2 构造函数
@Inject public MainPresenter(@ContextLifeCycle ("Activity")Context context, FinalDb finalDb, PreferenceUtils preferenceUtils, ObservableUtils mObservableUtils, EverNoteUtils everNoteUtils) { this.mContext = context; this.mFinalDb = finalDb; this.mPreferenceUtils = preferenceUtils; this.mEverNoteUtils = everNoteUtils; this.mObservableUtils = mObservableUtils; }
构造函数的第一个参数前有一个
@ContextLifeCycle ("Activity"),不知道读者是否对此感到疑惑,要分析这个首先要了解java中的自定义注解。
3.2.1 Java Annotation自定义注解
此处只提及几个关键点,读者感兴趣可以参考深入浅出Java Annotation(元注解和自定义注解)。3.2.1.1 Annotation概述
Annontation是Java5开始引入的新特征。中文名称一般叫注解。它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。3.2.1.2 定义Annotation
使用关键字@interface而不是interface方法定义Annotation的成员,方法返回值类型必须为primitive类型、Class类型、枚举类型、annotation类型或者由前面类型之一作为元素的一维数组,方法的后面可以使用 default和一个默认数值来声明成员的默认值,null不能作为成员默认值
3.2.1.3 元注解
@Target 所修饰的对象范围@Retention 该Annotation被保留的时间长短
@Documented 可以被例如javadoc此类的工具文档化
@Inherited 被标注的类型是被继承的
3.2.1.4 自定义注解
定义注解格式:public @interface 注解名 {定义体}
例如:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented public @interface Description { String value(); }
3.2.1.5 对项目中”ContextLifeCycle”的分析
@Qualifier @Documented @Retention(RUNTIME) public @interface ContextLifeCycle { String value() default "App"; }
@Qualifier: 限定注释符
因为ContextLifeCycle的实现需要反射,所以我们暂时不能跟踪到它的实现,所以暂时先不作实现分析。
3.2.2 FinalDb参数
FinalDb是一个比较庞大的类,是aFinal第三方库的一个子模块,主要是数据库操作,接下来让我们大致了解一下它。3.2.2.1 数据库连接池
private static HashMap<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主要包含以下属性:
private Context mContext = null; // android上下文 private String mDbName = "notes.db"; // 数据库名字 private int dbVersion = 1; // 数据库版本 private boolean debug = true; // 是否是调试模式(调试模式 增删改查的时候显示SQL语句) private DbUpdateListener dbUpdateListener; // private boolean saveOnSDCard = false;//是否保存到SD卡 private String targetDirectory;// 数据库文件在sd卡中的目录
3.2.2.2 构造函数
private FinalDb(DaoConfig config) { ... if (config.getTargetDirectory() != null && config.getTargetDirectory().trim().length() > 0) { this.db = createDbFileOnSDCard(config.getTargetDirectory(), config.getDbName()); } else { this.db = new SqliteDbHelper(config.getContext() .getApplicationContext(), config.getDbName(), config.getDbVersion(), config.getDbUpdateListener()) .getWritableDatabase(); } this.config = config; }
此部分判断如果配置中指定了文件目录,则在指定的文件目录创建数据库文件,否则使用SqliteDbHelper获取软件Data目录下的数据库。
3.2.2.2.1 createDbFileOnSDCard方法
通过SQLiteDatabase.openOrCreateDatabase(file, null)在SD卡创建数据库文件(*.db)。
3.2.2.2.2 SqliteDbHelper内部类
class SqliteDbHelper extends SQLiteOpenHelper { ... }
此类继承SQLiteOpenHelper获取安卓默认的数据库。
3.2.2.3 公有的create()方法
create方法重载了多个,配合使用一个或多个参数调用的情况。例如其中之一:public static FinalDb create(Context context, String targetDirectory, String dbName, boolean isDebug, int dbVersion, DbUpdateListener dbUpdateListener) { DaoConfig config = new DaoConfig(); config.setContext(context); config.setTargetDirectory(targetDirectory); config.setDbName(dbName); config.setDebug(isDebug); config.setDbVersion(dbVersion); config.setDbUpdateListener(dbUpdateListener); return create(config); }
后面便是对数据库CRUD操作和数据库关系操作,笔者能力有限,便不继续研读,接下来谈谈此框架的使用方法。
3.2.2.4 用法
首先创建相关Entity类,如:public class User { private int id; private String name; private String email; private Date registerDate; private Double money; /////////////getter and setter 不能省略哦/////////////// public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public Date getRegisterDate() { return registerDate; } public void setRegisterDate(Date registerDate) { this.registerDate = registerDate; } public Double getMoney() { return money; } public void setMoney(Double money) { this.money = money; } }
接下来使用aFinal即可
FinalDb db = FinalDb.create(this); User user = new User(); user.setEmail("..."); user.setName("..."); user.setRegisterDate(new Date()); db.save(user); List<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接口
public interface Fun<T> { T call() throws Exception; }
内部包含一个Fun接口,即为一个函数,每个需要作为参数的函数被包装在实现该接口的类中。如:
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(); } }
这样函数就能作为一个参数被传递到其它函数中。如:
create(new GetEverNoteUserFun(everNoteUtils))
此处将
方法称作
函数是为了便于理解。
3.2.5 EverNoteUtils参数
EverNoteUtils就是调用印象笔记的API,包含推送笔记和获取笔记等方法,这里可以看印象笔记官方提供的API,此处不作分析。待续
更多分析请阅读《极简笔记》源码分析(二)。参考
项目原地址 lguipeng/Notes使用Dagger 2进行依赖注入
浅谈依赖注入
textAppearance解析
android中xmlns:tools属性详解
深入浅出Java Annotation(元注解和自定义注解)
使用android快速开发框架afinal的FinalDb操作android数据库
相关文章推荐
- Qt移动应用开发(八):实现跨平台的QML和OpenGL混合渲染
- Android中全局Application的onCreate多次调用问题
- BP人工神经网络
- pipe管道——进程通信
- 算法系列-直接插入排序和希尔排序
- ArcGIS教程:为地理处理任务定义输出符号系统
- 汉诺塔递归解决方法经典分析
- MySQL最大连接数设置
- php中怎么使用call_user_func动态调用方法
- Java String 源码浅析 JDK1.7
- git教程
- Linq专题之var关键字
- 蓝桥杯 小朋友排队+
- QLibrary加载so的注意事项
- android数据库sqlite基本操作。
- 可变参数列表
- 第七周项目(1)-普通函数求两点间距离
- Eclipse 下maven 笔记
- ubuntu系统的grub配置
- STL中二分查找 lower_bound()