TouTiao开源项目 分析笔记13 最后一个订阅号的实现主页面
1.实现订阅号的基础类
1.1.本地订阅号的Bean类==>MediaChannelBean
public class MediaChannelBean implements Parcelable { public static final Creator<MediaChannelBean> CREATOR = new Creator<MediaChannelBean>() { @Override public MediaChannelBean createFromParcel(Parcel in) { return new MediaChannelBean(in); } @Override public MediaChannelBean[] newArray(int size) { return new MediaChannelBean[size]; } }; private String id; private String name; private String avatar; private String type; private String followCount; private String descText; private String url; public MediaChannelBean() { } protected MediaChannelBean(Parcel in) { id = in.readString(); name = in.readString(); avatar = in.readString(); type = in.readString(); followCount = in.readString(); descText = in.readString(); url = in.readString(); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(id); dest.writeString(name); dest.writeString(avatar); dest.writeString(type); dest.writeString(followCount); dest.writeString(descText); dest.writeString(url); } @Override public int describeContents() { return 0; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAvatar() { return avatar; } public void setAvatar(String avatar) { this.avatar = avatar; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getFollowCount() { return followCount; } public void setFollowCount(String followCount) { this.followCount = followCount; } public String getDescText() { return descText; } public void setDescText(String descText) { this.descText = descText; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } }
1.2.数据库建立订阅号的基础表==>MediaChannelTable
public class MediaChannelTable { /** * 头条号信息表 */ public static final String TABLENAME = "MediaChannelTable"; /** * 字段部分 */ public static final String ID = "id"; public static final String NAME = "name"; public static final String AVATAR = "avatar"; public static final String TYPE = "type"; public static final String FOLLOWCOUNT = "followCount"; public static final String DESCTEXT = "descText"; public static final String URL = "url"; /** * 字段ID 数据库操作建立字段对应关系 从0开始 */ public static final int ID_ID = 0; public static final int ID_NAME = 1; public static final int ID_AVATAR = 2; public static final int ID_TYPE = 3; public static final int ID_FOLLOWCOUNT = 4; public static final int ID_DESCTEXT = 5; public static final int ID_URL = 6; /** * 创建表 */ public static final String CREATE_TABLE = "create table if not exists " + TABLENAME + "(" + ID + " text primary key, " + NAME + " text, " + AVATAR + " text, " + TYPE + " text, " + FOLLOWCOUNT + " text, " + DESCTEXT + " text, " + URL + " text) "; }
1.3.实现最底层订阅号的数据库操作
public class MediaChannelDao { private SQLiteDatabase db; public MediaChannelDao(){ this.db= DatabaseHelper.getDatabase(); } public void initData(){ add("4377795668", "新华网", "http://p2.pstatp.com/large/3658/7378365093", "news", "", "传播中国,报道世界;权威声音,亲切表达。", "http://toutiao.com/m4377795668/"); add("52445544609", "互联网的这点事", "http://p3.pstatp.com/large/ef300164e786ff295da", "news", "", "每天为你速递最新、最鲜、最有料的互联网科技资讯!", "http://toutiao.com/m52445544609/"); } public boolean add(String id, String name, String avatar, String type, String followCount, String descText, String url) { ContentValues values = new ContentValues(); values.put(MediaChannelTable.ID, id); values.put(MediaChannelTable.NAME, name); values.put(MediaChannelTable.AVATAR, avatar); values.put(MediaChannelTable.TYPE, type); values.put(MediaChannelTable.FOLLOWCOUNT, followCount); values.put(MediaChannelTable.DESCTEXT, descText); values.put(MediaChannelTable.URL, url); long result = db.insert(MediaChannelTable.TABLENAME, null, values); return result != -1; } public List<MediaChannelBean> queryAll() { Cursor cursor = db.query(MediaChannelTable.TABLENAME, null, null, null, null, null, null); List<MediaChannelBean> list = new ArrayList<>(); while (cursor.moveToNext()) { MediaChannelBean bean = new MediaChannelBean(); bean.setId(cursor.getString(MediaChannelTable.ID_ID)); bean.setName(cursor.getString(MediaChannelTable.ID_NAME)); bean.setAvatar(cursor.getString(MediaChannelTable.ID_AVATAR)); bean.setType(cursor.getString(MediaChannelTable.ID_TYPE)); bean.setFollowCount(cursor.getString(MediaChannelTable.ID_FOLLOWCOUNT)); bean.setDescText(cursor.getString(MediaChannelTable.ID_DESCTEXT)); bean.setUrl(cursor.getString(MediaChannelTable.ID_URL)); list.add(bean); } cursor.close(); return list; } public boolean queryIsExist(String id) { Cursor cursor = db.query(MediaChannelTable.TABLENAME, null, MediaChannelTable.ID + "=?", new String[]{id}, null, null, null); if (cursor.moveToNext()) { cursor.close(); return true; } cursor.close(); return false; } public boolean delete(String mediaId) { int id = db.delete(MediaChannelTable.TABLENAME, MediaChannelTable.ID + "=?", new String[]{mediaId}); return id != -1; } }
2.构建订阅号视图页面
2.1.创建订阅号视图布局==>fragment_media.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/windowBackground" android:orientation="vertical" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <android.support.v4.widget.SwipeRefreshLayout android:id="@+id/refresh_layout" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.design.widget.CoordinatorLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tv_desc" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="@string/media_hint_desc"/> <android.support.v7.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" android:fadeScrollbars="true" android:scrollbarFadeDuration="1" android:scrollbars="vertical"/> </android.support.design.widget.CoordinatorLayout> </android.support.v4.widget.SwipeRefreshLayout> </LinearLayout>
预览图片:
2.2.定义一个长按监听事件==>长按弹出提示框
public interface IOnItemLongClickListener { /** * RecyclerView Item长按事件 */ void onLongClick(View view, int position); }
2.3.构建一个订阅号视图类
public class MediaChannelView extends RxFragment implements SwipeRefreshLayout.OnRefreshListener { private static final String TAG = "MediaChannelView"; private static MediaChannelView instance = null; private RecyclerView recyclerView; private SwipeRefreshLayout swipeRefreshLayout; private MultiTypeAdapter adapter; private MediaChannelDao dao = new MediaChannelDao(); private TextView tv_desc; private String isFirstTime = "isFirstTime"; private List<MediaChannelBean> list; public static MediaChannelView getInstance() { if (instance == null) { instance = new MediaChannelView(); } return instance; } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_media, container, false); initView(view); initData(); return view; } @Override public void onResume() { super.onResume(); swipeRefreshLayout.setColorSchemeColors(SettingUtil.getInstance().getColor()); setAdapter(); } private void initData() { SharedPreferences editor = getActivity().getSharedPreferences(TAG, Context.MODE_PRIVATE); boolean result = editor.getBoolean(isFirstTime, true); if (result) { dao.initData(); editor.edit().putBoolean(isFirstTime, false).apply(); } setAdapter(); } private void setAdapter() { Observable .create(new ObservableOnSubscribe<List<MediaChannelBean>>() { @Override public void subscribe(@NonNull ObservableEmitter<List<MediaChannelBean>> e) throws Exception { list = dao.queryAll(); e.onNext(list); } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .compose(this.<List<MediaChannelBean>>bindUntilEvent(FragmentEvent.DESTROY)) .subscribe(new Consumer<List<MediaChannelBean>>() { @Override public void accept(@NonNull List<MediaChannelBean> list) throws Exception { adapter.setItems(list); adapter.notifyDataSetChanged(); if (list.size() == 0) { tv_desc.setVisibility(View.VISIBLE); } else { tv_desc.setVisibility(View.GONE); } } }); } private void initView(View view) { recyclerView = view.findViewById(recycler_view); recyclerView.setHasFixedSize(true); recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); swipeRefreshLayout = view.findViewById(R.id.refresh_layout); swipeRefreshLayout.setColorSchemeColors(SettingUtil.getInstance().getColor()); swipeRefreshLayout.setOnRefreshListener(this); tv_desc = view.findViewById(R.id.tv_desc); IOnItemLongClickListener listener = new IOnItemLongClickListener() { @Override public void onLongClick(View view, int position) { final MediaChannelBean item = list.get(position); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setMessage("取消订阅\" " + item.getName() + " \"?"); builder.setPositiveButton(R.string.button_enter, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { new Thread(new Runnable() { @Override public void run() { dao.delete(item.getId()); setAdapter(); } }).start(); dialog.dismiss(); } }); builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); builder.show(); } }; adapter = new MultiTypeAdapter(); Register.registerMediaChannelItem(adapter, listener); recyclerView.setAdapter(adapter); } @Override public void onRefresh() { swipeRefreshLayout.setRefreshing(true); setAdapter(); swipeRefreshLayout.setRefreshing(false); } @Override public void onDestroyView() { super.onDestroyView(); if (instance != null) { instance = null; } } }
注意:如果setAdapter简化成下面这个函数,直观效果一直。
private void setAdapter(){ list=dao.queryAll(); adapter.setItems(list); adapter.notifyDataSetChanged(); if (list.size() == 0) { tv_desc.setVisibility(View.VISIBLE); } else { tv_desc.setVisibility(View.GONE); } }
但是,如果订阅号的数量很多很多后,这种效果远远不如订阅的方法性能好。
所以我们统一就用订阅的方式吧。
而且更加重要的是,如果没有数据的时候,
这里要进行界面操作,如果直接在io线程会发生异常的。
2.4.发现Register中还没有注册类型以及传入监听求
public static void registerMediaChannelItem(@NonNull MultiTypeAdapter adapter, @NonNull IOnItemLongClickListener listener) { adapter.register(MediaChannelBean.class, new MediaChannelViewBinder(listener)); }
这里发现了监听器传进去了,说明绑定类中要给每一行都要设置这个listener
然后这里又看到MediaChannelViewBinder绑定类还没有实现呢!
第三点主要讲解这个简单的绑定类。
3.订阅号视图绑定类
3.1.首先看一下视图布局吧。==>item_media_channel.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/viewBackground"> <LinearLayout android:id="@+id/content" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground" android:padding="8dp"> <com.meiji.toutiao.widget.CircleImageView android:id="@+id/cv_avatar" android:layout_width="52dp" android:layout_height="52dp" android:layout_gravity="center" android:scaleType="centerCrop" android:src="@color/textColorPrimary"/> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginLeft="8dp" android:layout_marginStart="8dp"> <TextView android:id="@+id/tv_mediaName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_toLeftOf="@+id/tv_followCount" android:layout_toStartOf="@+id/tv_followCount" android:maxLines="1" android:textSize="16sp" android:textStyle="bold" tools:text="新华国际"/> <TextView android:id="@+id/tv_followCount" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentEnd="true" android:layout_alignParentRight="true" android:text="" tools:text="111人关注"/> <TextView android:id="@+id/tv_descText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/tv_mediaName" android:layout_marginTop="4dp" android:ellipsize="end" android:maxLines="1" android:textSize="14sp" tools:text="中国军力超越日本 日本为什么不怕中国?普京一句话让国人顿悟"/> </RelativeLayout> </LinearLayout> <View android:id="@+id/divider" android:layout_width="match_parent" android:layout_height="1px" android:layout_below="@+id/content" android:background="@color/line_divider"/> </RelativeLayout>
视图效果预览:
3.2.然后就是这个绑定类了==>MediaChannelViewBinder
package com.jasonjan.headnews.binder.media; import android.content.Context; import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import com.jakewharton.rxbinding2.view.RxView; import com.jasonjan.headnews.R; import com.jasonjan.headnews.bean.media.MediaChannelBean; import com.jasonjan.headnews.interfaces.IOnItemLongClickListener; import com.jasonjan.headnews.main.ErrorAction; import com.jasonjan.headnews.util.ImageLoader; import com.jasonjan.headnews.widget.CircleImageView; import java.util.concurrent.TimeUnit; import io.reactivex.functions.Consumer; import me.drakeet.multitype.ItemViewBinder; /** * Created by JasonJan on 2017/12/14. */ public class MediaChannelViewBinder extends ItemViewBinder<MediaChannelBean,MediaChannelViewBinder.ViewHolder> { private IOnItemLongClickListener listener; public MediaChannelViewBinder(IOnItemLongClickListener listener) { this.listener = listener; } @NonNull @Override protected MediaChannelViewBinder.ViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { View view = inflater.inflate(R.layout.item_media_channel, parent, false); return new ViewHolder(view, listener); } @Override protected void onBindViewHolder(@NonNull final ViewHolder holder, @NonNull final MediaChannelBean item){ try { final Context context = holder.itemView.getContext(); String url = item.getAvatar(); ImageLoader.loadCenterCrop(context, url, holder.cv_avatar, R.color.viewBackground); holder.tv_mediaName.setText(item.getName()); holder.tv_descText.setText(item.getDescText()); RxView.clicks(holder.itemView) .throttleFirst(1, TimeUnit.SECONDS) .subscribe(new Consumer<Object>() { @Override public void accept(@io.reactivex.annotations.NonNull Object o) throws Exception { // MediaHomeActivity.launch(item.getId()); } }); } catch (Exception e) { ErrorAction.print(e); } } public class ViewHolder extends RecyclerView.ViewHolder implements View.OnLongClickListener { private CircleImageView cv_avatar; private TextView tv_mediaName; private TextView tv_followCount; private TextView tv_descText; private IOnItemLongClickListener listener; public ViewHolder(View itemView, IOnItemLongClickListener listener) { super(itemView); this.cv_avatar = itemView.findViewById(R.id.cv_avatar); this.tv_mediaName = itemView.findViewById(R.id.tv_mediaName); this.tv_followCount = itemView.findViewById(R.id.tv_followCount); this.tv_descText = itemView.findViewById(R.id.tv_descText); this.listener = listener; itemView.setOnLongClickListener(this); } @Override public boolean onLongClick(View v) { if (listener != null) { listener.onLongClick(v, getLayoutPosition()); return true; } return false; } } }
这里先理一理RxView.clicks思路。
这个ViewHolder继承于RecyclerView.ViewHolder
绑定类由于继承ItemViewBinder,不得不去实现onBindViewHolder<T,这里面的ViewHolder>
所以在这里面处理ViewHolder的holdr.itemView的时候
要用到RxView.clicks(view)
如下方的代码:
RxView.clicks(holder.itemView) .throttleFirst(1, TimeUnit.SECONDS) .subscribe(new Consumer<Object>() { @Override public void accept(@io.reactivex.annotations.NonNull Object o) throws Exception { MediaHomeActivity.launch(item.getId()); } });
RxView代表着用了第三方库,结合了RxJava。
采用订阅的方式处理点击事件。
当然也可以不用这种方式,不过我还没发现这种方式的好处。
这里的ThrottleFirst操作符会定期发射这个时间段里源Observable发射的第一个数据。
4.效果预览
4.1.目前完成的工作
新闻的主页面三种大类型
图片的一种大类型(也只用了一种)
视频的一种大类型(采用了新闻主页面的其中一种)
订阅号的主页面的一种大类型(也只采用了一种)
然后还有一些点击事件,调转到相应的活动页面还未实现。
4.2.目前手机真实数据效果