1. 程式人生 > >快速開發android應用6-實現scrollview和recyclerview同方向滑動

快速開發android應用6-實現scrollview和recyclerview同方向滑動

概述

本次快速開發Android應用系列,是基於課工場的公開課高效Android工程師6周培養計劃,記錄微服私訪APP的整個開發過程以及當中碰到的問題,供日後學習參考。

上一篇我們主要實現通過picasso獲取伺服器圖片,並通過輪播圖的形式展現以及實現個人中心介面的展示。還沒看過前一篇文章的朋友可以先去參考快速開發android應用5-使用picasso實現輪播圖

本篇我們主要實現首頁最新任務、最新資訊的獲取與展示,以及巡店頁面歷史巡店資料的獲取、展示和搜尋功能。涉及到的專案知識點包括:

  • 使用recyclerview展示任務及資訊資訊
  • 解決scrollerviewrecyclerview
    滑動衝突,實現同屏滑動
  • 使用xrecyclerview展示歷史巡店資訊
  • 通過關鍵字搜尋歷史巡店資訊
    效果圖:
    這裡寫圖片描述

首頁資訊、任務獲取展示

獲取資料

任務獲取介面

{
  "code": 0,
  "msg": "任務資訊獲取成功",
  "body": [
    {
      "title": "新產品Y008調研",
      "detail": "針對公司新產品Y008的市場調研。需要來店裡諮詢的客戶填寫問卷,並留聯絡方式。問卷已傳送至各位郵箱,請下載並列印。",
      "publishdate": "2016-08-15",
      "executedate": "2016-09-30"
, "state": 0 }, { "title": "使用者反饋統計", "detail": "需要來店裡諮詢的使用者統計,統計使用者相關資訊,資訊表需要到公司網站下載。", "publishdate": "2016-07-15", "executedate": "2016-10-11", "state": 0 }, { "title": "關於意見反饋", "detail": "現在開通提意見、贏大獎活動,可以提出自己發現目前存在的問題,或者有其他的建議,一經採納給予獎勵。意見通道可以通過APP意見反饋提交"
, "publishdate": "2016-07-11", "executedate": "2016-08-11", "state": 0 }, { "title": "優秀員工評選", "detail": "評選優秀員工,需要個員工登入公司網站進行投票,沒有投票的視為棄權。", "publishdate": "2016-05-10", "executedate": "2016-06-01", "state": 0 }, { "title": "回收舊產品,以舊換新", "detail": "公司現在退出以舊換新活動,相關說明檢視公司網站活動模組。", "publishdate": "2016-03-15", "executedate": "2016-07-10", "state": 1 }, { "title": "新產品XS03型號宣傳推廣", "detail": "新產品XS03型號的宣傳,每個店面需要放置相關產品在主要位置,店面門口需要有推介海報。展示產品需要有專人負責講解,會進行不定期抽查。", "publishdate": "2016-03-01", "executedate": "2016-06-01", "state": 1 }, { "title": "年度總結", "detail": "各店面對去年一年工作總結,包括銷售業績、發現的問題、解決方式,經理需要對每個員工做出考核評價", "publishdate": "2016-01-05", "executedate": "2016-01-25", "state": 1 } ]
}


資訊獲取介面

{
  "code": 0,
  "msg": "資訊資訊獲取成功",
  "body": [
    {
      "title": "華為聯想全球化啟示:如何在海外構建中國品牌",
      "summary": "騰訊科技",
      "imgurl": "http://mat1.gtimg.com/tech/00Jamesdu/2014/index/remark/2.png",
      "detail": "http://tech.qq.com/a/20151123/008196.htm"
    },
    {
      "title": "聯想取消中高階手機品牌VIBE",
      "summary": "騰訊科技",
      "imgurl": "http://mat1.gtimg.com/tech/00Jamesdu/2014/index/remark/2.png",
      "detail": "http://tech.qq.com/a/20151123/018308.htm"
    },
    {
      "title": "聯想計劃明年在印度生產1000萬部手機",
      "summary": "騰訊科技",
      "imgurl": "http://img1.gtimg.com/tech/pics/hv1/101/186/1974/128406881.jpg",
      "detail": "http://tech.qq.com/a/20151126/043557.htm"
    },
    {
      "title": "聯想簽約高通:中國手機產業躲不過專利費",
      "summary": "騰訊科技",
      "imgurl": "http://img1.gtimg.com/tech/pics/hv1/165/235/2024/131670690.jpg",
      "detail": "http://tech.qq.com/a/20160224/030848.htm"
    },
    {
      "title": "聯想:非洲是下個最大手機市場 超印度和中國",
      "summary": "騰訊科技",
      "imgurl": "http://img1.gtimg.com/tech/pics/hv1/224/96/2026/131765354.jpg",
      "detail": "http://tech.qq.com/a/20160226/044907.htm"
    },
    {
      "title": "眾創空間WeWork融資4.3億美元 聯想控股領投",
      "summary": "騰訊科技",
      "imgurl": "http://img1.gtimg.com/tech/pics/hv1/223/117/2033/132225883.jpg",
      "detail": "http://tech.qq.com/a/20160310/025118.htm"
    },
    {
      "title": "陳旭東公開信:聯想將在國內扭轉智慧手機業務",
      "summary": "騰訊科技",
      "imgurl": "http://mat1.gtimg.com/tech/00Jamesdu/2014/index/remark/2.png",
      "detail": "http://tech.qq.com/a/20160318/043508.htm"
    },
    {
      "title": "小米華為聯想魅族推出的千元機,都不是自己設計的",
      "summary": "網易新聞",
      "imgurl": "http://inews.gtimg.com/newsapp_ls/0/305511207_300240/0",
      "detail": "http://tech.qq.com/a/20160518/076472.htm"
    },
    {
      "title": "聯想釋出模組化手機Moto Z 投影、攝影、背殼能自選",
      "summary": "騰訊科技",
      "imgurl": "http://inews.gtimg.com/newsapp_ls/0/555495485_300240/0",
      "detail": "http://tech.qq.com/a/20160906/038980.htm"
    },
    {
      "title": "聯想的AR手機延期上市,智慧手機找點“創新”真不容易",
      "summary": "騰訊科技",
      "imgurl": "http://inews.gtimg.com/newsapp_ls/0/580125438_300240/0",
      "detail": "http://tech.qq.com/a/20160914/009726.htm"
    }
  ]
}

使用RecyclerView展現資料

以前展現列表資料,首先就會想到ListView,使用過ListView的人都碰到過滑動困頓,點選事件混亂,佈局不靈活等問題。基於以上幾點google推出了RecyclerView:ListView的升級版,它不僅解決了原來在ListView上存在的問題,而且佈局更靈活,同時提高了效率。
使用RecyclerView的方法和ListView是類似的。

第一步,獲取資料來源,以獲取資訊資料為例

  private void requestInfo() {
        Log.i(TAG, "requestInfo - 請求獲取資訊");
        String url = RequestUrl.Info + "?pagenum=1&type=0"; //獲取公司第一頁動態
        OkHttpHelper.getInstance().doGet(url, new OkHttpHelper.RequestCallback() {
            @Override
            public void onSuccess(String result) {
                Log.d(TAG, "requestInfo onSuccess 成功獲取資訊 - " + result);

                mInfoList = GsonUtil.parseInfoJson(result);
                if (mInfoList == null) {
                    //從資料庫中讀取
                    mInfoList = DataSupport.findAll(Info.class);
                } else {
                    //更新到資料庫
                    DataSupport.deleteAll(Info.class);
                    DataSupport.saveAll(mInfoList);
                }
                showInfoList(mInfoList);
            }

            @Override
            public void onFailure(IOException e) {
                Log.w(TAG, "requestInfo onFailure 獲取資訊失敗");
                e.printStackTrace();

                //從資料庫中讀取
                mInfoList = DataSupport.findAll(Info.class);
                showInfoList(mInfoList);
            }
        });
    }

第二步,建立一個Adapter例項,以資訊列表的InfoListBaseAdapter為例。

/**
 * HomeFragment資訊列表介面卡
 */
public class InfoListBaseAdapter extends RecyclerView.Adapter<InfoListBaseAdapter.InfoViewHolder> {
    private Context mContext;
    private List<Info> list;
    int number;

    public InfoListBaseAdapter(Context mContxt, List<Info> list, int number) {
        this.mContext = mContxt;
        this.list = list;
        this.number = number;
    }

    @Override
    public InfoViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = View.inflate(mContext, R.layout.fragment_home_info_item, null);
        return new InfoViewHolder(view);
    }

    @Override
    public void onBindViewHolder(InfoViewHolder holder, final int position) {
        Info rd = list.get(position);
        holder.title.setText(rd.getTitle());
        holder.context.setText(rd.getSummary());
        if (!"".equals(rd.getImgurl().trim()) && rd.getImgurl() != null) {
            Picasso.with(mContext).load(rd.getImgurl()).into(holder.img);
        }
        holder.root.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //跳轉到資訊詳情
                Toast.makeText(mContext, "資訊詳情介面敬請期待" + position, Toast.LENGTH_SHORT).show();
            }
        });
    }

    @Override
    public int getItemCount() {
        //顯示list的前幾條資料
//        if (list.size() >= number) {
//            return number;
//        } else {
            return list.size();
//        }
    }

    class InfoViewHolder extends RecyclerView.ViewHolder {
        ImageView img, arrow;
        TextView title, context;
        RelativeLayout root;

        public InfoViewHolder(View itemView) {
            super(itemView);
            arrow = (ImageView) itemView.findViewById(R.id.fragment_home_info_item_arrow);
            img = (ImageView) itemView.findViewById(R.id.fragment_home_info_item_img);
            context = (TextView) itemView.findViewById(R.id.fragment_home_info_item_context);
            title = (TextView) itemView.findViewById(R.id.fragment_home_info_item_title);
            root = (RelativeLayout) itemView.findViewById(R.id.item_root_home);
        }
    }

}
  • 這裡的InfoListBaseAdapter.InfoViewHolder繼承自RecyclerView.ViewHolder,它的作用和使用ListView寫的自定義ViewHolder的作用相同,都是再重新獲取itemview例項時,不需要再呼叫findViewById去找個各個子view,提高效率。
  • 在onCreateViewHolder()方法初始化view,然後在onBindViewHolder()繫結具體的position,繫結相關的資料。
  • 與ListView不同,RecyclerView是不能通過setOnItemClickListener()方法去設定item click事件,只能通過類似item.setOnClickListener()方法去設定。


第三步,初始化RecyclerView,設定介面卡、佈局管理器、是否需要動畫等。

        //獲取物件
        mInfoRecyclerView = (RecyclerView) view.findViewById(R.id.fragment_home_info_list);
       //設定佈局管理器   
       LinearLayoutManager infoLayoutManager = new LinearLayoutManager(mActivity, LinearLayoutManager.VERTICAL, false);
       //設定介面卡
                   mInfoRecyclerView.setLayoutManager(infoLayoutManager);

解決滑動衝突

當前主頁的佈局是一個scrollview巢狀著兩個recyclerview(一個顯示任務列表,一個顯示資訊列表),scrollviewrecyclerview都是可以上下滑動的,那怎麼解決它們的滑動衝突問題呢?
第一種方法recyclerview資料量較少,比如說我只顯示任務列表的前三條資料,那我們可以直接設定recyclerview不能滑動,全權把上下滑動事件交給scrollview處理。

        //設定task recyclerview不可滑動
        mTaskRecyclerView.setNestedScrollingEnabled(false);

第二種方法,設定scrollviewrecyclerview都是可滑動的,當上下滑動事件在recyclerview的區域時,那就滑動recyclerview;當不在recyclerview的區域時,就滑動scrollview

要實現scrollviewrecyclerview都可以滑動的效果,首先我們要了解scrollview具體是如何工作的?

原因

當事件分發到scrollview,會呼叫其中的onInterceptTouchEvent()方法,在這裡scrollview會去做判斷,若當前頁面可滑動且使用者正在做上下滑動,則擷取這個事件,交由自己的方法onTouchEvent()處理。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        /*
         * This method JUST determines whether we want to intercept the motion.
         * If we return true, onMotionEvent will be called and we do the actual
         * scrolling there.
         */

        /*
        * Shortcut the most recurring case: the user is in the dragging
        * state and he is moving his finger.  We want to intercept this
        * motion.
        */
        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
            return true;
        }

        if (super.onInterceptTouchEvent(ev)) {
            return true;
        }
        ...
    }

這也解釋了,為什麼不能對recyclerview進行滑動的原因,因為滑動事件已經被scrollview截取了,recyclerview壓根沒有收到滑動事件,肯定不會滑動了。

解決方法

瞭解了這個原因之後,我們就可以通過重寫scrollviewonInterceptTouchEvent()方法來重新判斷滑動事件是否要擷取。

這裡以資訊列表的mTaskRecyclerView為例,設定mTaskRecyclerView的高度值固定,當scrollview 滑動最底端(即滑動值getScrollY達到最大值時),不再擷取上下滑動事件。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (getScrollY() >= (getMaxScrollAmount() - 20)) {
            //不再截斷,將滑動事件交給子view處理
            return false;
        }
        return super.onInterceptTouchEvent(ev);
    }

當加上這段程式碼後,scrollview滑到最底端後,因為getScrollY()值不變,所有的上下滑動事件都交給子view mTaskRecyclerView了,導致不能重新滑到頂端。

為了能重新擷取到上下滑動事件,需要根據ev.getRawY()方法獲取當前觸控事件所在的位置,如果不在mTaskRecyclerView的區域內,則重新擷取上下滑動事件。

  @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(TAG, "onInterceptTouchEvent - getScrollY()=" + getScrollY() + ", getMaxScrollAmount()=" + getMaxScrollAmount());
        Log.d(TAG, "onInterceptTouchEvent - ev.getRawY()=" + ev.getRawY());
        Log.d(TAG, "onInterceptTouchEvent - mMaxScreenY=" + mMaxScreenY);
        if (getScrollY() >= (getMaxScrollAmount() - 20)) {
            if (mMaxScreenY == 0) {
                mMaxScreenY = mListener.getMaxScreenY();
            }
            mIsInBottomArea = ev.getRawY() > mMaxScreenY;
            if (mIsInBottomArea) {
                //不再截斷,將滑動事件交給子view處理
                return false;
            }
        }
        return super.onInterceptTouchEvent(ev);
    }

這裡的mMaxScreenY 獲取的是當scrollview滑到最底端後,mTaskRecyclerView的最小Y座標。

    public interface ScreenListener {
        int getMaxScreenY(); //獲取能滑動的最大Y座標
    }

        private void initScreenListener() {
        mPartScrollView.setScreenListener(new PartScrollView.ScreenListener() {
            @Override
            public int getMaxScreenY() {
                int height = mTaskRecyclerView.getHeight();
                int[] location = new int[]{0, 0};
                mTaskRecyclerView.getLocationOnScreen(location);
                Log.d(TAG, "getMaxScreenY - location[1]=" + location[1] + ",height=" + height);
                return location[1] + height;
            }
        });
    }

XRecyclerView展現歷史巡店列表資料

獲取資料

{
  "code": 0,
  "msg": "歷史巡店查詢成功",
  "page": 1,
  "datelist": [
    {
      "id": 1,
      "visitdate": "2016-10-20",
      "shopid": "WFSF75",
      "shoplocation": "中國北京市海淀區成府路207號",
      "userid": "num01",
      "shoplevel": "5;5;5",
      "feedback": "店面整潔,人員精神飽滿,沒有發現問題",
      "name": "北京新中關購物中心店",
      "imgpath": "/visitshop/img/visit/2016-10-20/",
      "imgname": "1476951920804_1.jpg;1476951920805_2.jpg"
    }
  ]
}

資料展現

XRecyclerView是RecyclerView的升級版,它是在RecyclerView的基礎上增加:

  • 增加上拉重新整理、下載載入更多自定義控制元件
  • 增加獲取不到資料時展現EmptyView
  • 增加自定義控制元件載入風格、自定義圖片

具體使用方法也和RecyclerView類似,在RecyclerView的基礎上,增加自定義方法setLoadingListener()、setEmptyView()等。

        recyclerView = (XRecyclerView) view.findViewById(R.id.activity_visitshop_list);
        recyclerView.setLoadingListener(this);
        //設定載入風格
        recyclerView.setLoadingMoreProgressStyle(ProgressStyle.SquareSpin);
        recyclerView.setRefreshProgressStyle(ProgressStyle.BallSpinFadeLoader);
        //設定線性列表展示
        recyclerView.setLayoutManager(
                new LinearLayoutManager(mActivity, LinearLayoutManager.VERTICAL, false));
        recyclerView.setAdapter(adapter);
        //設定空佈局
        View emptyView = view.findViewById(R.id.activity_visitshop_none);
        emptyView.setOnClickListener(this);
        recyclerView.setEmptyView(emptyView);

    @Override
    public void onRefresh() {
        //下拉重新整理
        pagenum = 1;
        initData();
    }

    @Override
    public void onLoadMore() {
        //載入更多
        initData();
    }

還有其他一些用法如設定自定義載入風格,設定自定義圖片,想了解的可以上github上看一下具體的使用說明。

搜尋框的實現

這裡,直接使用EditTextsetOnEditorActionListener()方法來實現,通過過載onEditorAction()來獲取搜尋點選的事件,並做相應的處理。

    @Override
    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
        /**
         * 當點選搜尋按鈕時
         */
        if (actionId == EditorInfo.IME_ACTION_SEARCH) {
            hideKeyboard();
            shop_name = search.getText().toString().trim();
            progress.setVisibility(View.VISIBLE);
            pagenum = 1;
            //店面查詢請求
            String urlString = RequestUrl.HistroyShop + "?userid=" + userid + "&pagenum=" + pagenum + "&shopName=" + shop_name;
            OkHttpHelper.getInstance().doGet(urlString, new OkHttpHelper.RequestCallback() {
                @Override
                public void onSuccess(String result) {
                    getShopSuccess(result);
                }

                @Override
                public void onFailure(IOException e) {
                    getShopFailed();
                }
            });
            IsSearch = true;
        }
        return false;
    }

附錄