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
中修改就可以啦!