代码地址如下:
http://www.demodashi.com/demo/11212.html
前言
本案例已经开源!如果你想免费下载,可以访问我的,所有案例均在上面,只求给个star。当然愿意支付小小金额请我喝茶也行(大学穷狗-.-)
一、准备工作
- 使用Android Studio开发
- 微信和QQ第三方sdk,需要自行申请(这个简单)
- 本案例使用提供的api,使用MVp+Material Design作为主体架构进行开发
- 体验完整功能,点击
二、程序实现
目录结构
目录结构如下,我按照功能分包:
实现思路
整体架构--MVP+Material
- 首先你得了解MVP架构在android中的使用,如果你还不了解,可以阅读我的
- 如果你不熟悉Material可以读
重点代码分析
如果讲述整个App,估计一篇文章说不清楚。那我干脆取其中一条线来分析。
下面主要分析文章列表--文章详情--文章分享主页文章列表
这里只选择Android文章模块进行介绍:
GankContract
public interface GankContract { interface View extends BaseView{ //错误 void showError(); //正在加载 void showLoading(); //停止加载 void Stoploading(); //显示数据列表 void showResult(ArrayList list); //网络错误 void showNotNetError(); } interface Presenter extends BasePresenter{ // 请求数据 void loadPosts(int PagerNum, boolean cleaing); //刷新数据 void reflush(); //加载更多 void loadMore(int PagerNum); //显示详情 void StartReading(int positon); //随便看看 void LookAround(); }}
GankFragment
Fragment的内容主要是文章列表,我们只分享重点:
//下拉刷新实现recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { boolean isScrollState=false; @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); LinearLayoutManager manager= (LinearLayoutManager) recyclerView.getLayoutManager(); //没有滚动时候 if (newState==RecyclerView.SCROLL_STATE_IDLE){ //获的最后一个可见的item int lastVisibilityItem=manager.findLastCompletelyVisibleItemPosition(); int totalItemCount=manager.getItemCount(); //判断是否滚动到底部并且是向下滑动 if (lastVisibilityItem==(totalItemCount-1)&&isScrollState){ presenter.loadMore(1); } } }//通知Presenter加载数据和设置item点击事件@Override public void showResult(ArrayListlist) { if (adapter==null){ Log.i(TAG, "showResult: "+list.size()); adapter=new GankNewsAdapter(list,getContext()); adapter.setItemOnClickListener(new OnRecyclerViewOnClickListener() { @Override public void onItemClick(View v, int position) { presenter.StartReading(position); } @Override public void onItemLongClick(View v, int position) { } }); recyclerView.setAdapter(adapter); }else { adapter.notifyDataSetChanged(); } }
GankPresenter
同样只分析重点代码:
//根据当前页数加载列表数据 @Override public void loadPosts(int PagerNum, final boolean cleaing) { CurrentPagerNum=PagerNum; if (cleaing) { view.showLoading(); } if (Network.networkConnected(context)) { model.load(Api.Gank_Android + PagerNum, new OnStringListener() { @Override public void onSuccess(String result) { try {// Log.i(TAG, "gankpresenter.model.load.result"+result); GankNews news = gson.fromJson(result, GankNews.class); //contenvalues只能存储基本类型的数据,像string,int之类的,不能存储对象这种东西,而HashTable却可以存储对象。// ContentValues values = new ContentValues(); if (cleaing) { list.clear(); } for (GankNews.Question item : news.getResults()) { /** * 1.数据库查重:首先检测数据库中是否已经储存过该条数据 * 2:因为每次重启后都是在网络上重新下载数据 如果是数据库已经存在的数据则不会重新加载,也导致了这些数据当前id值为空 * ,所有要绑定队友的id值. */ if (!queryIfIdExists(item.get_id())){ DbLiteOrm.insert(item, ConflictAlgorithm.Replace); }else { ArrayListganklist=App.DbLiteOrm.query(new QueryBuilder (GankNews.Question.class) .where(GankNews.Question.COL_ID+"=?",new String[]{item.get_id()})); GankNews.Question gankitem=ganklist.get(0); item.setId(gankitem.getId()); } list.add(item); } view.showResult(list); }catch (JsonSyntaxException e){ view.showError(); } view.Stoploading(); } @Override public void onError(VolleyError error) { view.Stoploading(); view.showError(); } }); } else { //更新列表缓存 因为详情页都是用webView呈现 所以缓存content为空 if (cleaing){ QueryBuilder query=new QueryBuilder(GankNews.Question.class); query.appendOrderDescBy("id"); query.limit(0,10*CurrentPagerNum); list.addAll(DbLiteOrm. query(query)); view.showResult(list); }else { view.showNotNetError(); } } }//判断数据库是否已经存在 public boolean queryIfIdExists(String _id){ ArrayList questionArrayList=App.DbLiteOrm.query(new QueryBuilder(GankNews.Question.class) .where(GankNews.Question.COL_ID+"=?",new String[]{_id})); if (questionArrayList.size()==0){ return false; } return true; }//传递当前点击item的信息,进入详情阅读@Override public void StartReading(int positon) { //每个item就是一组数据 GankNews.Question item=list.get(positon); Intent intent = new Intent(context, DetailActivity.class); intent.putExtra("type", BeanTeype.TYPE_Gank); intent.putExtra("id",list.get(positon).getId()); int id=list.get(positon).getId(); Log.i(TAG, "StartReading: "+id); intent.putExtra("_id", list.get(positon).get_id()); intent.putExtra("url",list.get(positon).getUrl()); intent.putExtra("title", list.get(positon).getDesc()); if (item.getImages()==null){ intent.putExtra("imgUrl", ""); }else { intent.putExtra("imgUrl", list.get(positon).getImages().get(0)); } /** * Content的startActivity方法,需要开启一个新的task。如果使用 Activity的startActivity方法, * 不会有任何限制,因为Activity继承自Context,重载了startActivity方法。 */ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); }//随便看看 随机选取@Override public void LookAround() { if (list.isEmpty()){ view.showError(); return; } StartReading(new Random().nextInt(list.size())); }
GankNewsAdapter
因为文章分两种:有图和无图。所有要进行分类加载
//判断是否有图和是否是底部加载item @Override public int getItemViewType(int position) { if (position==getItemCount()-1){ return TYPE_FOOTER; }if (list.get(position).getImages()==null){ return TYPE_NO_IMG; } return TYPE_NORMTAL; }//根据type加载不同ViewHolder@Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { switch (viewType){ case TYPE_NORMTAL: return new NormalViewHolder(inflater.inflate(R.layout.home_list_item_layout,parent,false),listener); case TYPE_FOOTER: return new FooterViewHolder(inflater.inflate(R.layout.list_footer,parent,false)); case TYPE_NO_IMG: return new NoImageViewHolder(inflater.inflate(R.layout.home_list_item_without_image,parent,false),listener); } return null; }//使用Glide加载图片。无图则不加载 @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (!(holder instanceof FooterViewHolder)){ GankNews.Question item=list.get(position); if (item!=null){ if (holder instanceof NormalViewHolder){ Glide.with(context) .load(item.getImages().get(0)) .asBitmap() .placeholder(R.mipmap.loading) .diskCacheStrategy(DiskCacheStrategy.SOURCE) .error(R.mipmap.loading) .centerCrop() .into(((NormalViewHolder) holder).imageView); ((NormalViewHolder) holder).textView.setText(item.getDesc()); }else if (holder instanceof NoImageViewHolder){ ((NoImageViewHolder) holder).textViewNoImg.setText(item.getDesc()); } } } }
详情页
DetailActivity
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.frame); if (savedInstanceState!=null){ detailFragment= (DetailFragment) getSupportFragmentManager().getFragment(savedInstanceState,"detailFragment"); }else { detailFragment=DetailFragment.newInstance(); getSupportFragmentManager().beginTransaction().replace(R.id.container,detailFragment).commit(); } //获取列表传过来的具体item数据 Intent intent=getIntent(); DetailPresenter presenter=new DetailPresenter(detailFragment,DetailActivity.this); presenter.setType((BeanTeype) intent.getSerializableExtra("type")); presenter.setId(intent.getIntExtra("id",1)); presenter.set_id(intent.getStringExtra("_id")); presenter.setTitle(intent.getStringExtra("title")); presenter.setUrl(intent.getStringExtra("url")); presenter.setImgUrl(intent.getStringExtra("imgUrl")); }
DetailContract
public class DetailContract { interface Presenter extends BasePresenter{ /** * 流浪器中打开 * 复制文本 * 复制连接 * 添加收藏或取消收藏 * 查询是否收藏 * 请求数据 * 分享到QQ * 分享到微信 * 分享到朋友圈 * 分享到微信收藏 */ void openInBrower(); void copyText(); void copyLink(); void addToOrDeleteFromBookMarks(); boolean queryIsBooksMarks(); void requestData(); void shareArticleToQQ(final MyQQListener listener); void shareArticleToWx(); void shareArticleToWxCommunity(); void shareArticleToWxCollect(); } interface View extends BaseView{ // 显示正在加载 void showLoading(); // 停止加载 void stopLoading(); // 显示加载错误 void showLoadingError(); // 显示分享时错误 void showSharingError(); // 正确获取数据后显示内容// void showResult(String result);// // 对于body字段的消息,直接接在url的内容 void showResultWithoutBody(String url); // 设置顶部大图 void showCover(String url); // 设置标题 void setTitle(String title); // 设置是否显示图片 void setImageMode(boolean showImage); // 用户选择在浏览器中打开时,如果没有安装浏览器,显示没有找到浏览器错误 void showBrowserNotFoundError(); // 显示已复制文字内容 void showTextCopied(); // 显示文字复制失败 void showCopyTextError(); // 显示已添加至收藏夹 void showAddedToBookmarks(); // 显示已从收藏夹中移除 void showDeletedFromBookmarks(); void showNotNetError(); void shareSuccess(); void shareError(); void shareCancel(); }}
DetailFragment
详情页主题是使用WebView显示,重点注意好设置属性和正确销毁:
@Override public void initView(View view) { ...... //webview设置属性 webview.getSettings().setJavaScriptEnabled(true); //缩放,设置为不能缩放可以防止页面上出现放大和缩小的图标 webview.getSettings().setBuiltInZoomControls(false); //缓存 webview.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); //开启DOM storage API功能 webview.getSettings().setDomStorageEnabled(true); //开启application Cache功能 webview.getSettings().setAppCacheEnabled(false); ..... }//早onDestroy中销毁WebView的对象@Override public void onDestroyView() { super.onDestroyView(); webview.removeAllViews(); webview.destroy(); webview=null; }
DetailPresenter
//复制链接地址 @Override public void copyLink() { ClipboardManager manager= (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); ClipData data=null; switch (type){ case TYPE_Gank: data=ClipData.newPlainText("text",url); } manager.setPrimaryClip(data); view.showTextCopied(); }//添加到收藏或者移除收藏 @Override public void addToOrDeleteFromBookMarks() { switch (type){ case TYPE_Gank: GankNews.Question gank= App.DbLiteOrm.queryById(id,GankNews.Question.class); if (queryIsBooksMarks()){ view.showDeletedFromBookmarks(); gank.mark=false; }else { view.showAddedToBookmarks(); gank.mark=true; } App.DbLiteOrm.update(gank); break; case TYPE_Front: FrontNews.Question front=App.DbLiteOrm.queryById(id,FrontNews.Question.class); if (queryIsBooksMarks()){ view.showDeletedFromBookmarks(); front.mark=false; }else { view.showAddedToBookmarks(); front.mark=true; } App.DbLiteOrm.update(front); break; case TYPE_IOS: IosNews.Question ios=App.DbLiteOrm.queryById(id,IosNews.Question.class); if (queryIsBooksMarks()){ view.showDeletedFromBookmarks(); ios.mark=false; }else { view.showAddedToBookmarks(); ios.mark=true; } App.DbLiteOrm.update(ios); } }//查询是否已经收藏 @Override public boolean queryIsBooksMarks() { if (_id ==null || type==null){ view.showLoadingError(); return false; } //true为已经收藏 false未收藏 switch (type){ case TYPE_Gank: GankNews.Question gank= App.DbLiteOrm.queryById(id,GankNews.Question.class); OrmLog.i(TAG,gank); boolean isMark=gank.mark; if (isMark){ return true; }else { return false; } case TYPE_Front: FrontNews.Question front=App.DbLiteOrm.queryById(id,FrontNews.Question.class); if (front.mark){ return true; }else { return false; } case TYPE_IOS: Log.i(TAG, "queryIsBooksMarks: "+id); IosNews.Question ios=App.DbLiteOrm.queryById(id,IosNews.Question.class); OrmLog.i(TAG,ios); if (ios.mark){ return true; }else { return false; } } return false; }//分享到QQ @Override public void shareArticleToQQ(MyQQListener listener) { //title == desc if (TextUtils.isEmpty(imgUrl)){ ShareSingleton.getInstance().shareToQQ((Activity) context,url,"推荐给你一篇文章",title, R.string.app_name, QQShare.SHARE_TO_QQ_FLAG_QZONE_ITEM_HIDE,listener); }else { ShareSingleton.getInstance().shareToQQ((Activity) context,url,"推荐给你一篇文章",title,imgUrl,R.string.app_name, QQShare.SHARE_TO_QQ_FLAG_QZONE_ITEM_HIDE,listener); } }//分享到微信 @Override public void shareArticleToWx() { //title == desc ShareSingleton.getInstance().shareWebToWx(url,"",title,true); }//分享到朋友圈 @Override public void shareArticleToWxCommunity() { //title == desc ShareSingleton.getInstance().shareWebToWx(url,"",title,false); }//分享到微信收藏 @Override public void shareArticleToWxCollect() { //title == desc ShareSingleton.getInstance().shareWebToWxCollect(url,"干货",title); }
ShareSingleton
关于微信和QQ分享的具体方法还得参考官方文章,我这里提出我自己写好的分享单例类
public class ShareSingleton { private Tencent mTencent; public static IWXAPI api; private static final int THUMB_SIZE = 150;//单例模式 private ShareSingleton() { } public static final ShareSingleton getInstance(){ return Singleton.INSTANCE; } private static class Singleton{ private static final ShareSingleton INSTANCE=new ShareSingleton(); } /** * 图文分享 图片来源网络 * !! 分享操作要在主线程中完成 * @param activity * @param targetUrl 这条分享消息被好友点击后的跳转URL。 * @param shareTitle 分享的标题, 最长30个字符。 * @param shareSummary 分享的消息摘要,最长40个字。 * @param netImgUrl 可填 分享图片的URL或者本地路径 * @param appName 手Q客户端顶部,替换“返回”按钮文字,如果为空,用返回代替 * @param shareToQQExtInt 额外选项 是否自动打开分享到QZone的对话框 * @param listener 分享回调接口 */ public void shareToQQ(Activity activity,String targetUrl,String shareTitle,String shareSummary, @Nullable String netImgUrl,@StringRes int appName,int shareToQQExtInt,MyQQListener listener){ if (mTencent==null){ mTencent=Tencent.createInstance(Constants.QQ_APP_ID,activity.getApplicationContext()); } final Bundle params = new Bundle(); params.putInt(QQShare.SHARE_TO_QQ_KEY_TYPE, QQShare.SHARE_TO_QQ_TYPE_DEFAULT); params.putString(QQShare.SHARE_TO_QQ_TARGET_URL,targetUrl); params.putString(QQShare.SHARE_TO_QQ_TITLE, shareTitle); params.putString(QQShare.SHARE_TO_QQ_SUMMARY, shareSummary ); params.putString(QQShare.SHARE_TO_QQ_IMAGE_URL, netImgUrl); params.putString(QQShare.SHARE_TO_QQ_APP_NAME,activity.getString(appName)); params.putInt(QQShare.SHARE_TO_QQ_EXT_INT, shareToQQExtInt); mTencent.shareToQQ(activity, params, listener); } /** * 文章分享 无图 * !! 分享操作要在主线程中完成 * @param activity * @param targetUrl 这条分享消息被好友点击后的跳转URL。 * @param shareTitle 分享的标题, 最长30个字符。 * @param shareSummary 分享的消息摘要,最长40个字。 * @param appName 手Q客户端顶部,替换“返回”按钮文字,如果为空,用返回代替 * @param shareToQQExtInt 额外选项 是否自动打开分享到QZone的对话框 * @param listener 分享回调接口 */ public void shareToQQ(Activity activity,String targetUrl,String shareTitle,String shareSummary ,@StringRes int appName,int shareToQQExtInt,MyQQListener listener){ if (mTencent==null){ mTencent=Tencent.createInstance(Constants.QQ_APP_ID,activity.getApplicationContext()); } final Bundle params = new Bundle(); params.putInt(QQShare.SHARE_TO_QQ_KEY_TYPE, QQShare.SHARE_TO_QQ_TYPE_DEFAULT); params.putString(QQShare.SHARE_TO_QQ_TARGET_URL,targetUrl); params.putString(QQShare.SHARE_TO_QQ_TITLE, shareTitle); params.putString(QQShare.SHARE_TO_QQ_SUMMARY, shareSummary ); params.putString(QQShare.SHARE_TO_QQ_APP_NAME,activity.getString(appName)); params.putInt(QQShare.SHARE_TO_QQ_EXT_INT, shareToQQExtInt); mTencent.shareToQQ(activity, params, listener); } /** * 分享文章到微信/朋友圈 * @param webUrl * @param webTitle * @param webDesc * @param isShareFriend */ public void shareWebToWx(@NonNull String webUrl,String webTitle,String webDesc,boolean isShareFriend){// 注册操作也可以写死在Application中 // 通过WXAPIFactory工厂,获取IWXAPI的实例 api=WXAPIFactory.createWXAPI(App.getContext(),Constants.WX_APP_ID,true); // 将该app注册到微信 api.registerApp(Constants.WX_APP_ID); //初始化一个WXWebpageObject对象,填写url WXWebpageObject webpag=new WXWebpageObject(); webpag.webpageUrl=webUrl; //用WXWebpageObject对象初始化一个WXMediaMessage对象 填写标题和描述 WXMediaMessage msg=new WXMediaMessage(webpag); msg.title=webTitle; msg.description=webDesc; //构造一个Req SendMessageToWX.Req req=new SendMessageToWX.Req(); req.transaction=buildTransaction("webpage");//transaction 字段用于唯一标识一个请求 req.message= msg; req.scene=isShareFriend ? SendMessageToWX.Req.WXSceneSession : SendMessageToWX.Req.WXSceneTimeline; api.sendReq(req); } /** * 分享文章到微信收藏 * @param webUrl * @param webTitle * @param webDesc */ public void shareWebToWxCollect(@NonNull String webUrl, String webTitle, String webDesc){// 注册操作也可以写死在Application中 // 通过WXAPIFactory工厂,获取IWXAPI的实例 api=WXAPIFactory.createWXAPI(App.getContext(),Constants.WX_APP_ID,true); // 将该app注册到微信 api.registerApp(Constants.WX_APP_ID); //初始化一个WXWebpageObject对象,填写url WXWebpageObject webpag=new WXWebpageObject(); webpag.webpageUrl=webUrl; //用WXWebpageObject对象初始化一个WXMediaMessage对象 填写标题和描述 WXMediaMessage msg=new WXMediaMessage(webpag); msg.title=webTitle; msg.description=webDesc; //构造一个Req SendMessageToWX.Req req=new SendMessageToWX.Req(); req.transaction=buildTransaction("webpage");//transaction 字段用于唯一标识一个请求 req.message= msg; req.scene=SendMessageToWX.Req.WXSceneFavorite; api.sendReq(req); }
这篇文章就分析这么多,如果你想了解跟多,欢迎下载源码。主要部分源码都有注释
三、部分运行效果
四、其他补充
如果你有问题可以提交到Github的issue上,也可以给我发邮件。我的邮件是yeshuwei.swy@gmail.com
Android--从零开始开发一款文章阅读APP
代码地址如下:
http://www.demodashi.com/demo/11212.html
注:本文著作权归作者,由demo大师代发,拒绝转载,转载需要作者授权