本次快速开发Android应用系列,是基于课工场的公开课高效Android工程师6周培养计划,记录微服私访APP的整个开发过程以及当中碰到的问题,供日后学习参考。
上一篇我们主要实现APP的主页界面的框架,使用viewpager+fragment来展现主页内容,使用BottomNavigationBar来完成页面的切换。
还没看过前一篇文章的朋友可以先去参考快速开发android应用4-使用viewpager+fragment构建主页
本篇主要通过picasso获取服务器图片,并通过轮播图的形式展现以及实现个人中心界面的展示。涉及到的项目功能点包括:
picasso+viewpager
展现轮播图SettingItemview
的实现 这里的图片是通过服务端的首页广告轮播接口获取的。
请求报文
请求url:http://localhost:8080/visitshop/announcement
请求方式:GET
响应报文
{
"code": 0,
"msg": "获取公告成功",
"body": [
{
"id": 4,
"imgUrl": "/visitshop//img/ann/ann4.jpg"
},
{
"id": 3,
"imgUrl": "/visitshop//img/ann/ann3.jpg"
},
{
"id": 2,
"imgUrl": "/visitshop//img/ann/ann2.jpg"
},
{
"id": 1,
"imgUrl": "/visitshop/img/ann/ann1.jpg"
}
]
}
picasso
是android大神 JakeWharton开发的一套图片缓存框架,文档上是这么介绍的。
A powerful image downloading and caching library for Android
使用起来也非常简单,只需要一行代码就可以搞定,真心nb
Picasso.with(context).load("http://i.imgur.com/DvpvklR.png").into(imageView);
在当前项目中,考虑到后续图片缓存框架的扩展性,对picasso
做了一层封装,结合viewpager
,实现轮播图效果。
第一步:添加picasso
库依赖
Picasso.with(context).load("http://i.imgur.com/DvpvklR.png").into(imageView);
第二步:封装图片加载过程到一个单独的方法中
/**
* 根据地址获取图片
* @param context
* @param urls 图片访问地址
* @param imgViews 显示图片的views
* @return
*/
public static void loadFromUrls(Context context, List urls, List imgViews) {
if (urls == null || urls.isEmpty()) {
return;
}
int count = urls.size(); //图片数量
if (imgViews == null || imgViews.isEmpty()) {
return;
}
if (imgViews.size() < count) {
count = imgViews.size();
}
for (int i = 0; i < count; i++) {
Log.d(TAG, "Picasso load url:" + urls.get(i));
Picasso picasso = new Picasso.Builder(context).build();
if (mCacheFile != null && mCacheFile.exists()) {
picasso = new Picasso.Builder(context)
.downloader(new OkHttp3Downloader(mCacheFile))
.build();
}
picasso.load(urls.get(i))
.into(imgViews.get(i));
}
}
第三步:使用viewpager.setPageAdapter
方法,设置图片imageViews
到viewpager
中。
private List mNoticeImgs = new ArrayList<>(); //公告图列表
mViewPager = (ViewPager)view.findViewById(R.id.fragment_img_viewpager);
mViewPager.setAdapter(mPagerAdapter);
private PagerAdapter mPagerAdapter = new PagerAdapter() {
@Override
public int getCount() {
return mNoticeImgs.size();
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
// Log.d(TAG, "ImageViewPager - setPrimaryItem " + position);
super.setPrimaryItem(container, position, object);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
Log.d(TAG, "ImageViewPager - instantiateItem " + position);
ImageView view = mNoticeImgs.get(position);
container.addView(view);
return view;
}
@Override
public Parcelable saveState() {
// Log.d(TAG, "ImageViewPager - saveState");
return super.saveState();
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
// Log.d(TAG, "ImageViewPager - restoreState");
super.restoreState(state, loader);
}
@Override
public void startUpdate(ViewGroup container) {
// Log.d(TAG, "ImageViewPager - startUpdate");
super.startUpdate(container);
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Log.d(TAG, "ImageViewPager - destroyItem " + position);
container.removeView(mNoticeImgs.get(position));
}
};
最后一步:就是通过网络请求获取图片请求地址列表,构建mNoticeImgs
数据,调用loadFromUrls
并刷新界面即可,具体代码就不贴了,需要的可自行下载,会在文章最后的附录中提供。
//网络请求、数据创建相关代码...
ImageLoader.loadFromUrls(mActivity, urls, mNoticeImgs);
//更新界面显示
mPagerAdapter.notifyDataSetChanged();
Picasso
默认的缓存路径位于data/data/your package name/cache/picasso-cache/
下。开发过程中我们难免会遇到一些需求,需要我们去修改图片的缓存路径。
picasso
的底层是通过okhttp
去下载图片的。
早期的okhttp
版本,可以直接使用picasso.download(new OkHttpDownloader(client))
方法设置一个downloader
,然后在okhttpclient.cache(new Cache(file, maxSize))
中设置一个新的缓存路径。如下代码所示
File file = new File("your cache path");
if (!file.exists()) {
file.mkdirs();
}
//设置图片缓存大小为运行时缓存的八分之一
long maxSize = Runtime.getRuntime().maxMemory() / 8;
OkHttpClient client = new OkHttpClient.Builder()
.cache(new Cache(file, maxSize))
.build();
Picasso picasso = new Picasso.Builder(this)
.downloader(new OkHttpDownloader(client))
.build();
但okhttp3版本不支持这样设置downloader,那该怎么更改缓存路径呢?
为了解决上面描述的不能使用OkHttp3
作为下载器的问题,jakewharton
大神专门写了一个OkHttp3Downloader
库,只要使用OkHttp3Downloader
替换前面的OkHttpDownloader就能支持啦
。
获取OkHttp3Downloader库地址为:https://github.com/JakeWharton/picasso2-okhttp3-downloader
大致的思路是启动一个定时器,每隔一段时间更改当前所处的页面,就能实现自动轮播的效果了。
android中实现定时操作的方法有很多,这里使用timer和timertask实现
第一步: 自定义图片轮播task
//自定义一个task,用于图片轮播
class AutoSlipTask extends TimerTask {
@Override
public void run() {
Log.i(TAG, "AutoSlipTask start, curr page=" + mCurrPosition);
if (mIsAutoSlide) {
int temp = mNoticeImgs.size() - 2;
if (mCurrPosition < 1) {
mCurrPosition = 1;
}
if (mCurrPosition > temp) {
mCurrPosition = temp;
}
mCurrPosition = (mCurrPosition + 1) % temp;
Log.i(TAG, "slip page to " + mCurrPosition);
mHandler.sendEmptyMessage(MSG_UPDATE_VEEWPAGE);
}
}
}
这里要注意:图片切换操作(属于UI操作)要在主线程,否则会报错
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == MSG_UPDATE_VEEWPAGE) {
mViewPager.setCurrentItem(mCurrPosition);
updataIndicator(mCurrPosition);
}
}
};
第二步:启动定时器
private void addAutoSlip() {
if (mNoticeImgs.size() < 2) {
return;
}
if (mTimer != null) {
mTimer.cancel();
}
mTimer = new Timer("AutoSlipTimer");
mTimer.schedule(new AutoSlipTask(), 3000, 3000);
}
第三步:通过前两步已经实现自动轮播的效果了,这步主要是处理当用户离开homefragment
,轮播不可见时,需要停止定时器;重新进入homefragment
后,再重新启动。
@Override
public void onStart() {
super.onStart();
Log.d(TAG, "onStart - " + mFragmentName);
//增加自动播放
if (mTimer == null) {
addAutoSlip();
}
}
@Override
public void onStop() {
super.onStop();
Log.d(TAG, "onStop - " + mFragmentName);
if (mTimer != null) {
mTimer.cancel();
mTimer = null;
}
}
这里循环滑动指的是当用户滑到第一张图片后,再向前滑,则会跳转到最后一张图片; 滑到最后一张图片后,再向后滑,则会跳转到第一张图片;从而实现循环滑动的效果。
使用viewpager要实现这样的效果,网上大部分的思路是在当前图片基础上增加两张,分别放在第一页和最后一页,第一页放原来最后一张图片的内容,最后一页放原来第一张图片的内容。当滑到最后一页,则直接跳转到第二页(即原来第一张图片的位置);当滑到第一页,则直接跳转到倒数第二页(即原来最后一张图片的位置)。
代码实现过程
/**
* 增加循环滑动效果
* @param requestUrls 请求的图片地址列表
*/
private void addCycleSlip(List requestUrls) {
if (requestUrls.size() < 2) {
return;
}
int len = requestUrls.size();
String firstUrl = requestUrls.get(len - 1);
String lastUrl = requestUrls.get(0);
//在第一页前加最后一张图
mNoticeImgs.add(0, buildImage(lastUrl));
requestUrls.add(0, lastUrl);
//在最后一页加第一张图
mNoticeImgs.add(buildImage(firstUrl));
requestUrls.add(firstUrl);
}
还要注意一点,循环轮播,图片数增加了,但指示点数是不能增多的,并且当前所在图片位置也要跟着变换处理。
private void updataIndicator(int position) {
//清除所有指示下标
mIndicatorLayout.removeAllViews();
//需要去掉重复的第一页和最后一页
for (int i = 1; i < mNoticeImgs.size() - 1; i++) {
ImageView img = new ImageView(mActivity);
//添加下标圆点参数
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
params.leftMargin = 5;
params.rightMargin = 5;
img.setLayoutParams(params);
img.setImageResource(R.drawable.sns_v2_page_point);
if (i == position) {
img.setSelected(true);
}
mIndicatorLayout.addView(img);
}
}
滑动页面时,指示点也要跟着变化
@Override
public void onPageSelected(int position) {
Log.i(TAG, "onPageSelected - " + position);
int size = mNoticeImgs.size();
//右滑到最后一页
if (position == size - 1) {
mViewPager.setCurrentItem(1, false);
return;
}
//左滑到第一页
if (position == 0) {
mViewPager.setCurrentItem(size - 2);
return;
}
mCurrPosition = position;
for (int i = 0; i < mIndicatorLayout.getChildCount(); i++) {
ImageView image = (ImageView) mIndicatorLayout.getChildAt(i);
if (i == position - 1) {
image.setSelected(true);
} else {
image.setSelected(false);
}
}
}
设置一个标志mIsAutoSlide
,通过viewpager.setOnTouchListener()
方法来获取用户的滑动事件,当用户在滑动时,不进行图片切换。
mViewPager.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.w(TAG, "receive event:" + event.getAction());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mIsAutoSlide = false;
break;
case MotionEvent.ACTION_MOVE:
mIsAutoSlide = false;
break;
case MotionEvent.ACTION_UP:
mIsAutoSlide = true;
break;
}
return false;
}
当页面在滑动过程中,也不进行图片的切换
@Override
public void onPageScrollStateChanged(int state) {
Log.i(TAG, "onPageScrollStateChanged - state=" + state);
if (state == ViewPager.SCROLL_STATE_DRAGGING) {
mIsAutoSlide = false;
} else {
mIsAutoSlide = true;
}
}
常用的自定义view的方式有:
这里的SetItemView
使用的是第一种方式,具体如何实现呢。
首先,定义一个布局文件settingitem.xml
文件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rootLayout"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@drawable/bg"
android:clickable="true"
android:gravity="center_vertical">
<ImageView
android:id="@+id/iv_lefticon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="23dp"
android:src="@drawable/clearcache"/>
<TextView
android:id="@+id/tv_lefttext"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="10dp"
android:layout_toRightOf="@id/iv_lefticon"
android:gravity="center_vertical"
android:textSize="16sp"/>
<ImageView
android:id="@+id/iv_righticon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_gravity="center"
android:layout_marginRight="23dp"
android:src="@drawable/task_arrow"/>
<View
android:id="@+id/underline"
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_alignParentBottom="true"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:background="#99999999"/>
RelativeLayout>
然后,实现SetItemView,重写view构造方法
public SetItemView(Context context) {
this(context, null);
}
public SetItemView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SetItemView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
getCustomStyle(context, attrs);
//设置点击监听事件
mRootLayout.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (null != mOnSetItemClick) {
mOnSetItemClick.click();
}
}
});
}
这里要实现自定义属性,需要新建一个attrs.xml
文件,添加要定义的属性名称
<resources>
<declare-styleable name="SetItemView">
<attr name="leftText" format="string"/>
<attr name="leftIcon" format="integer"/>
<attr name="rightIcon" format="integer"/>
<attr name="textSize" format="float"/>
<attr name="textColor" format="color"/>
<attr name="isShowUnderLine" format="boolean"/>
declare-styleable>
resources>
使用时,在xml根布局
中声明 xmlns:app="http://schemas.android.com/apk/res-auto"
,然后使用app:的方式来使用自定义属性
<com.torch.chainmanage.view.SetItemView
android:id="@+id/rl_me"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:isShowUnderLine="false"
app:leftIcon="@drawable/medata"
app:leftText="@string/fragment_me_tv_me"
app:rightIcon="@drawable/task_arrow"
app:textColor="@color/text_color_6"
app:textSize="16"/>
最后,在创建SetItemView
对象时,通过重写构造函数,得到自定义属性值,在完成SetItemView
的初始化。
/**
* 初始化控件自定义属性信息
*
* @param context
* @param attrs
*/
private void getCustomStyle(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SetItemView);
...
}
快速开发android应用相关的代码都会更新在我的github上,大家可以通过star来跟进项目代码的变动
https://github.com/youyutorch/RapidDevAndroid
参考资料: