Android实战开发——News

1.功能分析介绍

知识点

  • ViewPager :页面的滑动
  • PagerSlidingTabStrip :第三方的自定义View,使得菜单栏和下面的页面产生联动的效果。
  • ListView 列表视图
  • WebView 控件:详情页面加载网址
  • 如何获取网络数据并解析展示
  • 数据库的增删改查:需要保留自定义的频道信息,当下一次进入该应用时候,会显示上一次保留的频道信息。

使用第三方框架

  • Volley 框架:是网络加载数据的框架
  • Universal-image-loader 图片加载框架
  • PagerSlidingTabStrip 第三方定义view的使用

逻辑分析

  • 首界面为 ViewPager ,上面为 PagerSlidingTabStrip ,两个控件可以相互影响,点击“+”,可以跳转到频道订阅界面。
  • 频道订阅界面,能够选择首界面显示的新闻类型,改变上一次选择内容返回上级页面时会改变首界面的显示内容,其中头条和社会是默认选项,不能改变。
  • 点击首界面列表中的每一条目,会跳转到详细页面,显示新闻的详细信息。

具体代码的实现托管到了GitHub:https://github.com/ydd997/Android_news

下面介绍重要的几个模块。

2.页面布局绘制和接口分析

导入所需要的包

前期准备工作,把 PagerSlidingTabStrip 中的res和src中的相关文件导入: background_tab.xml 导入drawable中, attrs.xml 是关于自定义View PagerSlidingTabStrip 属性的xml文件,导入到values文件下,把 colors.xml 中的两条属性复制进入项目;,把 PagerSlidingTabStrip.java 放入view包(自己创建,存放自己写的View)下。

把需要的图片放到res的mipmap-hdpi中,因为图片本身比较少

直接第三方框架java包的导入资源:放到当前项目的build.gradle当中

之后点击右上角的Sync,将数据整体格式化和下载。

页面布局绘制

activity_main中,整体是上下结构,选用 LinearLayout ,整体是垂直方向,

<?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:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:background="@color/white"
        >

        <com.example.news.view.PagerSlidingTabStrip
            android:id="@+id/main_tabstrip"
            android:layout_height="60dp"
            android:layout_width="0dp"
            android:layout_weight="1"
            app:pstsTabBackground="@color/white"
            app:pstsDividerColor="#ffffff"
            app:pstsIndicatorHeight="8dp"
            app:pstsUnderlineHeight="3dp"
            >
        </com.example.news.view.PagerSlidingTabStrip>

        <ImageView
            android:id="@+id/main_iv_add"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:scaleType="center"
            android:src="@mipmap/bar_img_subscribe"
            />

    </LinearLayout>

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="@color/black"
        />

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/main_vp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
    </androidx.viewpager.widget.ViewPager>

</LinearLayout>

不需要上面的标题栏,在styles中修改属性为 NoActionBar

首页面的布局如下:

首页面中的ViewPager布局至关重要,是一个ListView

在Layout中创建一个ViewPager中所包含的Fragment布局 NewsInfoFragment ,在对应的布局文件 fragment_news_info 中绘制布局:整体就是一个ListView

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".frag.NewsInfoFragment">

    <!-- TODO: Update blank fragment layout -->
    <ListView
        android:id="@+id/newsfrag_lv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:dividerHeight="1dp"
        android:divider="@color/gray"
        />
</FrameLayout>

ListView中又涉及到每一个Item的布局,再绘制一个Layout item_newsfrag_lv.xml 作为ListView中item的布局:
整体采用线性布局,设置三张图片,占比为1:1:1,第一张图片fitXY等比例放大,后两张图片centerCrop锁定长宽比缩放,裁剪显示。下面新闻来源和时间的信息采用相对布局,

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp">


    <TextView
        android:id="@+id/item_newsfrag_tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:text="国考经验交流会"
        android:textColor="@color/black"
        android:textSize="18sp"
        />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="10dp"
        >
        <ImageView
            android:id="@+id/item_news_iv1"
            android:layout_width="0dp"
            android:layout_height="120dp"
            android:layout_weight="1"
            android:src="@mipmap/bg_defualt_220x150"
            android:scaleType="fitXY"
            />
        <ImageView
            android:id="@+id/item_news_iv2"
            android:layout_width="0dp"
            android:layout_height="120dp"
            android:layout_weight="1"
            android:src="@mipmap/bg_defualt_220x150"
            android:scaleType="centerCrop"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            />
        <ImageView
            android:id="@+id/item_news_iv3"
            android:layout_width="0dp"
            android:layout_height="120dp"
            android:layout_weight="1"
            android:src="@mipmap/bg_defualt_220x150"
            android:scaleType="centerCrop"
            />
    </LinearLayout>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        >
        <TextView
            android:id="@+id/item_news_tv_source"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="新浪"
            android:drawableLeft="@mipmap/topic_user_default"
            android:drawablePadding="20dp"
            android:gravity="center_vertical"
            android:textColor="@color/gray"
            android:textSize="14sp"
            />
        <TextView
            android:id="@+id/item_news_tv_time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:text="2020-7-17 11:28:18"
            android:textColor="@color/gray"
            android:textSize="14sp"
            android:layout_alignBaseline="@id/item_news_tv_source"
            />
    </RelativeLayout>

</LinearLayout>

ListView中每一个Item的布局展示效果入下:

数据来源和获取

从聚合数据中,找到新闻头条,申请接口,

数据的获取:

接口的整理

写一个能够将我们要访问的网址都存放的类,对该类进行统一的操作。

创建一个新的包 bean ,在该包中创建一个新闻的URL类 NewsURL :(将这些接口放到统一的类中进行管理)

package com.example.news.bean;

public class NewsURL {
    //公共的key
    public static String key="46004cb8305704349056ee49ae3c5aca";
    public static String info_url="http://v.juhe.cn/toutiao/index?key="+key+"&type=";
    //头条
    public static String headline_url=info_url+"top";
    //社会
    public static String society_url=info_url+"shehui";
    //国内
    public static String home_url=info_url+"guonei";
    //国际
    public static String international_url=info_url+"guoji";
    //娱乐
    public static String entertainment_url=info_url+"yule";
    //体育
    public static String sport_url=info_url+"tiyu";
    //军事
    public static String military_url=info_url+"junshi";
    //科技
    public static String science_url=info_url+"keji";
    //财经
    public static String fiance_url=info_url+"caijing";
    //时尚
    public static String fashion_url=info_url+"shishang";
}

将这些接口放到统一的类中便于管理,这些接口还有所对应的名称,对应的名称会显示在ViewPager的上面,所以要将接口和名称绑定到一起,可以将接口和名称封装到同一个对象里。

在bean中新建一个专门用于表示接口和其类型的类 TypeBean

package com.example.news.bean;
/*
* 绑定接口名称和类型的类
* */
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

public class TypeBean implements Serializable {
    private int id; //进行数据库的存取
    private String title;
    private String url;
    private boolean isShow; //是否被选中

    //全参的构造方法
    public TypeBean(int id, String title, String url, boolean isShow) {
        this.id = id;
        this.title = title;
        this.url = url;
        this.isShow = isShow;
    }

    //空参的构造方法
    public TypeBean() {
    }
    
    //用到的set、get方法
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public boolean isShow() {
        return isShow;
    }

    public void setShow(boolean show) {
        isShow = show;
    }

    //内存存储
    public static List<TypeBean> getTypeList(){
        List<TypeBean>mDatas=new ArrayList<>();
        TypeBean tb1 = new TypeBean(1,"头条",NewsURL.headline_url,true);
        TypeBean tb2 = new TypeBean(2,"社会",NewsURL.society_url,true);
        TypeBean tb3 = new TypeBean(3,"国内",NewsURL.home_url,true);
        TypeBean tb4 = new TypeBean(4,"国际",NewsURL.international_url,true);
        TypeBean tb5 = new TypeBean(5,"娱乐",NewsURL.entertainment_url,true);
        TypeBean tb6 = new TypeBean(6,"体育",NewsURL.sport_url,true);
        TypeBean tb7 = new TypeBean(7,"军事",NewsURL.military_url,true);
        TypeBean tb8 = new TypeBean(8,"科技",NewsURL.science_url,true);
        TypeBean tb9 = new TypeBean(9,"财经",NewsURL.fiance_url,true);
        TypeBean tb10 = new TypeBean(10,"时尚",NewsURL.fashion_url,true);
        mDatas.add(tb1);
        mDatas.add(tb2);
        mDatas.add(tb3);
        mDatas.add(tb4);
        mDatas.add(tb5);
        mDatas.add(tb6);
        mDatas.add(tb7);
        mDatas.add(tb8);
        mDatas.add(tb9);
        mDatas.add(tb10);
        return  mDatas;
    }
}

现在就将想要获取的网址和他们的标题都封装在同一对象里,并且将所有的对象都存储在集合里,可以通过操作这个集合来操作相关信息。

下面将获取到的数据都展现在ViewPager中,并且可以上下滑动,产生上下联动的效果。

3.页面逻辑代码

完成布局和所对应的Activity代码的编写

MainActivity.java 中声明控件:

ViewPager mainVp;  
PagerSlidingTabStrip tabStrip;  //显示Title
ImageView addIv; //点击跳转到下级页面中

之后在 OnCreate 中通过 findViewById 找到这些控件:

mainVp=findViewById(R.id.main_vp);
tabStrip=findViewById(R.id.main_tabstrip);
addIv=findViewById(R.id.main_iv_add);

ViewPagerPagerSlidingTabStrip 是相互对应的, ViewPager 中所包含Fragment,把Fragment放到一个集合中,集合类型为Fragment;网址以及标题都存储在TypeBean当中,可以把选中的TypeBean放到一个集合中:

List<Fragment>fragmentList; // ViewPager 所显示的内容
List<TypeBean>selectTypeList; //所选中的类型的集合

创建一个初始化界面的函数initPager():

    private void initPager() {
        /*初始化页面的函数*/
        //List<TypeBean> typeList = TypeBean.getTypeList(); //获取全部

        List<TypeBean> typeList = DBManager.getSelectTypeList(); //获取选中的栏目的页面

        selectTypeList.addAll(typeList);
        for (int i = 0; i < selectTypeList.size(); i++) {
            TypeBean typeBean = selectTypeList.get(i); //得到每一个栏目的信息对象
            NewsInfoFragment infoFragment = new NewsInfoFragment();
            //向Fragment当中传值
            Bundle bundle = new Bundle();
            bundle.putSerializable("type",typeBean);
            infoFragment.setArguments(bundle);
            fragmentList.add(infoFragment);
        }
    }

此时ViewPager的数据源已经完成,接下来写ViewPager对应的Adapter

创建一个新的Adapter 为 NewsInfoAdapter 。ViewPager对应的类型是Fragment类型,

ViewPager跳转到其他页面中,返回时会造成页面数据的变化,这里要继承 FragmentStatePagerAdapter

public class NewsInfoAdapter extends FragmentStatePagerAdapter {
    Context context;
    List<Fragment>fragmentList; //viewpager每个页面页面展示的fragment集合
    List<TypeBean>typeBeanList; //PagerSlidingTabStrip所展示的标题

    //构造方法
    public NewsInfoAdapter(FragmentManager fm,Context context,List<Fragment>fragmentList,List<TypeBean>typeBeanList) {
        super(fm);
        this.context=context;
        this.fragmentList=fragmentList;
        this.typeBeanList=typeBeanList;
    }

    @Override
    public int getItemPosition(@NonNull Object object) {
        return PagerAdapter.POSITION_NONE;
    }
    
    //要求重写的函数
    @Override
    public Fragment getItem(int position) {
        return fragmentList.get(position); //返回指定位置的fragment
    }
    
    //要求重写的函数
    @Override
    public int getCount() {
        return fragmentList.size(); //返回要加载的页数
    }


    //返回指定位置的标题(如果ViewPager和其他控件有互相位置的关联关系需要重写这个方法)
    @Nullable
    @Override
    public CharSequence getPageTitle(int position) {
        TypeBean typeBean = typeBeanList.get(position);
        String title = typeBean.getTitle();
        return title;  //返回指定位置的标题
    }
}

回到MainActivity中继续设置适配器:

        //创建适配器对象
        adapter = new NewsInfoAdapter(getSupportFragmentManager(), this, fragmentList, selectTypeList);
        //设置适配器
        mainVp.setAdapter(adapter);
        //关联TapStrip和ViewPager
        tabStrip.setViewPager(mainVp);

到此TapStrip和ViewPager关联完成:将ViewPager所用到的Fragment创建出来,把网址和标题传入到Fragment当中,联网是在Fragment中联网的(这里不进行操作)。把Fragment中的集合传入到Adapter当中,给ViewPager设置适配器,使得TapStrip和ViewPager相互关联,这就是MainActivity目前写到的代码。

ListView展示的数据源:新建一个java类 InfoBean

把测试得到的数据复制,然后格式化生成一个Bean类,

由于涉及到使用 Universal-image-loader 图片加载框架来加载图片,这里先创建一个类 UnitApp 继承自Application,这是我们自定义的Application类,在一个项目工程中,他的对象是唯一的。

这里定义一个初始化ImageLoader的方法 initImageLoader ,要自己写。

public class UniteApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        initImageLoader(getApplicationContext()); //初始化图片加载框架ImageLoader
    }

    //初始化图片加载框架ImageLoader
    private void initImageLoader(Context context) {
        //ImageLoader的设置参数
        ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(context)
                .threadPriority(Thread.MAX_PRIORITY).denyCacheImageMultipleSizesInMemory()
                .diskCacheFileNameGenerator(new Md5FileNameGenerator())
                .tasksProcessingOrder(QueueProcessingType.LIFO)
                .writeDebugLogs()
                .build();
        ImageLoader.getInstance().init(configuration);
    }
}

初始化图片加载框架ImageLoader中:设置了线程的优先级是最高级;使用一定数量的缓存;使用MD%的磁盘存储命名方式;使用LIFO(最近最少使用放后面)排队方式;打印log日志。

这里要对自定义的Application进行声明:

创建一个ListView的适配器 InfoitemAdapter ,继承自 BaseAdapter

其中获取三张图片的地址分别为pic1、pic2、pic3,如果有地址存在,就显示,如果没有地址就不显示,还不让他占地方(View.GONE)

/*每一个Fragment当中的ListView的适配器*/
public class InfoitemAdapter extends BaseAdapter {

    Context context; //表示ListView所在的Activity
    List<InfoBean.ResultBean.DataBean> mDatas; //数据源
    ImageLoader imageLoader;
    DisplayImageOptions options; //图片加载配置信息

    public InfoitemAdapter(Context context, List<InfoBean.ResultBean.DataBean> mDatas) {
        this.context = context;
        this.mDatas = mDatas;
        imageLoader=ImageLoader.getInstance();
        options=new DisplayImageOptions.Builder()
                .showImageOnLoading(null) //正在加载中什么都不展示
                .showImageForEmptyUri(null) //空字符串显示空
                .showImageOnFail(null)  //加载失败显示空
                .cacheInMemory(true).cacheOnDisk(true).considerExifParams(true) //使用缓存、磁盘存储
                .bitmapConfig(Bitmap.Config.RGB_565).build(); //存储的图片类型是565
    }

    @Override
    public int getCount() {
        return mDatas.size(); //返回集合的长度
    }

    @Override
    public Object getItem(int position) {
        return mDatas.get(position); //返回指定位置的数据源
    }

    @Override
    public long getItemId(int position) {
        return position; //返回位置
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder=null;
        //1.判断内存中是否有复用的view
        if(convertView==null){
            //2.将布局转换成新的view进行使用
            convertView= LayoutInflater.from(context).inflate(R.layout.item_newsfrag_lv,null);
            holder=new ViewHolder(convertView);
            convertView.setTag(holder);
        }else {
            holder= (ViewHolder) convertView.getTag();
        }
        //获取指定位置的数据源
        InfoBean.ResultBean.DataBean dataBean = mDatas.get(position);
        holder.titleTv.setText(dataBean.getTitle());
        holder.sourceTv.setText(dataBean.getAuthor_name());
        holder.timeTv.setText(dataBean.getDate());

        //获取三张图片的地址
        String pic1 = dataBean.getThumbnail_pic_s();
        String pic2 = dataBean.getThumbnail_pic_s02();
        String pic3 = dataBean.getThumbnail_pic_s03();

        if (TextUtils.isEmpty(pic1)) {
            holder.iv1.setVisibility(View.GONE); //如果为空就不显示也不占地方
        }else {
            holder.iv1.setVisibility(View.VISIBLE); //不为空就显示
            imageLoader.displayImage(pic1,holder.iv1,options);
        }

        if (TextUtils.isEmpty(pic2)) {
            holder.iv2.setVisibility(View.GONE);
        }else {
            holder.iv2.setVisibility(View.VISIBLE);
            imageLoader.displayImage(pic2,holder.iv2,options);
        }

        if (TextUtils.isEmpty(pic3)) {
            holder.iv3.setVisibility(View.GONE);
        }else {
            holder.iv3.setVisibility(View.VISIBLE);
            imageLoader.displayImage(pic3,holder.iv3,options);
        }

        return convertView;
    }

    //定义控件
    class ViewHolder{
        TextView titleTv,sourceTv,timeTv;
        ImageView iv1,iv2,iv3;
        //初始化item控件
        public ViewHolder(View view){
            titleTv=view.findViewById(R.id.item_newsfrag_tv_title);
            sourceTv=view.findViewById(R.id.item_news_tv_source);
            timeTv=view.findViewById(R.id.item_news_tv_time);
            iv1=view.findViewById(R.id.item_news_iv1);
            iv2=view.findViewById(R.id.item_news_iv2);
            iv3=view.findViewById(R.id.item_news_iv3);
        }
    }
}

到此为止,每一个Item中的控件都显示完成,titleTv、sourceTv和timeTv通过setText设置了文本资料,ImageView通过ImageLoader来加载显示的内容,

ListView的适配器 InfoitemAdapter 就写完了

回到他所在的 NewsInfoFragment 中,设置Adapter对象,

public class NewsInfoFragment {
    ListView infoLv;
    private String url;
    //listView的数据源
    List<InfoBean.ResultBean.DataBean> mData;
    private InfoitemAdapter adapter;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view=inflater.inflate(R.layout.fragment_news_info, container, false);
        infoLv=view.findViewById(R.id.newsfrag_lv);
        //获取Activity传递的数据
        Bundle bundle = getArguments();
        TypeBean typeBean= (TypeBean) bundle.getSerializable("type");
        url = typeBean.getUrl();

        mData = new ArrayList<>();
        //创建ListView的适配器对象

        adapter = new InfoitemAdapter(getActivity(), mData);
        infoLv.setAdapter(adapter);

        return view;
    }
}

现在为止其实是并没有数据的,因为我们的网址并没有获取数据,我们的数据源是一个空的集合,只是一个集合但其实里面并没有数据,所以传入到adapter当中是什么都显示不出来的。

我们使用第三方框架 Volley 来获取网络数据,获取相对应网址的信息,

网络数据的加载

在自定义的App UnitApp 中对于 Volley 进行声明:

Volley 是如何获得网络请求的呢?通常是将 封装到一个BaseFragment当中。创建一个新的java类 BaseFragment 继承自 Fragment ,将网络请求的过程写在这个Fragment当中。

public class BaseFragment extends Fragment implements Response.Listener<String>, Response.ErrorListener {
    public void loadDate(String url){
        //创建网络请求对象 StringRequest 
        StringRequest request=new StringRequest(url,this,this);

        //将请求添加到请求队列中
        UniteApp.getHttpQueue().add(request);
    }

    @Override
    public void onErrorResponse(VolleyError error) {
        //获取网络请求失败时,会回调的函数
    }

    @Override
    public void onResponse(String response) {
        //获取网络请求成功时,会回调的函数
    }
}

回到 NewsInfoFragment 中就可以加载网络数据了:

此时 NewsInfoFragment 中的代码如下:

public class NewsInfoFragment extends BaseFragment {
    ListView infoLv;
    private String url;
    //listView的数据源
    List<InfoBean.ResultBean.DataBean> mData;
    private InfoitemAdapter adapter;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view=inflater.inflate(R.layout.fragment_news_info, container, false);
        infoLv=view.findViewById(R.id.newsfrag_lv);
        //获取Activity传递的数据
        Bundle bundle = getArguments();
        TypeBean typeBean= (TypeBean) bundle.getSerializable("type");
        url = typeBean.getUrl();

        mData = new ArrayList<>();
        //创建ListView的适配器对象

        adapter = new InfoitemAdapter(getActivity(), mData);
        infoLv.setAdapter(adapter);

        loadDate(url);
        return view;
    }

    //获取数据成功时,会调用的函数
    @Override
    public void onResponse(String response) {
        InfoBean infoBean = new Gson().fromJson(response, InfoBean.class);
        List<InfoBean.ResultBean.DataBean> list = infoBean.getResult().getData();
        //添加到数据源中
        mData.addAll(list);
        //提示adapter数据源发送变化了,更新数据
        adapter.notifyDataSetChanged();
    }

    //获取数据失败时,会调用的函数
    @Override
    public void onErrorResponse(VolleyError error) {

    }
}

这里用到了网络数据,需要将网络请求添加上:

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

Android9以上的手机运行连不起网,需要加一个

android:usesCleartextTraffic="true"

此时运行遇到的问题:

在 Android 6.0 中,我们取消了对 Apache HTTP 客户端的支持。 从 Android 9 开始,默认情况下该内容库已从 bootclasspath 中移除且不可用于应用。

要继续使用 Apache HTTP 客户端,以 Android 9 及更高版本为目标的应用可以向其 AndroidManifest.xmlapplication 节点下 添加以下内容:

<uses-library 
    android:name="org.apache.http.legacy" 
    android:required="false"/>

此时运行可以看到新闻界面,这里忘记截图了o(╥﹏╥)o

现在并不知道浏览的界面具体对应上面哪一个标题,点击上面的菜单栏时候字体是没有变化的。这里希望点击上面的标题时,对应的标题颜色和字体发生变化。

PagerSlidingTabStrip 这个第三方的自定义View中定义几个变量:

	//正在被选中的位置
	private int selectionPosition;
	//设置被选文字的大小
	private int selectionTextSize=18;
	//设置被选中的文字颜色为蓝色
	private int selectionTextColor= Color.BLUE;

然后将被选中的位置 selectionPosition 进行赋值,

PagerSlidingTabStrip 中的 PageListener下的onPageSelected添加正在被选中的位置:

updateTabStyles() 中可以对文字的颜色、大小进行设置,添加代码实现判断这个位置是否为选中位置,如果是选中位置,就改变文字颜色和文字大小:

再次执行 updateTabStyles() 函数:

现在运行可以发现当我们滑动的时候,上面的条目可以对的上而且文字和颜色都会发生变化。

4.页面频道订阅逻辑编写

需要实现:点击加号跳转到“频道订阅界面”,给“频道订阅”中的每一条设置点击事件,点击会显示是否被订阅,并将数据存储到数据库中。

新建一个activity AddItemActivity ,在布局 activity_add_item.xml 进行布局,整体为线性布局,上面的频道订阅显示为相对布局,中间一条分割线,下面是一个ListView:

接下来写对应的Item的布局,在Layout文件下创建一个新的布局 item_add_lv.xml ,这个左右结构,选择相对布局。

MainActivity 中需要实现点击加号按钮实现响应事件,之后跳转到刚才的 AddItemActivity ,这里让整个Activity实现接口 OnClickListener ,然后重写点击事件。:(这里通过事件源所在类实现

    //实现点击加号从MainActivity跳转到AddItemActivity界面
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.main_iv_add:
                Intent intent = new Intent(MainActivity.this, AddItemActivity.class);
                startActivity(intent);
                break;
        }
    }

AddItemActivity 中添加控件声明和寻找控件:

    //声明控件
    ImageView backIv;
    ListView addLv;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_add_item);

        //查找控件
        backIv=findViewById(R.id.add_iv_back);
        addLv=findViewById(R.id.add_lv);
    }

backIv 要实现返回上一级的功能,在这里依然是实现 OnClickListener 的接口,然后重写 onClick 方法。首先给 backIv 设置监听:

backIv.setOnClickListener(this); //添加点击事件的监听

因为当前的Activity实现了 OnClickListener 这个接口,所以这个Activity的对象就是这个接口的对象,要向backIv中传入 OnClickListener 的接口对象,就可以直接传入他的实现类Activity的对象,所以这里传 this 即可。

backIv 被点击之后的事件可以在 onClick 方法中执行:

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.add_iv_back:
                finish(); //销毁当前的Activity,返回上一级界面
                break;
        }
    }

此时 backIv 的操作已写完。

接下来就是写对 addLv 的操作,它是用来显示所有的频道信息, ListView addLv 中的数据源应该是之前写的 TypeBeanTypeBean 中就封装了title、URL和是否显示。

//数据源
List<TypeBean>mDatas;

由于信息是会改变的,当这次选中的频道,我们希望下次进入之后还会保持上一次选中的结果,所以这里需要本地存储,这里选择的本地存储为数据库。

新建一个关于数据库的包 db ,然后创建一个数据库的管理类 DBOpenHelper ,使其继承于 SQLiteOpenHelper

public class DBOpenHelper extends SQLiteOpenHelper {
    public DBOpenHelper(@Nullable Context context) {
        super(context, "info.db", null, 1);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        String sql="create table itype(id integer primary key,title varchar(10) unique not null,url text not null,isshow varchar(10) not null)";
        db.execSQL(sql);
        String inserSql="insert into itype values(?,?,?,?)";
        db.execSQL(inserSql,new Object[]{1,"头条", NewsURL.headline_url,"true"});
        db.execSQL(inserSql,new Object[]{2,"社会",NewsURL.society_url,"true"});
        db.execSQL(inserSql,new Object[]{3,"国内",NewsURL.home_url,"true"});
        db.execSQL(inserSql,new Object[]{4,"国际",NewsURL.entertainment_url,"true"});
        db.execSQL(inserSql,new Object[]{5,"娱乐",NewsURL.entertainment_url,"true"});
        db.execSQL(inserSql,new Object[]{6,"体育",NewsURL.sport_url,"false"});
        db.execSQL(inserSql,new Object[]{7,"军事",NewsURL.military_url,"false"});
        db.execSQL(inserSql,new Object[]{8,"科技",NewsURL.science_url,"false"});
        db.execSQL(inserSql,new Object[]{9,"财经",NewsURL.fiance_url,"false"});
        db.execSQL(inserSql,new Object[]{10,"时尚",NewsURL.fashion_url,"false"});
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}

其中true或false决定了该栏目是显示还是隐藏。

接下来写一个获取数据库中全部信息的集合。在数据库的包 db 中新建一个数据库的管理类 DBManager ,在这里写一个关于数据库声明的函数,再添加一个获取数据库中全部类型的list集合:

public class DBManager {
    public static SQLiteDatabase database;

    public static void initDB(Context context){
        DBOpenHelper helper=new DBOpenHelper(context);
        database=helper.getWritableDatabase();
    }
    
    /*获取数据库中全部行的内容,存储到集合当中*/
    public static List<TypeBean>getAllTypeList(){
        List<TypeBean>list=new ArrayList<>();
        Cursor cursor=database.query("itype",null,null,null,null,null,null);
        while (cursor.moveToNext()){
            int id = cursor.getInt(cursor.getColumnIndex("id"));
            String title = cursor.getString(cursor.getColumnIndex("title"));
            String url = cursor.getString(cursor.getColumnIndex("url"));
            String showstr = cursor.getString(cursor.getColumnIndex("isshow"));
            Boolean isshow = Boolean.valueOf(showstr);
            TypeBean typeBean=new TypeBean(id,title,url,isshow);
            list.add(typeBean);
        }
        return list;
    }
}

将数据库的声明 database 放到全局变量中,在 UniteApp.java 中添加:

DBManager.initDB(this); //声明全局的数据库对象

AddItemActivity 需要的就是数据库中的所有信息,这里可以直接调用 DBManager 方法来获取:

mDatas= DBManager.getAllTypeList();

此时数据源就有了,接下来要创建适配器对象,写一下ListView的适配器对象:新建一个java class AddItemAdapter ,让它继承于 BaseAdapter ,重新里面的四个方法:

public class AddItemAdapter extends BaseAdapter {
    Context context;
    List<TypeBean>mDatas;

    //通过构造方法将上面两个内容传递进来
    public AddItemAdapter(Context context, List<TypeBean> mDatas) {
        this.context = context;
        this.mDatas = mDatas;
    }

    @Override
    public int getCount() {
        return mDatas.size(); //返回一共显示的字段
    }

    @Override
    public Object getItem(int position) {
        return mDatas.get(position); //返回当前位置的数据源
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        convertView= LayoutInflater.from(context).inflate(R.layout.item_add_lv,null);

        //初始化convertView当中的控件
        TextView nameTv=convertView.findViewById(R.id.item_add_tv);
        final ImageView iv=convertView.findViewById(R.id.item_add_iv);

        //获取指定位置的数据
        final TypeBean typeBean=mDatas.get(position); //获取到当前位置的数据源
        nameTv.setText(typeBean.getTitle());

        //当isShow()设置为true的时候,对应的后面为对号,当isShow()为false的时候,对应的后面为加号,就是不选中
        if (typeBean.isShow()){
            iv.setImageResource(R.mipmap.subscribe_checked);
        }else {
            iv.setImageResource(R.mipmap.subscribe_unchecked);
        }

        //为了避免所有的选项都没有选中ViewPager没有东西可以显示,默认前两项是选中的
        if (position == 0 || position == 1) {
            iv.setVisibility(View.INVISIBLE);
        }else {
            iv.setVisibility(View.VISIBLE);
            convertView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    typeBean.setShow(!typeBean.isShow());  //改变选中的状态
                    if (typeBean.isShow()) {
                        iv.setImageResource(R.mipmap.subscribe_checked);
                    } else {
                        iv.setImageResource(R.mipmap.subscribe_unchecked);
                    }
                }
            });
        }
        return convertView;
    }
}

接下来就在 AddItemActivity 中创建适配器对象和设置适配器:

        //创建适配器对象
        adapter = new AddItemAdapter(this, mDatas);
        //设置适配器
        addLv.setAdapter(adapter);

至此这个界面完成。

每次选中想要订阅的频道,想要在下次打开app的时候还是保留上次选中的频道,还需要把点击的内容进行提交。

onPause 是Activity中的一个生命周期,表示失去焦点时调用的方法,Activity一共有七个生命周期: onCreate (创建了) 、 onStart(启动) 、 onResume(获取焦点)、 onPause (失去焦点)、 onStop (停止) 、 onDestroy(销毁)、 onRestart(重新启动)。

当一个Activity跳转到另一个界面,该Activity就会处于先onPause(失去焦点),再onStop(停止) 的阶段,并没有销毁,因为它依然在栈当中存在着,当返回到这个Activity界面之后,首先会执行onRestart(重新启动),不会执行创建,再执行onStart(启动)

所以 onRestart (重新启动)是失去焦点但是并没有销毁,重新获得焦点之后所执行的生命周期。

这些生命周期都不需要我们自己调用,Android底层会根据Activity的状态自动调用

所以这里可以用生命周期的状态来决定,这里一旦Activity的失去焦点,说明它已经被销毁(这里就是被销毁掉了,因为没有做跳转界面的操作),这里可以将本次选中的内容进行保留、提交。

所以这里可以写一下对于数据修改的方法。

DBOpenHelper 中添加:

    /*修改数据库当中信息的选中记录*/
    public static void updateTypeList(List<TypeBean>typeList){
        for (int i = 0; i < typeList.size(); i++) {
            TypeBean typeBean = typeList.get(i);
            String title = typeBean.getTitle();
            ContentValues values = new ContentValues();
            values.put("isshow",String.valueOf(typeBean.isShow()));
            database.update("itype",values,"title=?",new String[]{title}); //在主线程中直接修改数据库(该数据库数据量比较少可以这样做)
        }
    }

之后在 AddItemActivity 中添加如下代码,当该页面失去焦点时候,修改数据库:

    @Override
    protected void onPause() {
        super.onPause();
        DBManager.updateTypeList(mDatas);
    }

运行程序之前先把之前安装的app卸载掉,因为数据库只有在刚装的时候才会执行onCreate方法,如果是更新的话就不再执行onCreate方法了,而是执行onUpdate方法。
效果如下:

这里先不管ViewPager页数是否改变,可以看到可以正常返回上一级页面,在下次打开app的时候还会是保留上次选中的频道,说明 AddItemActivity 对于数据库的操作是正确的。

ViewPager页数的改变是获取数据库的信息,下面介绍。

5.页面详细信息逻辑编写

需要实现:根数数据库内容的变化来改变ViewPager和PagerSlidingTabStrip的显示;点击ListView中的每一项跳转到相应的网址当中。

需要将选中的条目放到一个集合中,在 DBManager 中添加如下代码:

    /*获取所有要求显示内容的集合*/
    public static  List<TypeBean>getSelectTypeList(){
        List<TypeBean>list = new ArrayList<>();
        Cursor cursor = database.query("itype", null, "isshow='true'", null, null, null, null);
        while (cursor.moveToNext()) {
            int id = cursor.getInt(cursor.getColumnIndex("id"));
            String title = cursor.getString(cursor.getColumnIndex("title"));
            String url = cursor.getString(cursor.getColumnIndex("url"));
            TypeBean bean = new TypeBean(id, title, url, true);
            list.add(bean);
        }
        return list;
    }

更改一下 MainActivity 中的代码,原来是获取TypeBean中的getTypeList()函数来显示全部页面,现在需要获取选中的栏目的页面,就来获取 DBManager中的getSelectTypeList()函数:

现在运行界面如下:(现在还并没有引起后面选中和前面对应的改变,只是显示的是后台选中的那几个栏目,)

一旦要改变ViewPager中的数量,就要对其Adapter——> NewsInfoAdapter 进行操作,需要在这里写一个函数 getItemPosition

    @Override
    public int getItemPosition(@NonNull Object object) {
        return PagerAdapter.POSITION_NONE;
    }

页数发生变化的情况是:当我们在频道界面改变订阅的频道时候,返回ViewPager的时候页面会发生变化。这里首先会执行 onRestart(重新启动),再执行 onStart (启动)。

在这里执行 onRestart 方法:

    protected void onRestart() {
        super.onRestart();

        //先清空ViewPager的数据源,再清空选中列表的数据源
        fragmentList.clear(); //首先将fragment整体清空
        selectTypeList.clear(); //将选中的列表清空

        initPager(); //重新加载ViewPager的显示页

        //提示上下都更新数据
        adapter.notifyDataSetChanged(); //通知adapter更新
        tabStrip.notifyDataSetChanged(); //通知上面的PagerSlidingTab更新
    }

现在的效果如下:

现在就实现了“频道订阅”会改变ViewPager所显示频道的功能。

现在还剩最后一个功能没有实现:就是点击ListView中的每一项跳转到相应的网址当中。

这里给出了直接的URL:

可以将这个网址直接放置到Webview当中展示。

新建一个package add (关于添加频道的包)把AddItemActivity和AddItemAdapter放进去(为了更加清晰明了)

直接新建一个Activity DescActivity ,布局如下比较简单:

DescActivity 中进行声明控件和找到控件:

NewsInfoFragment 中添加一个函数 setListener() ,这个是设置ListView中每一项点击事件的函数

    /*设置ListView中每一项点击事件的函数*/
    private void setListener() {
        infoLv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                //获取指定位置的数据源
                InfoBean.ResultBean.DataBean dataBean = mData.get(position);
                String url = dataBean.getUrl();
                Intent intent = new Intent(getActivity(), DescActivity.class); //从所在的Activity跳转到DescActivity当中
                //在跳转的过程中进行传值
                intent.putExtra("url",url);
                startActivity(intent);
            }
        });
    }

接下来就可以在 DescActivity 中获取所对应的URL,在 onCreate 中添加:

        url=getIntent().getStringExtra("url");

现在网址有啦,需要进行加载,在 onCreate 中继续添加:

        //创建WebView的设置类,对属性进行设置
        WebSettings webSettings = descWeb.getSettings();
        webSettings.setJavaScriptEnabled(true); //设置页面支持js交互
        webSettings.setUseWideViewPort(true);  //将图片调整到适合WebView页面的大小
        webSettings.setLoadWithOverviewMode(true); //缩放至屏幕大小
        webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);  //设置webview的缓存方式
        webSettings.setAllowFileAccess(true);  //设置可以访问文件
        webSettings.setJavaScriptCanOpenWindowsAutomatically(true); //支持js打开新窗口
        webSettings.setLoadsImagesAutomatically(true); //支持自动加载图片
        webSettings.setDefaultTextEncodingName("UTF-8"); //设置编码格式

        //设置要加载的网址
        descWeb.loadUrl(url); //此时系统会默认用手机浏览器打开网址

        //为了直接通过webview直接打开网址,需要设置以下操作
        descWeb.setWebViewClient(new WebViewClient(){
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
                //使用webview要加载的URL
                view.loadUrl(url);
                return true;
            }
        });

现在的效果如下:

只是点击返回的时候直接返回上一级Activity而不是上一级页面,如果想要返回上一级页面的话,进行如下操作:

重写 DescActivity 中的 onKeyDown 方法:

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK && descWeb.canGoBack()) {
            descWeb.goBack(); //返回上一级
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }

再次运行:

到此实现基本功能。

在values下的 strings.xml 可以更改应用名称:

在mipmap中导入图标,接下来在 AndroidManifest.xml 中修改就可以啦!

posted @ 2020-07-19 21:15  Ylxxxxx  阅读(1490)  评论(0编辑  收藏  举报