Android 高仿知乎日报(1)

来源:互联网 发布:佛山淘宝代运营 编辑:程序博客网 时间:2024/04/27 23:51

个人蛮喜欢没事看看知乎的,前阵子凑巧也在网上搜到了知乎日报的API,详情见某位开发者在Github上的分享:知乎日报 API 分析

靠着这个,我就做了一个高仿知乎日报的小应用

这里写图片描述

这里写图片描述

动态图看起来不怎么流畅,其实真机运行的话还是很流程的,毕竟这只是一个纯粹的阅读类APP,并没有加入什么乱七八糟的功能,且界面主要都是用Fragment呈现的

在这里就来分享下我的源代码并记录下开发流程吧

一、工程介绍

工程不算太复杂,Activity用到了三个而已,包括主界面Activity,文章内容Activity,加上启动界面Activity,其实真正有作用的也就两个而已
除此以外就是自定义接口有点多,达到了九个。。

这里写图片描述

其次,用到的开源库有三个

  • FastJson 用来解析JSON数据,据说解析速度最快
  • universal-image-loader 用来下载、显示、缓存图片
  • android-async-http 网络访问框架,还是挺方便的

这里写图片描述

二、知乎日报API分析

获取最新文章的接口:http://news-at.zhihu.com/api/4/news/latest

返回的数据为JSON格式的

{    "date": "20160804",    "stories": [        {            "images": [                "http://pic4.zhimg.com/b247609f382ec5d097c51d468975fd1b.jpg"            ],            "type": 0,            "id": 8644697,            "ga_prefix": "080409",            "title": "以现有的技术,能不能把地沟油检测出来?"        },        {            "images": [                "http://pic4.zhimg.com/b556013584ac5f190f1a343aad2b75bb.jpg"            ],            "type": 0,            "id": 8645106,            "ga_prefix": "080408",            "title": "写给产品 / 市场 / 运营的数据抓取黑科技教程"        },        {            "images": [                "http://pic2.zhimg.com/eaf3fb69ddfe636c6c4c40c2c76029c5.jpg"            ],            "type": 0,            "id": 8644252,            "ga_prefix": "080407",            "title": "里约奥运已经开始啦~这里是一份熬夜与早起的时间表"        }}

其中,date代表当日时间,stories为一个JSON数组,包含了一组文章数据,如文章标题,文章配图,文章ID等

依靠得到的文章ID,再加上获取文章详细内容的接口http://news-at.zhihu.com/api/4/news/8643890就可以获取数据了,其中8643890为文章ID
获取到的数据格式为:

{"body":"<div class=\"main-wrap content-wrap\">\n<div class=\"headline\">\n\n<div class=\"img-place-holder\"><\/div>\n\n\n\n<\/div>\n\n<div class=\"content-inner\">\n\n\n\n\n<div class=\"question\">\n<h2 class=\"question-title\"><\/h2>\n\n<div class=\"answer\">\n\n<div class=\"meta\">\n<img class=\"avatar\" src=\"http:\/\/pic4.zhimg.com\/4ac31ef63_is.jpg\">\n<span class=\"author\">刘明,<\/span><span class=\"bio\">非典型法律人<\/span>\n<\/div>\n\n<div class=\"content\">\n<p>网络用户协议的订约方式决定了,如果没有外部力量干涉,网络用户协议中会存在大量不利于网络用户的&ldquo;霸王条款&rdquo;。总体看来,这些不公平可以分为程序不公平和实体不公平两类,在此简单列举一二具有共同性的例子:<\/p>\r\n<p><strong>程序不公平<\/strong><\/p>\r\n<p>1.以超链接方式展示合同条款,使网络用户很容易忽略用户协议,以及在用户协议中嵌套的其他协议。<\/p>\r\n<p>2.一揽子同意。很多网站的用户协议实际上是由多份协议共同组成的,但网络用户通常只需要点击一次同意,就被视为已经一揽子的同意了所有协议,而实际上由于这些协议大多通过超链接方式提供,所以用户可能根本就没注意到它们的存在。<\/p>\r\n<p>3.对网络服务提供者权利保留条款和限制网络用户权利的条款缺少明显标注,网络用户即使浏览用户协议,也很可能忽略这些条款。<\/p>\r\n<p><strong>实体不公平<\/strong><\/p>\r\n<p>1.网络服务提供者权利保留条款。如网络游戏运营商保留网络游戏中游戏人物和装备的所有权,玩家只有使用权。<\/p>\r\n<p>2.个人信息授权收集条款。如很多用户协议中都有约定,网络用户同意网络服务提供者在很广的范围内收集其在使用网络服务过程中产生的个人信息,并同意将这些信息交给第三方使用。<\/p>\r\n<p>3.限制网络用户的处分权利。如某公司禁止网络用户交易聊天软件号码,网游运营商也经常禁止用户交易其网络游戏中的人物和装备。<\/p>\r\n<p>4.随时更改、终止合同的权利。几乎所有用户协议中都会有这条。<\/p>\r\n<p>5.网络服务提供者对技术故障免责。几乎所有用户协议中都会有这条,约定无论是否因不可抗力引起技术故障,一律免责。<\/p>\r\n<p>6.纠纷解决条款。将网络用户与网络服务提供者之间法律纠纷的管辖权约定在方便网络服务提供者应诉的地方,可能给网络用户通过司法渠道主张权利造成困难。<\/p>\r\n<p>除此之外,不同的网络服务提供者还会根据自己的经营需要,制定一些特殊的不公平条款。例如某网络专车的服务条款在很长一段时间以来,都约定乘客与司机之间是个人劳务关系,而非运输合同关系,一旦出现交通事故导致车外人受伤,实际上应由雇主,也就是乘客来承担赔偿责任。当然,在《网络预约出租车经营服务管理暂行办法》出台后,专车平台需要承担承运人责任,这种约定也就不存在了。<\/p>\n<\/div>\n<\/div>\n\n\n<div class=\"view-more\"><a href=\"http:\/\/www.zhihu.com\/question\/26978264\">查看知乎讨论<span class=\"js-question-holder\"><\/span><\/a><\/div>\n\n<\/div>\n\n\n<\/div>\n<\/div>","image_source":"Yestone.com 版权图片库","title":"软件安装、网站注册的用户协议里有哪些著名的陷阱条款?","image":"http:\/\/pic2.zhimg.com\/9f1fc77ede31203a3117b205a7120239.jpg","share_url":"http:\/\/daily.zhihu.com\/story\/8643890","js":[],"ga_prefix":"080407","images":["http:\/\/pic1.zhimg.com\/dd1487cd8011ec69b3ca45c0faa87bc4.jpg"],"type":0,"id":8643890,"css":["http:\/\/news-at.zhihu.com\/css\/news_qa.auto.css?v=4b3e3"]}

即为html格式的字符串,文章详情页的界面就是用WebView来呈现的,毕竟数据格式千遍万户,用WebView是最简单且呈现效果最好的方式
其中还包含了CSS文件的地址:"css":["http:\/\/news-at.zhihu.com\/css\/news_qa.auto.css?v=4b3e3"]
这个需要下载到本地然后导入工程中,因为CSS文件是连续使用的且一般不会频繁更改的,所以可以直接作为本地资源,不需用户下载

其他接口的含义可自行查看

三、代码

主界面包含下拉刷新功能,且有侧滑菜单,这个都用官方提供的支持库即可,activity_main.xml的布局文件如下:

<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    android:id="@+id/drawerLayout"    android:layout_width="match_parent"    android:layout_height="match_parent">    <android.support.v4.widget.SwipeRefreshLayout        android:id="@+id/sr"        android:layout_width="match_parent"        android:layout_height="match_parent">        <LinearLayout            android:layout_width="match_parent"            android:layout_height="match_parent"            android:orientation="vertical">            <android.support.v7.widget.Toolbar                android:id="@+id/toolbar"                android:layout_width="match_parent"                android:layout_height="?attr/actionBarSize"                android:background="?attr/colorPrimaryDark"                android:fitsSystemWindows="true"                app:popupTheme="@style/ThemeOverlay.AppCompat.Light"                app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />            <FrameLayout                android:id="@+id/fl_content"                android:layout_width="match_parent"                android:layout_height="match_parent" />        </LinearLayout>    </android.support.v4.widget.SwipeRefreshLayout>    <fragment        android:id="@+id/menuFragment"        android:name="czy.kankan.myapplication.Fragment.MenuFragment"        android:layout_width="300dp"        android:layout_height="match_parent"        android:layout_gravity="left" /></android.support.v4.widget.DrawerLayout>

当中,id为fl_content的FrameLayout即用来容纳Fragment

 <FrameLayout   android:id="@+id/fl_content"   android:layout_width="match_parent"   android:layout_height="match_parent" />

由动态图可以看到,程序除了主界面外,还包括多个主题分类,如果每次都要启动个Activity来回切换的话,未免太费力了,所以这里采用Fragment来呈现
这里首先新建个BaseFragment作为所有Fragmnet的父类,并定义一些通用的函数

public abstract class BaseFragment extends Fragment {    protected Activity mActivity;    @Override    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {        mActivity = getActivity();        return initView(inflater, container, savedInstanceState);    }    @Override    public void onActivityCreated(Bundle savedInstanceState) {        super.onActivityCreated(savedInstanceState);        initData();    }    @Override    public void onDestroy() {        super.onDestroy();        mActivity = null;    }    protected abstract View initView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState);    protected void initData() {    }    protected void hint(View view, String content, int color) {        Snackbar snackbar = Snackbar.make(view, content, Snackbar.LENGTH_SHORT);        snackbar.getView().setBackgroundColor(color);        snackbar.show();    }    public MainActivity getRootActivity() {        return (MainActivity) mActivity;    }}

主界面的文章列表均是用RecycleView呈现,包括顶部的循环播放图片的Banner均是RecycleView的一个子项
所以,MainFragment只要包含一个RecycleView和一个FloatingActionButton即可
activity_ article_list.xml文件如下:

<?xml version="1.0" encoding="utf-8"?><FrameLayout 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.support.v7.widget.RecyclerView        android:id="@+id/articleList"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:background="#e2dedf" />    <android.support.design.widget.FloatingActionButton        android:id="@+id/fab"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_gravity="bottom|end"        android:layout_margin="@dimen/fab_margin"        android:src="@drawable/top"        app:backgroundTint="#bae9eff1" /></FrameLayout>

FloatingActionButton用来当点击后滑动到顶部

既然是使用RecycleView,也就需要一个ArticleListAdapter继承于RecyclerView.Adapter<RecyclerView.ViewHolder>

public class ArticleListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {    private List<Stories> storiesList;    private LayoutInflater inflater;    private Context context;    private final int TYPE_TOP = 0;    private final int TYPE_ARTICLE = 1;    private final int TYPE_FOOTER = 2;    public OnLoadTopArticleListener loadTopArticleListener;    private OnSlideToTheBottomListener slideListener;    private OnArticleItemClickListener clickListener;    private ArticleListTopHolder articleListTopHolder;    public ArticleListAdapter(Context context) {        this.context = context;        init();    }    private void init() {        inflater = LayoutInflater.from(context);        storiesList = new ArrayList<>();        //文章列表点击事件监听        clickListener = new OnArticleItemClickListener() {            @Override            public void OnItemClickListener(int position) {                int id = storiesList.get(position - 1).getId();                Intent intent = new Intent(context, ArticleContentActivity.class);                Bundle bundle = new Bundle();                bundle.putInt("ID", id);                intent.putExtras(bundle);                context.startActivity(intent);            }        };        //加载banner文章事件监听        loadTopArticleListener = new OnLoadTopArticleListener() {            @Override            public void onSuccess(List<TopStories> topStoriesList) {                if (articleListTopHolder != null) {                    articleListTopHolder.banner.update(topStoriesList);                    articleListTopHolder.banner.startPlay();                    notifyDataSetChanged();                }            }            @Override            public void onFailure() {            }        };    }    @Override    public int getItemViewType(int position) {        if (position == 0) {            return TYPE_TOP;        }        if (position + 1 == getItemCount()) {            return TYPE_FOOTER;        }        return TYPE_ARTICLE;    }    @Override    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {        View view;        if (viewType == TYPE_ARTICLE) {            view = inflater.inflate(R.layout.article_list_item, parent, false);            return new ArticleListHolder(view);        } else if (viewType == TYPE_FOOTER) {            view = inflater.inflate(R.layout.fooder, parent, false);            return new ArticleListFooterHolder(view);        }        view = inflater.inflate(R.layout.banner_layout, parent, false);        return new ArticleListTopHolder(view);    }    @Override    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {        switch (getItemViewType(position)) {            case TYPE_ARTICLE:                ArticleListHolder articleListHolder = (ArticleListHolder) holder;                Constant.getImageLoader().displayImage(storiesList.get(position - 1).getImages().get(0),                        articleListHolder.articleImage, Constant.getDisplayImageOptions());                articleListHolder.articleTitle.setText(storiesList.get(position - 1).getTitle());                articleListHolder.setItemClickListener(clickListener);                break;            case TYPE_FOOTER:                //只有当文章数量不为零时才启用事件监听                if (slideListener != null && storiesList != null && storiesList.size() > 0) {                    slideListener.onSlideToTheBottom();                }                break;            case TYPE_TOP:                articleListTopHolder = (ArticleListTopHolder) holder;                break;        }    }    @Override    public int getItemCount() {        return storiesList.size() + 1;    }    //当刷新文章列表时使用    public void setData(ArticleLatest articleLatest) {        storiesList.clear();        storiesList.addAll(articleLatest.getStories());    }    //当加载下一页文章内容时使用    public void addData(List<Stories> storiesList) {        this.storiesList.addAll(storiesList);    }    public void setSlideToTheBottomListener(OnSlideToTheBottomListener slideListener) {        this.slideListener = slideListener;    }}

函数getItemViewType(int position)用来获取子项的类型,这里分为三种,即顶部Banner,文章列表,底部用来呈现“正在加载”字样的TextView
里面用到了多个回调函数,例如Banner数据加载回调,文章列表点击事件监听等

而MainFragment就要来进行具体的数据加载操作了,当数据为空时显示默认数据,当数据获取成功后,则刷新Adapter

public class MainFragment extends BaseFragment {    //文章列表    private RecyclerView recyclerView;    private FloatingActionButton floatingActionButton;    private ArticleListAdapter adapter;    private OnLoadLatestArticleListener latestListener;    private OnLoadBeforeArticleListener beforeListener;    private boolean flag;    @Override    protected View initView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {        View view = inflater.inflate(R.layout.activity_article_list, container, false);        recyclerView = (RecyclerView) view.findViewById(R.id.articleList);        floatingActionButton = (FloatingActionButton) view.findViewById(R.id.fab);        floatingActionButton.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                recyclerView.smoothScrollToPosition(0);            }        });        recyclerView.setLayoutManager(new LinearLayoutManager(mActivity, LinearLayoutManager.VERTICAL, false));        return view;    }    @Override    protected void initData() {        adapter = new ArticleListAdapter(mActivity);        recyclerView.setAdapter(adapter);        //加载最新文章事件监听        latestListener = new OnLoadLatestArticleListener() {            @Override            public void onSuccess(ArticleLatest articleLatest) {                adapter.setData(articleLatest);                getRootActivity().setDate(articleLatest.getDate());                List<TopStories> topStoriesList = articleLatest.getTop_stories();                if (adapter.loadTopArticleListener != null) {                    adapter.loadTopArticleListener.onSuccess(topStoriesList);                }                stopRefresh();                if (!flag) {                    flag = true;                } else {                    hint(recyclerView, "已经是最新文章啦", Color.parseColor("#0099CC"));                }                //加载最新文章成功后在后台再加载下一页                getBeforeArticleList();            }            @Override            public void onFailure() {                if (mActivity != null) {                    hint(recyclerView, "好奇怪,文章加载不来", Color.parseColor("#0099CC"));                }                stopRefresh();            }        };        //加载过去文章事件监听        beforeListener = new OnLoadBeforeArticleListener() {            @Override            public void onSuccess(ArticleBefore articleBefore) {                adapter.addData(articleBefore.getStories());                adapter.notifyDataSetChanged();                getRootActivity().setDate(articleBefore.getDate());            }            @Override            public void onFailure() {                if (mActivity != null) {                    hint(recyclerView, "好奇怪,文章加载不来", Color.parseColor("#0099CC"));                }            }        };        //滑动到底部事件监听        OnSlideToTheBottomListener slideListener = new OnSlideToTheBottomListener() {            @Override            public void onSlideToTheBottom() {                getBeforeArticleList();            }        };        adapter.setSlideToTheBottomListener(slideListener);        getLatestArticleList();    }    public void getLatestArticleList() {        if (!HttpUtil.isNetworkConnected(mActivity)) {            hint(recyclerView, "似乎没有连接网络?", Color.parseColor("#0099CC"));            stopRefresh();            return;        }        HttpUtil.getLatestArticleList(latestListener);    }    public void getBeforeArticleList() {        if (!HttpUtil.isNetworkConnected(mActivity)) {            hint(recyclerView, "似乎没有连接网络?", Color.parseColor("#0099CC"));            return;        }        HttpUtil.getBeforeArticleList(getRootActivity().getDate(), beforeListener);    }    public void stopRefresh() {        if (getRootActivity() != null) {            getRootActivity().setRefresh(false);        }    }}

以上多个地方用到了HttpUtil当中的静态方法,该网络请求是异步的,当数据获取到后利用回调函数执行数据刷新即可

1 0
原创粉丝点击