开源项目学习笔记-知乎日报purify

项目地址:https://github.com/izzyleung/ZhihuDailyPurify


今天我们学习一个知乎日报的第三方客户端。代码写的很简洁,但不简单。

首先,看看AndroidManifest.xml,一个自定义的application,theme写在了这里,保持了一致的体验。再说说这个theme,继承的是Theme.AppCompat.Light.NoActionBar, 里面有一个属性叫windowActionModeOverlay,设为true可以使actionMode(比如文字复制)下,覆盖掉actionBar的布局;然后是MainActivity:里面需要注意的是这个属性

android:alwaysRetainTaskState 这个属性用来标记应用的task是否保持原来的状态,“true”表示总是保持,“false”表示不能够保证,默认为“false”。此属性只对task的根Activity起作用,其他的Activity都会被忽略。 默认情况下,如果一个应用在后台呆的太久例如30分钟,用户从主选单再次选择该应用时,系统就会对该应用的task进行清理,除了根Activity,其他Activity都会被清除出栈,但是如果在根Activity中设置了此属性之后,用户再次启动应用时,仍然可以看到上一次操作的界面。 这个属性对于一些应用非常有用,例如Browser应用程序,有很多状态,比如打开很多的tab,用户不想丢失这些状态,使用这个属性就极为恰当。

然后就是PrefsActivity,PortalActivity, SearchActivity分别是:设置,入口(这里的功能是通过选择日期来呈现该日的news),搜索。


于是我们来到MainActivity:

在main之前,我们当然要看看baseActivity,里面写的很简约,一个protected的Toolbar,仅仅进行了findView和setSupportActionBar(mToolBar)的操作(当然在onCreate中)。一个protected的layoutResId,也就是layout的布局id。

然后mainActivity的onCreate中,第一件事就是layoutResId赋值为R.layout.activity_main了,接着才调用super.onCreate(savedInstanceState);(好像顺序没影响吧);点进main布局看看吧。典型的MD的风格:根布局是CoordinatorLayout, 上面放着AppBarLayout以实现一些toolbar的特效,对,就是duang的那种。这里AppBarLayout的theme是ThemeOverlay.AppCompat.Dark,里面包着一个toolbar和一个TabLayout;

toolbar有一些属性,再啰嗦几句吧:background背景色用的是?attr/colorPrimary,这个属性好像能自动获得主题色中的colorPrimary,minHeight用的是?attr/actionBarSize, 也许根据系统版本不同高度不同?管他呢,直接用。app:layout_scrollFlags="scroll|enterAlways" 这就是特效的设置了,所有需要在滑动时隐藏的控件都要设置scroll标签,enterAlways表示向下滑动时,该控件会重新可见。然后popupTheme和app:theme都用了ThemeOverlay.AppCompat开头的。。这俩theme一直没搞清楚啥区别。等我有机会试验下。(立了flag= =)

再看看tabLayout的属性,也有一个android:theme,用的跟toolbar的差不多。ThemeOverlay.AppCompat.Dark,tabBackground用的也是?attr/colorPrimary, tabIndicatorColor这个颜色就可以自定义啦。也就是tab上title的颜色,tabMode是scrollable(难道不可滚动也算是tab?)

整个AppBarLayout下面是一个ViewPager,which承担着主要的内容了。因为这个pager作为一个可滚动的View跟AppBarLayout配合,有个属性必须设置: app:layout_behavior="@string/appbar_scrolling_view_behavior"

最后是一个FloatingActionButton, 用layout_gravity放到右下角,设置了margin就行了~

回到onCreate中,我们应该先初始化一些View。viewPager.setOffscreenPageLimit(7):

Set the number of pages that should be retained to either side of the current page in the view hierarchy in an idle state. 就是说设置了View树在闲置状态下,当前page任意一边应当被retain的page数量。

接着看看viewpager的adapter:用的是FragmentStatePagerAdapter,但是官方建议是有大量page时用这个来维持page的状态,这里作为tabs,用FragmentPagerAdapter应该足够了。复写方法:

-Fragment getItem(int i),这里new了一个NewListFragment,然后把一些参数放到了bundle中用setArguments(bundle)传递过去。比如是否是第一页,date日期。

-getCount() 这里写死为7。固定显示了7个页面。

-CharSequence getPageTitle(int position) 如果position为0就显示今日日报,其他的就把title设置为日期的字符串。这里用的是DateFormat.getDateInstance().format(calendar.getTime());(不吐槽这乱七八糟的calendar和date方法了)


接着我们当然看看pager里面的Fragment是什么?

先说布局,很简单。SwipeRefreshLayout,里面一个RecyclerView; 第一个方法onAttach(Context context)中,执行了一个RecoverNewsListTask, 正如名字所见,是恢复缓存的news数据的一个AsyncTask, doInbackground中,调用了MyApplication.getDataSource()获得了DailyNewsDataSource这个静态对象(也就是说这个数据辅助操作对象是一个静态的,在Application中的onCreate中初始化的对象),我们看看这个对象里面封装了什么东西?

成员变量:一个SQLiteDatabase, 一个DBHelper,一个allColumns的String[],里面包括了COLUMN_ID(文章的Id?), COLUMN_DATE, COLUMN_CONTENT

DailyNewsDataSource里面,构造函数中初始化了dbHelper,还有一个open方法:database=dbHelper.getWritableDataBase();

然后看看insetDailyNewsList(String date, String content),也就是把news插入到数据库中的方法。使用ContentValues来put进date和content,用database.insert(TABLE_NAME, null, values),插入到table。

接着是updateNewsList(String date, String content), 也就是更新新的news到数据库中。依然是ContentValues,然后database.update(TABLE_NAME,values,whereClause, null)

第三个whereClause填的是String的 date=xxx,以插入到相对应的日期。

然后insertOrUpdateNewsList(String date, String content)方法,判断了newsOfTheDay(date)是否为空,如果不为空就update,为空就应该insert。

那么这个newsOfTheDay(String date)做了啥呢。这里根据date来database.query了一下这个table(其余参数为null),返回一个cursor,然后cursor.moveToFirst();(啥用)

拿着cursor调用了cursorToNewsList(cursor),此方法判断了cursor不为空,cursor.getCount()>0,然后new GsonBuilder().create().fromJson(cursor.getString(2), Type.newsListType),别问我啥意思,我也不懂。

其中Type是反射(java.lang.reflect.Type). public static final newsListType=new TypeToken<List<DailyNews>>

大概懂了。。也就是说,拿着cursor,搜索到json数据(cursor.getString(2),这个2又是啥意思呢),用了Gson的fromJson方法,传入json和Type, 这个Type是一个TypeToken<T>,T就表示你想转换成的对象。这里是List<DailyNews>也就是DailyNews的一个list。

以上就是DailyNewsDataSource这个操作数据库的类。

接着看NewsListFragment的onCreate(Bundle savedInstanceState),当savedInstanceState为空时,也就是Fragment正常运行,没有被系统回收时,先getArguments,获得一些参数的值比如date, 是否是第一页。然后调用了setRetainInstance(true)来达到activity重建时保存Fragment状态的目的。

然后是onCreateView,初始化了recyclerView,调用了recyclerView.setHasFixedSize方法来优化性能。然后recyclerView.setLayoutManager().接着初始化adapter

看看NewsAdapter吧:

构造方法传入的是List<DailyNews>的数据对象,接着调用了setHasStableIds(true),关于此方法参考 https://www.reddit.com/r/androiddev/comments/2lr8bf/what_does_recyclerview_sethasstableids_do_and_why/

然后一个updateNewsList(List<DailyNews> newsList)方法:先this.newsList=newsList; 然后notifyDataSetChanged();用来更新数据

onCreateViewHolder之前,看看CardViewHolder,implements了onClickListener,里面的public成员有newsImage,用来显示图片,questionTitle问题标题,dailyTitle就是日报标题,还有一个share按钮的imageView。在构造函数中,除了默认的View v, 多了一个clickResponseListener的interface(定义在ViewHolder内部,包含俩方法,一个处理share按钮,一个处理其他地方),用来实现recyclerView的Item的点击事件。然后:v.setOnClickListener(this);share.setOnClickListener(this);onClick方法中判断v==share, 如果是share:clickResponseListener.onShare(v, getAdapterPosition());如果不是:clickResponseListener.onCardClick(getAdapterPosition);

onCreateViewHolder中:

从ViewGroup parent中parent.getContext();获得context。然后View itemView=LayoutInflater.from(context).inflate(R.layout.recycler_item,parent,false);

然后new CardViewHolder()传入itemView,new 一个ClickResponseListener,复写onCardClick:browseOrShare(context, position, true),onShare: new一个PopupMenu,MenuInflater inflater= popup.getMenuInflater();inflater.inflate(R.menu.context_news_list, popup.getMenu());popup.setOnMenuItemClickListener()>>>>判断Item的id,然后browseOrShare(context, position, false)——判断dailyNews是否为多项,是的话则获得QuestionTitleList(是一个List<String>)然后.toArray(new String[该list.size()])获得String[]。然后new 一个AlertDialog.Builder(context).setTitle(dailyNews.getDailyTitle())(注意,DailyTitle是首页的粗体title,唯一值).setItems(questionTitles, new 一个DialogInterface.OnClickListener,在onClick中,判断是浏览还是分享(谁让他非要把两件事情写在一个方法里呢),如果是浏览就根据点击Item的位置到dailyNews中的QuestionUrlList中来取对应的Url,并执行goToZhihu,否则是分享的话,就获得相应的Title和Url然后执行share)

那么我们看看查看知乎问题,和分享这两个功能:

goToZhihu(Context context, String url): 先用PreferenceManger.getDefaultSharedPreferences(context),然后getBoolean获得是否用知乎客户端打开这一值(设置里面有)。如果不用:那么进入openUsingBrowser使用浏览器;如果用,就要先检查知乎是否安装了,参考下面的代码

public static boolean isZhihuClientInstalled() {
    try {
        return preparePackageManager().getPackageInfo("com.zhihu.android", PackageManager.GET_ACTIVITIES) != null;
    } catch (PackageManager.NameNotFoundException ignored) {
        return false;
    }
}

    public static boolean isIntentSafe(Intent intent) {
        return preparePackageManager().queryIntentActivities(intent, 0).size() > 0;
    }

    private static PackageManager preparePackageManager() {
        return ZhihuDailyPurifyApplication.getInstance().getPackageManager();
    }
}
如果安装了,就new Intent(Intent.ACTION_VIEW, Uri.parse(url)), intent.setPackage("com.zhihu.android"); 然后startActivity(intent);如果没有同样进入用浏览器打开的方法。浏览器打开的方法跟知乎打开类似,都是new Intent(Intent.ACTION_VIEW, Uri.parse(url)); 然后一样,检查下isIntentSafe(intent)就可以直接startActivity了。


share(Context context, String questionTitle, String questionUrl) 因为是分享,所以new Intent(Intent.ACTION_SEND); 然后share.setType("text/plain"), 这里的参数叫MIME, MIME是描述消息内容类型的网络标准,它可以包含文本、图像、音视频以及其他应用程序专用的数据。然后share.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET)这个flag很重要,我的training笔记里有提到过。就是过一段时间重新打开app时会直接进入app而不是第三方的分享界面。然后share.putExtra(Intent.EXTRA_TEXT, questionTitle+" "+questionUrl+" 分享自知乎网"); context.startActivity(Intent.createChooser(share, "这里填上chooser的标题")); 因为是分享,所以每次都要显示chooser。


我们说到哪来着?swipeRefreshLayout需要setOnRefreshListener,其中onRefresh()中就写了doRefresh方法该方法就根据date获取news,然后刷新结束后调用setRefreshing(false), 对UI进行更新。第二个方法就是swipeRefreshLayout.setColorSchemeResources(R.color.xxx)设置该刷新提示的主题色,这个参数可以是多个。


我们看看选择日期来显示日报的PortalActivity:

我们看到这个activity并没有setContentView,也没有给baseActivity中的layoutId重新赋值。也就是意味着,此activity用的就是base的layout(一个toolbar和一个用来放Fragment的framelayout)。由于是第二级activity(只要不是main,好像都是第二级activity),onCreate中调用了getSupportActionBarl().setDisplayHomeAsUpEnable(true)以启用toolbar上面的返回按钮; 接着就调用了显示选择日期的Fragment。里面给日历选择器加了个接口。onDateSelectedListener中,执行了接口的方法。而portalActivity则实现了这个接口。

如果日期有效(在今天和知乎生日之间),则用NewsListFragment来replace掉现在的fragment布局。如果无效则显示提示。另外。toolbar有一个向前、向后的按钮。点击之向前的话,先判断是不是今天,如果是今天就没有明天内容;向后的话,先判断是不是生日,如果是生日就不能往后查看了,然后return;return下面接着更新代码就行了。

UI结构:portalActivity with空白的framelayout作为容器+calenderPickerViewFragment负责选择日期+newsListFragment负责显示数据。

然后看看searchActivity:

同样的,也是空activity+fragment的结构。重点看看SearchNewsFragment,此fragment用来显示搜索后的结果。大体布局跟NewsListFragment类似,区别就是每个new的item上面加了一个所谓的StickyHeaderItemDecoration,是一个自定义的itemDecoration,当然要调用recylcerView.addItemDecoration(header)。然后还有一个update方法。就是set一下对应的数据,然后notifyDataSetChanged(); 在SearchActivity的initView中,我发现了一个自定义的SearchView,然后作者new了一个Relativelayout并且relative.addView(searchView)然后toolbar.addView(relative),不理解为什么把searchView包上一层relativeLayout。然后就是一个SearchTask,继承自baseTask(作者挺喜欢写继承的。。),base里面的doInbackground则进行了网络请求,返回的是List<DailyNews>。这里使用了GSON进行解析。然后SearchTask则复写了onPreExecute和onPostExecute,前者处理搜索前的ui提示,这里new了一个ProgressDialog, 然后dialog.setOnCancelListener里面,在dialog被cancel时调用SearchTask.this.cancel(true);最后别忘了dialog.show();至于onPostExecute则根据拿到的newsList,调用searchNewsFragment(用来展示搜索结果的)的update方法就行了。这里还有一个判断if(isCancelled()), 表示这个task被cancel了,然后可以展示一个toast或者snackBar给用户一个反馈。

最后看看PrefsActivity:

这个比较简单。onCreate中就直接getFragmentManager().beginTransaction.replace(R.id.fragment_frame, new PrefsFragment()).commit();然后PrefsFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener。onCreate中,调用addPreferencesFromResource(R.xml.prefs)这个xml写在res下的xml文件夹下面,根节点是<PreferenceScreen。然后findPreference("about").setOnPreferenceClickListener(this)则是为about按钮写监听。我们还可以通过(PreferenceCategory)findPreference("这里是设置组也就是preferenceCategory的key")获得该category的对象,然后category.removePreference(findPreference("该组下某个preference的key"))来控制该设置的显示与否。然后onPreferenceClick里面根据preference.getKey判断是哪个按钮,做出相应事件即可。补充一句,textview中换行只要加上"\n"就行。

以上就是ZhihuDailyPurify整个项目流程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值