【小白做项目-02(仿知乎日报APP)】(联网,webview,Recyclerview多item,Json解析,刷新加载)手把手教你Android实战

【小白做项目-02】

本文为Karthus77原创转载请说明

一、项目需求

  1. 仿照“知乎日报APP”设计
  2. 轮播图不做要求
  3. 评论楼中楼不做要求
  4. 用户系统不做要求

二、效果展示

三、项目分析

仿照知乎日报这个项目,是一个较为复杂的APP项目
该项目的核心是“数据获取”到“数据展示”的一个过程
核心知识点如下:

  1. Recyclerview多item
  2. Recyclview使用
  3. Json数据解析
  4. webview使用
  5. smartrefresh的使用
  6. 网络访问获取数据
  7. glide工具加载图片

四、实战操作

1.项目框架搭建

1.1 Activity搭建

该项目需要3个活动页面

  1. 主页面(展示新闻标题)
  2. 内容页面(展示具体新闻内容)
  3. 评论页面(展示用户评论)

1.2 添加网络访问权限


在AndroidManifest.xml文件中加入以下两行代码。
并在<application下方加入以下代码

 android:usesCleartextTraffic="true"

AndroidManifest是对应Android应用的一个配置文件,例如我在该文件中加入了联网许可,这个APP就可以联网。

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

1.3 添加依赖

首先,什么是依赖,为什么要去添加它呢?
简单的来说,依赖是一个程序,是一堆代码,这段程序或代码可以帮助我们解决很多问题。比如我们在写C语言程序是,要include头文件,头文件中就已经为我们写好了很多函数的定义。依赖就是如此,Android Studio内部没有实现的功能,我们以依赖方式添加,就可以实现依赖中的功能。
需要说明的是,依赖虽好,可不要频繁使用,因为依赖是别人写出来的,内部的实现功能你了解的并不清晰,并不理解,此种利弊,需要自己好好考量
在这里插入图片描述
在build.gradle中加入以下依赖(glide图片加载工具,smartrefresh刷新工具,recyclerview视图工具)

    implementation 'com.github.bumptech.glide:glide:4.11.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
    implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.2'
    implementation 'com.scwang.smartrefresh:SmartRefreshHeader:1.1.2'

2.UI界面搭建

2.1界面部分

由于UI界面参考“知乎日报APP”我们只需要模仿即可

我们只需要做出图片中红线包裹的UI即可

2.2状态栏透明效果设置

我们可以看到红框部分状态栏是图片背景的底色
设置方法,在xml文件对应的Activity的onCreate周期后面加入以下代码即可实现该效果

 if (Build.VERSION.SDK_INT >= 21){
    
    
            View decorView = getWindow().getDecorView();
            decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
            getWindow().setStatusBarColor(Color.TRANSPARENT);
        }

3.Recyclerview使用

什么是Recyclerview呢?它有什么用呢?
通俗的话,Recyclerview就是一个强大的控件,用于重复展示某一类东西。展示的每一个东西叫做item

如图中所示,每一个新闻就是Recyclerview中的一个item。同样在我们QQ聊天为微信聊天的地方,每一个联系人的小窗就是一个item,有了Recyclerview我们就可以做出知乎日报中新闻的效果了

3.1 Recyclerview工作原理

我们说Recyclerview是一个控件,也就是说它是类似与Textview,Editview的一种工具。我们在Activity中对Textview等可以进行一系列的操作。同样,我们在Activity对Recyclerview进行操作。

Recyclerview既然要以一个形式重复展示多组数据(item),所以Recyclerview控件的本质功能就是“数据接收”到“数据展示”的一个工具。
那么如何进行数据的接受呢?数据的接受必须以一定的形式存贮起来,我们知道数据存贮的方式有数组,有链表,有结构体。而数据的展示就要用到Adapter适配器,即根据数据适配对应的展示形式。
Adapter能够将Recyclerview和Activity联系在一起,来展示信息。

3.2 布局文件中Recyclerview

在xml文件中加入Recyclerview

<com.scwang.smartrefresh.layout.SmartRefreshLayout
            android:id="@+id/refreshLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/recyclerView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
        </com.scwang.smartrefresh.layout.SmartRefreshLayout>

由于需要刷新和加载功能,我们提前在Recyclerview中包裹我们的smartfresh

3.3 创建item布局

在这里插入图片描述
在 layout文件中新建一个layout XML file 文件用于写出每一条新闻文件
分析每一条新闻布局
在这里插入图片描述
可以看出每一个item有标题,小标题和图片三部分
这里给出该item的xml的代码

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/paper_item"
    android:layout_width="match_parent"
    android:layout_height="105dp">
    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="100dp">


        <TextView
            android:id="@+id/titleArticle"
            android:layout_width="224sp"
            android:layout_height="wrap_content"
            android:layout_marginLeft="15sp"
            android:textSize="16sp"
            android:layout_marginTop="35sp"
            android:textStyle="bold"
            android:text="小事·怀孕,怕吗?"
            android:textColor="@color/black">

        </TextView>
        <TextView
            android:id="@+id/viceTitle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="12sp"
            android:layout_below="@id/titleArticle"
            android:layout_marginTop="5sp"
            android:text="VOL·1244"
            android:layout_marginLeft="15sp">

        </TextView>
        <ImageView
            android:id="@+id/imageTitle"
            android:layout_width="75sp"
            android:layout_height="75sp"
            android:layout_alignParentRight="true"
            android:layout_marginTop="25sp"
            android:layout_marginRight="15sp">

        </ImageView>
        <TextView
            android:id="@+id/everyday"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/viceTitle"
            android:text=""
            android:textSize="10sp"
            android:layout_marginLeft="50sp"
            android:layout_marginTop="10sp">

        </TextView>


    </RelativeLayout>

</LinearLayout>

3.4 创建Adapter

在这里插入图片描述

新建Java Class 并命名为MyAdapater用于新闻类展示
Adapater里面的具体代码我们在后面添加,先把它创建好

4.网络访问获取数据

我们在前面已经为项目添加了网络访问的权限
下面我们就进行网络访问的操作
首先网络访问需要进行在子线程而不是主线程中
因为网络访问是一个耗时操作
新建子线程

 final Thread thread=new Thread(new Runnable()

4.1 Calender日历获取时间

利用calender获取当天的时间和早中晚来显示红框部分

代码如下

final StringBuilder response = new StringBuilder();
                final StringBuilder response1 = new StringBuilder();
                String line;
                String line1;
                Calendar c=Calendar.getInstance();
                int t = c.get(Calendar.HOUR_OF_DAY);
                if(t>=18)
                {
    
    
                    time.setText("晚上好!");
                }
                else if(t<=8&&t>=6)
                {
    
    
                    time.setText("早上好!");
                }
                else
                {
    
    
                    time.setText("知乎日报");
                }
                int d=c.get(Calendar.DAY_OF_MONTH);
                String days=String.format("%d",d);
                Date.setText(days);

                int months=c.get(Calendar.MONTH);
                switch (months+1)
                {
    
    
                    case 1:month.setText("一月");
                    break;
                    case 2:month.setText("二月");
                    break;
                    case 3:month.setText("三月");
                    break;
                    case 4:month.setText("四月");
                    break;
                    case 5:month.setText("五月");
                    break;
                    case 6:month.setText("六月");
                    break;
                    case 7:month.setText("七月");
                    break;
                    case 8:month.setText("八月");
                    break;
                    case 9:month.setText("九月");
                    break;
                    case 10:month.setText("十月");
                    break;
                    case 11:month.setText("十一月");
                    break;
                    default:month.setText("十二月");

4.2 发送网络请求

我们已经新建了一个子线程,在这个子线程中进行联网操作
先建立一个StringBulider获取数据
StringBulider能够将获取的数据转化成字符串储存起来

final StringBuilder response = new StringBuilder();
try {
    
    
                    URL url=new URL("https://news-at.zhihu.com/api/3/stories/latest");//获取服务器哦地址
                    HttpURLConnection urlConnection= (HttpURLConnection) url.openConnection();//双方建立连接

                    urlConnection.setRequestMethod("GET");//给服务器发送请求
                    InputStream inputStream=urlConnection.getInputStream(); //字节流
                    Reader reader=new InputStreamReader(inputStream); //把字节流转化成字符流
                    BufferedReader bufferedReader=new BufferedReader(reader);//字符流 转成 缓冲流,一次可以读一行


                    while ((line=bufferedReader.readLine())!=null){
    
    //当temp读到的数据为空就结束
                        response.append(line);

                    }

在这个过程中,url对应的是接口数据网站,setRequestMedthod设置访问方式为get,利用readline函数以行方式读取所有数据,读完以后,所有数据已经存贮在response当中

4.3 Json数据解析与Map,list储存

Json是一种易于理解的数据储存形式,有着广泛的应用,知乎日报官方Api返回我们的是Json数据,因此我们需要将他解析。

我们用火狐浏览器打开接口网站,火狐浏览器会自动帮我们解析,便于理解
在这里插入图片描述
解析Json数据需要理解对象与数组
我们以latest这个接口为例,stories对应了当天的6篇文章的Json数据,也就说stories对应了一个Json数组,这个数组中有6个对象
在每个对象中有文章的大小标题,和对应封面图片的Url
下面给出解析代码

runOnUiThread(new Runnable() {
    
    
                        @Override
                        public void run() {
    
    
                            try {
    
    
                                JSONObject jsonObject = new JSONObject(String.valueOf(response));
                                date=jsonObject.getInt("date");
                                int day=date%100;
                                String days=String.format("%d",day);
                                JSONArray jsonArray = jsonObject.getJSONArray("stories");
                                for (int i = 0; i < 6; i++) {
    
    
                                    Map<String,Object> map=new HashMap<>();
                                    JSONObject jsonObject1 = (JSONObject) jsonArray.getJSONObject(i);
                                    String title = jsonObject1.getString("title");
                                    String vicetitle = jsonObject1.getString("hint");
                                    String url=jsonObject1.getString("url");
                                    String id=jsonObject1.getString("id");
                                    JSONArray jsonArray1=jsonObject1.getJSONArray("images");
                                    String imageUrl=(String)jsonArray1.get(0);
                                    map.put("url",url);
                                    map.put("id",id);
                                    map.put("image",imageUrl);
                                    map.put("title", title);
                                    map.put("hint",vicetitle);
                                    list.add(map);
                                }

                                myAdapter = new MyAdapter(Paper.this,list);
                                recyclerView.setAdapter(myAdapter);



runOnUiThread是返回主线程的操作,由于UI操作只能在主线程中操作,而我们的联网操作是在子线程中进行的操作,而我们需要将Json数据用Recyclerview的形式展示是UI操作所以需要返回UI线程,也就是主线程

数据储存在list当中,并利用map键值对的方式方便我们在Adpater中数据的获取

4.4 Adapter适配与多item

 myAdapter = new MyAdapter(Paper.this,list);
                                recyclerView.setAdapter(myAdapter);

在Activity中就是这两行代码连接了Recyclerview和Adapter

public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
    
    
    private Paper context;
    private List<Map<String,Object>> list;
    private View inflater;
    private AdapterView.OnItemClickListener onItemClickListener;
    //构造方法,传入数据
    public MyAdapter(Paper context, List<Map<String,Object>> list){
    
    
        this.context = context;
        this.list = list;
    }
    private static final int news = 0;
    private static final int date = 1;
    private static final int noNet= 2;
    @Override
    public int getItemViewType(int position) {
    
    
        int size=list.get(position).size();
        if (size==1)
        {
    
    
            return date;
        }
        else if(size==2)
        {
    
    
            return noNet;
        }
        else
        {
    
    
            return news;
        }
    }

在这段代码中,我们利用getItemViewtype获得需要展示的视图

我们可以看到这里有两种不同的item,所以我们的Recyclerview还要能够适配多种item形式,getItemViewType就是获取需要的Item我们以每条list中的大小(有几个map键值对)来判断对应的item种类

@Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    
    
        //创建ViewHolder,返回每一项的布局
        if(viewType==news)
        {
    
    
        inflater = LayoutInflater.from(context).inflate(R.layout.item,parent,false);
        ViewHolder ViewHolder = new ViewHolder(inflater);
        return ViewHolder;}
        else if(viewType==noNet)
        {
    
    
            inflater=LayoutInflater.from(context).inflate(R.layout.item_nonet,parent,false);
            noHolder noHolder=new noHolder(inflater);
            return  noHolder;
        }
        else
        {
    
    
            inflater = LayoutInflater.from(context).inflate(R.layout.item_date,parent,false);
            dateHolder dateHolder = new dateHolder(inflater);
            return  dateHolder;
        }
    }

在onCreatViewHolder中绑定对应的item

@Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
    
    
        //将数据和控件绑定
        int viewType=getItemViewType(position);
                if(viewType==0)
                {
    
    

           ViewHolder viewholder = (ViewHolder) holder;
           String number_title=list.get(position).get("title").toString();
           if(number_title.length()>27)
           {
    
    
               String title=number_title.substring(0,27);
               viewholder.titlePaper.setText(title+"...");
           }
           else{
    
    
               viewholder.titlePaper.setText(list.get(position).get("title").toString());
           }

        viewholder.titleVice.setText(list.get(position).get("hint").toString());
        Glide.with(context).load(list.get(position).get("image")).into(viewholder.titleImage);

        viewholder.paper_item.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                Intent intent=new Intent(context,Context.class);
                Bundle bd=new Bundle();
                bd.putString("id",list.get(position).get("id").toString());
                bd.putString("url",list.get(position).get("url").toString());
                intent.putExtras(bd);
                context.startActivity(intent);

            }
        });}
                else if(viewType==2)
                {
    
    
                    noHolder noHolder=(noHolder) holder;
                    noHolder.hint.setText("网络好像走丢了");
                }
        else {
    
    

            dateHolder viewHolder = (dateHolder) holder;
            String day=list.get(position).get("date").toString();
            int nowDay=(Integer.parseInt(day)%100);
            int nowMonth=((Integer.parseInt(day)%10000)/100);
            viewHolder.date.setText(nowMonth+" 月 "+nowDay+" 日");}






    }

在onBindViewHolder中对需要用到的每一种item进行操作

 @Override
    public int getItemCount() {
    
    
        //返回Item总条数
        return list.size();
    }

    //内部类,绑定控件
    class ViewHolder extends RecyclerView.ViewHolder{
    
    
        LinearLayout paper_item;
        TextView titlePaper;
        TextView titleVice;
        ImageView titleImage;
        TextView everyday;
        public ViewHolder(View view) {
    
    
            super(view);
            everyday=view.findViewById(R.id.everyday);
            titlePaper= view.findViewById(R.id.titleArticle);
            titleVice= view.findViewById(R.id.viceTitle);
            titleImage = view.findViewById(R.id.imageTitle);
            paper_item=view.findViewById(R.id.paper_item);
        }
    }
    class dateHolder extends RecyclerView.ViewHolder{
    
    
        TextView date;
        TextView background;
        public dateHolder (View view){
    
    
            super(view);
            date=view.findViewById(R.id.newDate);
            background=view.findViewById(R.id.background);
        }
    }
    class noHolder extends RecyclerView.ViewHolder{
    
    
        TextView hint;
        public noHolder (View view){
    
    
            super(view);
            hint =view.findViewById(R.id.hint);
        }
    }

创建内部类绑定每一种布局

5.刷新与加载

刷新与加载,听起来就是很高级的功能啊。
如何去理解它呢。刷新与加载的本质其实就是两个点击事件,只不过是以滑动的形式表现出来的罢了。
刷新和加载的实质也是数据的更新与加载,我们利用Recyclerview加载item使用到的list数据,list的数据变多了,Recyclerview显示的item也会变多

refreshLayout.setOnRefreshListener

这段代码用于刷新事件的展开

refreshLayout.setOnLoadMoreListener

这段代码用于加载事件,下面给出加载部分的完整代码

 refreshLayout.setOnLoadMoreListener(new OnLoadMoreListener() {
    
    
            @Override
            public void onLoadMore(RefreshLayout refreshlayout) {
    
    
                refreshlayout.finishLoadMore(2000/*,false*/);//传入false表示加载失败
                final Thread thread=new Thread(new Runnable() {
    
    


                    @Override
                    public void run() {
    
    

                        final StringBuilder response1 = new StringBuilder();
                        String line1;
                        try {
    
    
                           yesterday=yesterday-1;

                            URL Url=new URL("https://news-at.zhihu.com/api/3/news/before/"+String.valueOf(yesterday));//获取服务器哦地址
                            HttpURLConnection UrlConnection = (HttpURLConnection) Url.openConnection();//双方建立连接
                            UrlConnection.setRequestMethod("GET");
                            InputStream inputStream1=UrlConnection.getInputStream();
                            Reader reader1=new InputStreamReader(inputStream1);
                            BufferedReader bufferedReader1=new BufferedReader(reader1);

                            while ((line1=bufferedReader1.readLine())!=null){
    
    //当temp读到的数据为空就结束
                                response1.append(line1);

                            }

                            runOnUiThread(new Runnable() {
    
    
                                @Override
                                public void run() {
    
    
                                    try {
    
    
                                        JSONObject jsonObject = new JSONObject(String.valueOf(response1));
                                        JSONArray jsonArray = jsonObject.getJSONArray("stories");
                                        Map<String,Object> map1=new HashMap<>();
                                        Integer day1=jsonObject.getInt("date");
                                        map1.put("date",day1);
                                        list.add(map1);
                                        for (int i = 0; i < 6; i++) {
    
    
                                            Map<String,Object> map=new HashMap<>();
                                            JSONObject jsonObject1 = (JSONObject) jsonArray.getJSONObject(i);
                                            String title = jsonObject1.getString("title");
                                            String vicetitle = jsonObject1.getString("hint");
                                            String url=jsonObject1.getString("url");
                                            String id=jsonObject1.getString("id");
                                            JSONArray jsonArray1=jsonObject1.getJSONArray("images");
                                            String imageUrl=(String)jsonArray1.get(0);
                                            map.put("url",url);
                                            map.put("id",id);
                                            map.put("image",imageUrl);
                                            map.put("title", title);
                                            map.put("hint",vicetitle);
                                            list.add(map);
                                        }


                                        myAdapter.notifyDataSetChanged();

                                    } catch (JSONException e) {
    
    
                                        e.printStackTrace();
                                    }

                                }
                            });
                            inputStream1.close();
                            reader1.close();
                            bufferedReader1.close();
                        } catch (Exception e) {
    
    
                           runOnUiThread(new Runnable() {
    
    
                               @Override
                               public void run() {
    
    
                                   list.clear();
                                   Map<String,Object> map=new HashMap<>();
                                   map.put("1",1);
                                   map.put("2",2);
                                   list.add(map);
                                   myAdapter = new MyAdapter(Paper.this,list);
                                   recyclerView.setAdapter(myAdapter);
                               }
                           });

                            e.printStackTrace();
//这里要有联网失败提示
                        }



                    }
                });
                thread.start();//启动线程

            }
        });

6.WebView的使用

顾名思义,webview即网页视图的意思

对于每篇文章的内容部分我们使用webView控件即可

WebView webView=findViewById(R.id.webView);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.loadUrl(url);

仅仅三行代码即可,其中的url是对应该文章的url地址

7.评论展示与glide工具>

glide的使用同样需要添加依赖,我们已经在第一步的时候添加了glide的依赖,那么glide怎么使用呢

Glide.with(context).load(list.get(position).get("image")).into(viewholder.titleImage);

glide的使用只有一行代码with部分是指当前的Activity,也就是context,load部分的get("image“),image是指所要加载图片的url地址

评论部分的头像图片显示为圆形,用glide工具可以加载圆形图片

lide.with(context).load(list.get(position).get("image")).apply(RequestOptions.bitmapTransform(new CircleCrop())).into(viewholder.head);

用上面的代码即可实现
评论部分的设计无需刷新加载,只需要用到多item即可,小伙伴们不妨自己尝试去做
对了,每条评论下面的时间要自己写算法哦,因为接口传过来的是一个很大的不知道怎么处理的数字,自己想想怎么能够得到具体时间吧

8.断网处理

我们的APP是需要联网的,但是万一你在没有网的情况下打开它会怎么样。
答案是:会闪退
所以我们需要做好断网处理,断网处理很简单。我们在联网操作时用到了
try{}catch结构
在这里插入图片描述
我们如果没有联网,程序会进入catch部分,所以我们在catch部分进行断网处理即可

catch (Exception e) {
    
    
                           runOnUiThread(new Runnable() {
    
    
                               @Override
                               public void run() {
    
    
                                   list.clear();
                                   Map<String,Object> map=new HashMap<>();
                                   map.put("1",1);
                                   map.put("2",2);
                                   list.add(map);
                                   myAdapter = new MyAdapter(Paper.this,list);
                                   recyclerView.setAdapter(myAdapter);
                               }
                           });

这是我catch部分的代码,用于加载一个提示界面

效果如下

五、总结

总的来说,这是一个真正意义上的,可以说是一个成熟APP,作为练手项目,我很推荐,它有一定的难度,能够充分考验一个项目整体的架构能力。当然,整篇教程我在许多细节方面没有详细解答,因为我认为需要留有一部分自己思考。我认为能够独立做完这个项目,那么一定会对你的Android开发有着很大的提升。

该项目的源代码和接口可以私信我即可,当然我可不是教大家做盗版软件啊,纯当小白自己练习。

下一个项目是一个淘宝类的项目,有个人用户系统,可以上架购买商品,有兴趣的小伙伴不妨关注一波~

猜你喜欢

转载自blog.csdn.net/Karthus77/article/details/113755242