概述
本次快速开发Android应用系列,是基于课工场的公开课高效Android工程师6周培养计划,记录微服私访APP的整个开发过程以及当中碰到的问题,供日后学习参考。
上一篇我们主要实现通过picasso获取服务器图片,并通过轮播图的形式展现以及实现个人中心界面的展示。还没看过前一篇文章的朋友可以先去参考快速开发android应用5-使用picasso实现轮播图
本篇我们主要实现首页最新任务、最新资讯的获取与展示,以及巡店页面历史巡店数据的获取、展示和搜索功能。涉及到的项目知识点包括:
- 使用
recyclerview
展示任务及资讯信息 - 解决
scrollerview
和recyclerview
滑动冲突,实现同屏滑动 - 使用
xrecyclerview
展示历史巡店信息 - 通过关键字搜索历史巡店信息
效果图:
首页资讯、任务获取展示
获取数据
任务获取接口
请求报文
请求url:http://localhost:8080/visitshop/task?pagenum=1
请求类型:GET响应报文
{
"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
}
]
}
资讯获取接口
请求报文
请求url:http://localhost:8080/visitshop/info?pagenum=1&type=0
请求类型:GET响应报文
{
"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展现历史巡店列表数据
获取数据
请求报文
请求url:http://localhost:8080/visitshop/history?userid=num01&pagenum=1
请求类型:GET响应报文
{
"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;
}
附录
快速开发android应用相关的代码都会更新在我的github上,大家可以通过star来跟进项目代码的变动。
https://github.com/youyutorch/RapidDevAndroid
参考资料: