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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM