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);
ViewPager
和 PagerSlidingTabStrip
是相互对应的, 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.xml
的 application
节点下 添加以下内容:
<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
中的数据源应该是之前写的 TypeBean
, TypeBean
中就封装了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
中修改就可以啦!