快速開發android應用6-實現scrollview和recyclerview同方向滑動
概述
本次快速開發Android應用系列,是基於課工場的公開課高效Android工程師6周培養計劃,記錄微服私訪APP的整個開發過程以及當中碰到的問題,供日後學習參考。
上一篇我們主要實現通過picasso獲取伺服器圖片,並通過輪播圖的形式展現以及實現個人中心介面的展示。還沒看過前一篇文章的朋友可以先去參考快速開發android應用5-使用picasso實現輪播圖
本篇我們主要實現首頁最新任務、最新資訊的獲取與展示,以及巡店頁面歷史巡店資料的獲取、展示和搜尋功能。涉及到的專案知識點包括:
- 使用
recyclerview
展示任務及資訊資訊 - 解決
scrollerview
和recyclerview
- 使用
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
(一個顯示任務列表,一個顯示資訊列表),scrollview
和recyclerview
都是可以上下滑動的,那怎麼解決它們的滑動衝突問題呢?
第一種方法,recyclerview
資料量較少,比如說我只顯示任務列表的前三條資料,那我們可以直接設定recyclerview
不能滑動,全權把上下滑動事件交給scrollview
處理。
//設定task recyclerview不可滑動
mTaskRecyclerView.setNestedScrollingEnabled(false);
第二種方法,設定scrollview
和recyclerview
都是可滑動的,當上下滑動事件在recyclerview
的區域時,那就滑動recyclerview
;當不在recyclerview
的區域時,就滑動scrollview
。
要實現scrollview
和recyclerview
都可以滑動的效果,首先我們要了解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
壓根沒有收到滑動事件,肯定不會滑動了。
解決方法
瞭解了這個原因之後,我們就可以通過重寫scrollview
的 onInterceptTouchEvent
()方法來重新判斷滑動事件是否要擷取。
這裡以資訊列表的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上看一下具體的使用說明。
搜尋框的實現
這裡,直接使用EditText
的 setOnEditorActionListener
()方法來實現,通過過載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;
}