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