项目源码地址 https://github.com/liaozhoubei/NetEasyNews/tree/dev_kotlin
在前面将框架搭建完毕后,就可以正式开始写界面逻辑了。说到写界面,其实把框架搭建完后,剩下的都只是苦力活,没太多的技术含量。那么我们就开始剩下的课程吧!
在我们的应用中使用 tablayout + ViewPager + fragment 来呈现多标签页面,那么就会出现一个问题, viewpager 有预加载的功能,一般会加载 4 个所有的fragment ,也就是说,如果在 fragment 初始化的时候请求网络,那么就会同时有 4 个页面都会请求,但是用户只想查看 1 个页面,这就会造成流量的浪费,当然啦,现在处于 4G 向 5G 进发的时代,相信用户也不在乎。但是作为开发而言还是要考虑到此时的效率问题。
基于以上问题,我们需要在当前 fragment 显示的时候加载数据,页面不显示的时候就不加载数据,同时也要避免页面来回切换时重复加载网络,那么需要怎么解决呢?答案在 fragment 自身。
fragment 本身有 setUserVisibleHint(isVisibleToUser: Boolean) 方法,此方法会在fragment 可见性发送改变的时候调用,也就是页面显示/隐藏的时候被调用,我们的办法就是重写此方法。如下:
override fun setUserVisibleHint(isVisibleToUser: Boolean) { if (isFirstLoad && isVisibleToUser) {//视图变为可见并且是第一次加载 onlazyLoadData(); isFirstLoad = false; } super.setUserVisibleHint(isVisibleToUser) }然而还有个问题,如果按照上面的代码第一次进入页面之后setUserVisibleHint先被调用,这时视图还没有完成创建,所以数据加载操作不会被调用。。而之后没有切换页面,Fragment的可见性也就不会发生改变了,setUserVisibleHint也就不会被调用了,那么数据加载也就不会被执行了。
解决方案也就,那就是在 fragment 的 onViewCreated(view: View, savedInstanceState: Bundle?) 中判断是否已经显示,如果显示了,就进行加载网络,如下:
// 在此次判断是否fragment 初次显示,若是则需要加载数据 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) isFirstLoad = true;//视图创建完成,将变量置为true if (getUserVisibleHint()) {//判断Fragment是否可见 onlazyLoadData();//数据加载操作 isFirstLoad = false;//将变量置为false } onNetWorkRefresh() }如此便完成了 fragment 的懒加载实现,整体代码如下:
open abstract class BaseFragment : Fragment(), View.OnClickListener { private var mLoading: LinearLayout? = null; private var attaach = false; var mHandler = Handler() var mContext: Context? = null private var isFirstLoad = false public var onFragmentInteractionListener: OnFragmentInteractionListener? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { var parentView: View = inflater.inflate(R.layout.fragment_base, container, false); val activity = activity val layoutInflater = LayoutInflater.from(activity) val view = layoutInflater.inflate(getLayoutResId(), null, false) (parentView.findViewById<View>(R.id.frame_container) as FrameLayout).addView(view) return parentView } // 在此次判断是否fragment 初次显示,若是则需要加载数据 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) isFirstLoad = true;//视图创建完成,将变量置为true if (getUserVisibleHint()) {//判断Fragment是否可见 onlazyLoadData();//数据加载操作 isFirstLoad = false;//将变量置为false } onNetWorkRefresh() } override fun setUserVisibleHint(isVisibleToUser: Boolean) { if (isFirstLoad && isVisibleToUser) {//视图变为可见并且是第一次加载 onlazyLoadData(); isFirstLoad = false; } super.setUserVisibleHint(isVisibleToUser) } private fun onlazyLoadData() { initData() } /** * 加载要显示的数据 */ protected abstract fun initData() .... }我们的新闻主界面是这样子的:
这种界面的实现一般用 tablayout + viewpager 来实现,它们的布局文件非常简单,如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:background="@color/aliceblue" > <com.google.android.material.tabs.TabLayout android:id="@+id/tab_layout" android:layout_width="match_parent" android:layout_height="40dp" android:background="@drawable/tab_bg" app:tabIndicatorColor="@color/colorTheme" app:tabIndicatorHeight="5dp" app:tabSelectedTextColor="@color/colorTheme" app:tabTextColor="@color/defaultTextColor"> </com.google.android.material.tabs.TabLayout> <androidx.viewpager.widget.ViewPager android:id="@+id/tablayout_viewpager" android:layout_width="fill_parent" android:layout_height="match_parent" android:background="@color/white" /> </LinearLayout>也就是一个垂直布局,然后将 viewpager 交由 tablayout 管理即可。 整体的 NewsFragment 代码如下:
class NewsFragment : BaseFragment() { var sharedPreferences: SharedPreferences? = null var handler: Handler = Handler(); private var listDataSave: ListDataSave? = null private var myChannelList: List<ProjectChannelBean.TListBean>? = null private var moreChannelList: List<ProjectChannelBean.TListBean>? = null private var fragments: MutableList<BaseFragment>? = null private var fixedPagerAdapter: FixedPagerAdapter? = null private var baseFragment: BaseFragment? = null // 当前新闻频道的位置 private var tabPosition: Int = 0 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) onFragmentInteractionListener?.onFragmentTitleChange("新闻中心") sharedPreferences = mContext?.getSharedPreferences("Setting", Context.MODE_PRIVATE) listDataSave = ListDataSave(mContext, "channel") fragments = ArrayList<BaseFragment>() fixedPagerAdapter = FixedPagerAdapter(childFragmentManager) getDataFromSharedPreference() fixedPagerAdapter!!.setChannelBean(myChannelList) fixedPagerAdapter!!.setFragments(fragments) tablayout_viewpager.setAdapter(fixedPagerAdapter) tab_layout.setupWithViewPager(tablayout_viewpager) } override fun getLayoutResId(): Int { return R.layout.tablayout_pager } override fun initData() { } /** * 判断是否第一次进入程序 * 如果第一次进入,直接获取设置好的频道 * 如果不是第一次进入,则从sharedPrefered中获取设置好的频道 */ private fun getDataFromSharedPreference() { var isFirst = sharedPreferences!!.getBoolean("isFirst", true) myChannelList = CategoryDataUtils.getChannelCategoryBeans() moreChannelList = getMoreChannelFromAsset() listDataSave!!.setDataList("myChannel", myChannelList) listDataSave!!.setDataList("moreChannel", moreChannelList) val edit = sharedPreferences!!.edit() edit.putBoolean("isFirst", false) edit.commit() fragments!!.clear() for (i in myChannelList!!.indices) { baseFragment = NewsListFragment.newInstance(myChannelList!!.get(i).tid!!) as BaseFragment fragments!!.add(baseFragment!!) } if (myChannelList!!.size <= 4) { tab_layout.setTabMode(TabLayout.MODE_FIXED) } else { tab_layout.setTabMode(TabLayout.MODE_SCROLLABLE) } } /** * 从Asset目录中读取更多频道 * * @return */ private fun getMoreChannelFromAsset(): List<ProjectChannelBean.TListBean> { val moreChannel = IOUtils.readFromFile(mContext, "projectChannel.txt") var gson = Gson() var projectChannelBean = gson.fromJson(moreChannel, ProjectChannelBean::class.java); var projectChannelBeanList = projectChannelBean.tList return projectChannelBeanList!! } }总共不到 100 多行的代码,但实质上核心代码只有以下几行:
// 设置 viewpager 的适配器 tablayout_viewpager.setAdapter(fixedPagerAdapter) // 设置 tab_layout 的显示效果 if (myChannelList!!.size <= 4) { // 让 tab_layout 均分,适合少量 fragment 显示 tab_layout.setTabMode(TabLayout.MODE_FIXED) } else { // 让 tab_layout 可滑动显示,适合多个 fragment 显示 tab_layout.setTabMode(TabLayout.MODE_SCROLLABLE) } // 将 ViewPager 交由 tablayout 托管 tab_layout.setupWithViewPager(tablayout_viewpager)以上就完成了一个整体的布局,接下来需要显示相应栏目的内容
对于新闻项目的 UI 界面一般都很简单,就是一个列表页面,我们这里直接使用 RecyclerView 。但是还有上拉加载更多呢?我们这里使用谷歌推出的 jetpack 系列库中的 paging 库,paging 的使用还算简单,但是需要重写 PositionalDataSource 类来实现 DataSource.Factory ,具体知识就不详细说了,可上网查询资料,也可看我的这篇博客,其中还将谷歌的示例由 kotin 转成 java (我一定是太闲了 ( ̄▽ ̄)")
https://www.jianshu.com/p/c650a429a209 从 android-paging 开始学习 Paging
NewsListFragment 的代码也很简单,没什么好说的,如下:
class NewsListFragment : BaseFragment() { private var mStartIndex = 0 // 请求数据的起始参数 private var tid: String? = null // 栏目频道id private val adapter = NewsListAdapter() companion object { public val KEY = "TID" @JvmStatic fun newInstance(tid: String) = NewsListFragment().apply { arguments = Bundle().apply { putSerializable(KEY, tid) } } } override fun getLayoutResId(): Int { return R.layout.fragment_news_list } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) recyclerView.adapter = adapter val mLayoutManager = LinearLayoutManager(mContext) recyclerView.setLayoutManager(mLayoutManager) if (arguments != null) { //取出保存的频道TID tid = arguments!!.getString("TID") } val config = PagedList.Config.Builder() .setPageSize(20) //配置分页加载的数量 .setEnablePlaceholders(false) //配置是否启动PlaceHolders .setInitialLoadSizeHint(20) //初始化加载的数量 .build() var server = RetrofitHelper.getInstance(HttpUrl.parse(Api.host)!!, mContext!!) val liveData = LivePagedListBuilder(MyDataSource(tid!!, server), config) .build() liveData.observe(this, Observer<PagedList<NewsListNormalBean>>{ // 获取请求到的数据给 adapter adapter.submitList(it) adapter.notifyDataSetChanged() }) } ...... class MyDataSource(var tid: String, var server: NetEasyService) : DataSource.Factory<Int, NewsListNormalBean>() { override fun create(): DataSource<Int, NewsListNormalBean> { return NewsListPositionalDataSource(tid!!, server) } } }代码的主要逻辑为 onViewCreated 方法,我们取出通过 intent 传递的信息后,就设置了一个 LivePagedListBuilder 监听,监听 paging 的数据回调。
诸位或许会有疑问,网络请求呢?相关的网络请求在 NewsListPositionalDataSource 类中,代码如下
class NewsListPositionalDataSource(var tid: String, var server:NetEasyService) : PositionalDataSource<NewsListNormalBean>() { // 当 paging 没有数据时调用 override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<NewsListNormalBean>) { try { var call = server.getNewsListNormalBean(from=tid, offset=params.requestedStartPosition); var response = call.execute() if (response.isSuccessful){ var body = response.body() var newsListNormalBeanList = DataParse.NewsList(response.body(), tid) callback.onResult(newsListNormalBeanList,0) }else{ Log.e("NewsListDataSource", "error"); } } catch (e: Exception) { e.printStackTrace() } } // 当 paging 拉到底部的时候调用 override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<NewsListNormalBean>) { try { var call = server.getNewsListNormalBean(tid, params.startPosition); var response = call.execute() if (response.isSuccessful){ var body = response.body() var newsListNormalBeanList = DataParse.NewsList(response.body(), tid) callback.onResult(newsListNormalBeanList) }else{ Log.e("NewsListDataSource", "error"); } } catch (e: Exception) { e.printStackTrace() } } }这下子就清晰了,我们通过 LivePagedListBuilder 设置了一个数据源,然后获取数据源的回调更新界面给 adapter 。同时这样还有一个好处,paging 中使用到了会自动判断 fragment 的 lifeCycle ,也就是说当界面不存在的之后,数据是不会通过回调通知 ui 的,避免了运行过程中 ui 已经销毁的问题。
ok ,上面我们已经知道了在 netlistFragment 中会通过 paging 判断当前的列表是否有数据还是拉到页面底部需要请求更多,然后通过接口回调的方式将数据传递给 adapter 。那么剩下的就是在 adapter 的数据显示了。
仔细观察网易新闻的列表显示,发现它会有三种界面交替,如下:
一种是列表都是大图,一种是右边为大图,一种是三种图片在一起。那么要怎么实现呢?RecyclerView.Adapter 很好的处理了这种情况,我们只需要重写 getItemViewType 方法,判断当前的新闻需要显示哪种类型的新闻,然后创建相应的 viewHolde 即可,代码如下:
class NewsListAdapter : PagedListAdapter<NewsListNormalBean, BaseNewsHolder>(REPO_COMPARATOR) { private val BIG_IMG = 0 private val SMALL_IMG = 1 private val THREE_IMG = 2 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseNewsHolder { var view: View if (viewType == BIG_IMG) { view = View.inflate(parent.context, R.layout.item_news_big_pic, null) return BigImgViewHolder(view) } else if (viewType == THREE_IMG) { view = View.inflate(parent.context, R.layout.item_news_three_pic, null) return ThreeImgViewHolder(view) } else { view = View.inflate(parent.context, R.layout.item_news_normal, null) return SmallImgViewHolder(view) } } override fun getItemViewType(position: Int): Int { var viewType = SMALL_IMG val newsListNormalBean = getItem(position) val hasAd = newsListNormalBean?.hasAD val imgextraBeenlist = newsListNormalBean?.imgextra if (hasAd == 1) { viewType = BIG_IMG } else if (imgextraBeenlist != null && imgextraBeenlist!!.size > 1) { viewType = THREE_IMG } return viewType } override fun onBindViewHolder(holder: BaseNewsHolder, position: Int) { val newsListNormalBean = getItem(position) var imgextraBeenlist = newsListNormalBean?.imgextra val imageSrc = newsListNormalBean?.imgsrc val title = newsListNormalBean?.title val source = newsListNormalBean?.source val postTime = newsListNormalBean?.ptime // 文章的id号 val docid = newsListNormalBean?.docid if (getItemViewType(position) == BIG_IMG) { // 一张大图的情况 val bigImgViewHolder = holder as BigImgViewHolder Glide.with(bigImgViewHolder.big_Image.context) .load(imageSrc) .placeholder(R.drawable.defaultbg_h) .into(bigImgViewHolder.big_Image) } else if (getItemViewType(position) == THREE_IMG) { val threeImgViewHolder = holder as ThreeImgViewHolder // 三张图片的情况 setNetPicture(imageSrc, threeImgViewHolder.one_image) for (j in imgextraBeenlist!!.indices) { if (j == 0) { setNetPicture(imgextraBeenlist.get(j).imgsrc, threeImgViewHolder.two_image) } else if (j == 1) { setNetPicture(imgextraBeenlist.get(j).imgsrc, threeImgViewHolder.three_image) } } } else if (getItemViewType(position) == SMALL_IMG) { // 标准视图的情况 val smallImgViewHolder = holder as SmallImgViewHolder // 设置图片 setNetPicture(imageSrc, smallImgViewHolder.item_news_tv_img) holder.item_news_tv_title } ...... } } private fun setNetPicture(url: String?, img: ImageView) { Glide.with(img.context) .load(url) .placeholder(R.drawable.defaultbg) .into(img) } companion object { private val REPO_COMPARATOR = object : DiffUtil.ItemCallback<NewsListNormalBean>() { override fun areItemsTheSame(oldItem: NewsListNormalBean, newItem: NewsListNormalBean): Boolean = oldItem.title == newItem.title override fun areContentsTheSame(oldItem: NewsListNormalBean, newItem: NewsListNormalBean): Boolean = oldItem.url == newItem.url } } }我们通过 getItemViewType 方法,判断传入的当前的新闻需要使用哪种试图,然后在 onCreateViewHolder 方法中获取这种视图的类型,然后初始化相应的 ViewHolder 即可,最后再 onBindViewHolder 中绑定相应视图的数据。
以上就是整个新闻栏目的数据显示,剩下的图片频道以及视频频道的ui基本大同小异,就不在啰嗦了。 下面让我们来看看整个新闻界面的效果吧:
写道这里,其实一个简单项目也就完成了,说实话这真是一个相当简单的新闻UI,现在我们的项目已经完成了 80% 了,剩下的就是重头戏,视频播放了,视频播放会采取 ffmpeg 的方式,也就是说直接使用 ndk 开发的方式进行播放流视频,说实话,这方面的东西相当麻烦,尤其是博主并非搞 C/C++ 出身的,出现了问题都感觉相当的棘手,不知道怎么处理,但是还是需要将目标完成,各位看官就等着下一期吧!
如果大家对于项目的后续感兴趣可以关注我的个人微信公众号,每次更新都会在上面进行推送,还可以加入QQ群共同进步呢
微信公众号: Android实战之旅
微信号: unidirection
扫描关注:
也可以申请加群,大家一起沟通交流
QQ 群:799054441
也欢迎大家打赏,哈哈