前言:相信大家肯定都在项目开发中或多或少的接触过轮转广告图,有些朋友也自己写过。其实是比较初级的东西,是去年在做交行客户端维护时将Gallery的实现替换为ViewPager的一种方案。经过朋友们以及在其他项目的使用,功能不断完善,使用也更简单。转载请标明出处:
http://blog.csdn.net/xuehuayous/article/details/50518393;
本文出自:【Kevin.zhou的博客】
一、 最终效果
按照以前博客的风格,我们先看下最终实现的效果:
二、 需求
我们希望做可以做成这样的,可以在xml布局中引入控件,然后再代码中设置数据,这样他就可以自己轮转了,我不关心它内部是怎么实现的,包括下载图片,我给你设置的数据里面包含图片的链接地址你自己去给我下载并且显示就是了,不需要我再给你设置,当然图片能够缓存,而且根据控件大小来压缩图片就更好了。OK,以上的都满足你!
三、实现方案
实现方案比较简单,就是使用ViewPager显示图片,使用Handler实现定时跳转。
四、定义数据实体
通常我们的轮转大图会有图片、下方的描述文本,点击后还要跳转,当然可以是Activity或者网址等,基本就是这些。那我们就先定义数据实体类。
其实我们要descText、imgUrl、link就可以了,但是为什么还提供type呢?细心的朋友可能注意过交通银行轮转大图的第一个图片和其他的不同,为一个天气信息,那么就可以定义这个type了,比如这个type为weather,那么通过判断type来加载不同布局。
五、 定义接口
我们根据需求来定义接口,首先是可以设置数据、更新数据,设置的数据可以是JSON、实体类、以及集合的方式,当然除了数据的操作之外还要提供开始自动跳转、停止自动跳转,还有设置跳转的时间间隔等等。那么我们来定义操作接口如下:
这里提供的setInterval()、和setScrollDuration()有点容易引起混乱,setScrollDuration()就是我们上面提到的跳转时间间隔,setInterval()就是一个图片开始露头到完全显示的间隔。这个一般不需要设置,除非要求比较高,所有扩展出来方便配置。
六、 定义属性
我们来提供以下几个属性可以在xml布局中配置控件,因为提供了设置自定义布局所有,这里的设置指示点间隔以及设置指示点选择器只是针对默认的布局有用。
七、 书写控件
由于可能以后会提供其他形式的轮转图,这里把和界面操作相关的分离出来,底层的逻辑抽取为基类。
1. 构造函数初始化自定义属性
在BaseLoopView中初始化自定义属性
public BaseLoopView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// 设置默认属性
final float defaultDotMargin = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics());
final int defaultInterval = 3000;
// 设置样式属性
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LoopView);
mDotMargin = a.getDimension(R.styleable.LoopView_loop_dotMargin, defaultDotMargin);
mInterval = a.getInt(R.styleable.LoopView_loop_interval, defaultInterval);
autoLoop = a.getBoolean(R.styleable.LoopView_loop_autoLoop, false);
mDotSelector = a.getResourceId(R.styleable.LoopView_loop_dotSelector, R.drawable.loop_view_dots_selector);
mDefaultImgId = a.getResourceId(R.styleable.LoopView_loop_defaultImg, 0);
mLoopLayoutId = a.getResourceId(R.styleable.LoopView_loop_layout, 0);
a.recycle();
initRealView();
}
2. 初始化View
在AdLoopView中初始化View,AdLoopView即是我们要用的轮转广告大图控件。
@Override
@SuppressWarnings("deprecation")
protected void initRealView() {
View view = null;
if (mLoopLayoutId != 0) {
// If there is a custom loop view layout id set, try and inflate it
view = LayoutInflater.from(getContext()).inflate(mLoopLayoutId, null);
// ViewPager
mViewPager = (ViewPager) view.findViewById(R.id.loop_view_pager);
// 指示点父控件
dotsView = (LinearLayout) view.findViewById(R.id.loop_view_dots);
// 描述文字
descText = (TextView) view.findViewById(R.id.loop_view_desc);
}
if(view == null) {
view = createDefaultView();
}
setScrollDuration(1000); // 设置页面切换时间
this.addView(view);
}
在初始化View的时候如果设置了自定义的布局就使用自定义的布局,如果没有使用自定义布局那么调用createDefaultView()默认创建一个布局。
private View createDefaultView() {
RelativeLayout contentView = new RelativeLayout(getContext());
int viewWidth = ViewGroup.LayoutParams.MATCH_PARENT;
int viewHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
ViewGroup.LayoutParams viewParams = new ViewGroup.LayoutParams(viewWidth, viewHeight);
contentView.setLayoutParams(viewParams);
// 初始化ViewPager
mViewPager = new ViewPager(getContext());
mViewPager.setId(R.id.loop_view_pager);
int viewPagerWidth = LayoutParams.MATCH_PARENT;
int viewPagerHeight = LayoutParams.WRAP_CONTENT;
LayoutParams viewPagerParams = new LayoutParams(viewPagerWidth, viewPagerHeight);
this.addView(mViewPager, viewPagerParams);
// 初始化下方指示条
RelativeLayout bottomLayout = new RelativeLayout(getContext());
int bottomLayoutWidth = LayoutParams.MATCH_PARENT;
int bottomLayoutHeight = LayoutParams.WRAP_CONTENT;
LayoutParams bottomLayoutParams = new LayoutParams(bottomLayoutWidth, bottomLayoutHeight);
bottomLayoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, mViewPager.getId());
Drawable mBackground = new ColorDrawable(Color.DKGRAY);
mBackground.setAlpha((int) (0.3 * 255));
bottomLayout.setBackgroundDrawable(mBackground);
bottomLayout.setGravity(Gravity.CENTER_VERTICAL);
this.addView(bottomLayout, bottomLayoutParams);
// 初始化指示点父控件
dotsView = new LinearLayout(getContext());
dotsView.setId(R.id.loop_view_dots);
int dotsViewWidth = LayoutParams.WRAP_CONTENT;
int dotsViewHeight = LayoutParams.WRAP_CONTENT;
LayoutParams dotsViewParams = new LayoutParams(dotsViewWidth, dotsViewHeight);
dotsView.setOrientation(LinearLayout.HORIZONTAL);
dotsViewParams.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
dotsViewParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);
bottomLayout.addView(dotsView, dotsViewParams);
// 初始描述文字
descText = new TextView(getContext());
int descTextWidth = LayoutParams.MATCH_PARENT;
int descTextHeight = LayoutParams.WRAP_CONTENT;
LayoutParams descTextParams = new LayoutParams(descTextWidth, descTextHeight);
descTextParams.addRule(RelativeLayout.LEFT_OF, dotsView.getId());
descText.setSingleLine(true);
descText.getPaint().setTextSize((int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 14, getResources().getDisplayMetrics()));
descText.setTextColor(Color.WHITE);
descText.setGravity(Gravity.LEFT);
int padding = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics());
descText.setPadding(padding, padding, padding, padding);
bottomLayout.addView(descText, descTextParams);
return contentView;
}
这里就是用代码写的布局,看着一大坨,其实很简单。就是相对布局中有一个ViewPager以及指示文字等。
3. 初始化ViewPager
然后是初始化ViewPager,设置指示点,描述文字,以及设置点击监听。
private void initLoopViewPager() {
adapter = initAdapter();
adapter.setDefaultImgId(mDefaultImgId);
mViewPager.setAdapter(adapter);
initDots(mLoopData.items.size()); // 初始化指示点
if(null != descText) {
String descStr = mLoopData.items.get(0).descText;
if(!TextUtils.isEmpty(descStr)) {
descText.setText(descStr); // 初始化描述信息
}
}
setViewListener(); // 初始化点击监听事件
int startPosition = Integer.MAX_VALUE / 2 - Integer.MAX_VALUE / 2 % mLoopData.items.size();
mViewPager.setCurrentItem(startPosition, false); // 设置当前显示的位置
if (mHandler == null) {
mHandler = new LoopHandler(this, (Activity)getContext());
}
if (autoLoop) {
startAutoLoop();
}
}
4. 初始化ViewPager的Adapter
在Adapter中将初始化的跳转页面存储到软引用中,这样就解决了每调动一次就初始化页面对象的问题。
@Override
public Object instantiateItem(ViewGroup container, final int position) {
final View view;
final int index = position % mLoopData.items.size();
String imageUrl = mLoopData.items.get(index).imgUrl;
// 首先读取软引用,如果不存在则初始化该View
if (instantiateViewMap.containsKey(index)) {
SoftReference<View> reference = instantiateViewMap.remove(index);
if (null == reference || null == reference.get()) {
// ToastUtils.showShortToast(mContext, "被回收了...");
view = instantiateItemView(imageUrl, index);
} else {
view = reference.get();
// ToastUtils.showShortToast(mContext, "读取软引用...");
}
} else {
// ToastUtils.showShortToast(mContext, "初始化...");
view = instantiateItemView(imageUrl, index);
}
if(mOnItemClickListener != null) {
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) { // 条目点击监听回调
mOnItemClickListener.onItemClick(BaseLoopAdapter.this, view, index, position);
}
});
}
container.addView(view, 0);
return view;
}
5. SmartImageView使用
轮转图片ImageView使用的是在SmartImageView基础上完善而来的LoopImageView,他可以根据控件大小对图片进行压缩。
6. ViewPager监听回调中修改信息
在ViewPager的跳转监听回调中修改描述文字以及指示点的位置信息
@Override
protected void setOnPageChangeListener() {
// 数据适配器滑动监听
mViewPager.addOnPageChangeListener(new OnPageChangeListener() {
@Override
public void onPageSelected(int position) {
int i = position % mLoopData.items.size();
if(null != dotsView) {
dotsView.getChildAt(i).setEnabled(true);
}
if(null != dotsView && currentPosition != -1) {
dotsView.getChildAt(currentPosition).setEnabled(false);
}
currentPosition = i;
if(null != descText) {
if(!TextUtils.isEmpty(mLoopData.items.get(i).descText)) {
if(descText.getVisibility() != View.VISIBLE)
descText.setVisibility(View.VISIBLE);
String imageDesc = mLoopData.items.get(i).descText;
descText.setText(imageDesc);
} else {
if(descText.getVisibility() == View.VISIBLE)
descText.setVisibility(View.GONE);
}
}
// 跳转到头部尾部的监听回调
if(mOnLoopListener != null) {
if(i == 0) {
mOnLoopListener.onLoopToStart(position);
} else if(i == mLoopData.items.size() -1) {
mOnLoopListener.onLoopToEnd(position);
}
}
}
@Override
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels) {}
@Override
public void onPageScrollStateChanged(int state) {}
});
}
八、 性能优化
1. Handler可能引起Activity内存泄露问题
public class LoopHandler extends Handler {
private final WeakReference<Activity> mActivity;
private final WeakReference<BaseLoopView> mLoopView;
public LoopHandler(BaseLoopView loopView, Activity activity) {
this.mLoopView = new WeakReference<BaseLoopView>(loopView);
this.mActivity = new WeakReference<Activity>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 0:
Activity activity = mActivity.get();
BaseLoopView loopView = mLoopView.get();
if (activity != null && loopView != null) {
if (!loopView.isAutoScroll()) return;
int change = (loopView.getDirection() == BaseLoopView.LEFT) ? -1 : 1;
loopView.getViewPager().setCurrentItem(loopView.getViewPager().getCurrentItem() + change, true);
loopView.sendScrollMessage(loopView.getInterval());
} else {
removeMessages(0);
if(loopView != null) {
loopView.releaseResources();
}
}
default:
break;
}
}
}
2. 手指按下时候停止自动跳转
手指按下的时候我们希望它停止自动跳转,然后手指抬起的时候它继续跳转
/**
* stopScrollWhenTouch为TRUE时
* 按下操作停止轮转
* 抬起操作继续轮转
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if(stopScrollWhenTouch) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
if(isAutoScroll) {
stopAutoLoop();
isStoppedByTouch = true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if(isStoppedByTouch) {
startAutoLoop(mInterval);
isStoppedByTouch = false;
}
break;
}
}
return super.dispatchTouchEvent(ev);
}
3. 去除ViewPager竖向滑动事件
比如把轮转大图添加到ListView或者SrollView中的时候,我们希望向上滑动轮转控件,也可以整体向上滑动。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 当Y方向滑动距离大于X方向滑动距离时不获取滚动事件
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = ev.getX();
mDownY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
if(Math.abs(ev.getY() - mDownY) > Math.abs(ev.getX() - mDownX)) {
getParent().requestDisallowInterceptTouchEvent(false);
} else {
getParent().requestDisallowInterceptTouchEvent(true);
}
mDownX = ev.getX();
mDownY = ev.getY();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return super.onInterceptTouchEvent(ev);
}
4. 轮转控件不可见时停止跳转
在轮转控件不可见的时候就没必要让它继续自动跳转了,当可见的时候再继续跳转。
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
// 当不可见的时候停止自动跳转
switch (visibility) {
case VISIBLE:
if(isStoppedByInvisible) {
startAutoLoop(mInterval);
isStoppedByInvisible = false;
}
break;
case INVISIBLE:
case GONE:
if(isAutoScroll) {
stopAutoLoop();
isStoppedByInvisible = true;
}
break;
}
}
九、简单使用
1. 在xml添加控件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:kevin="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.kevin.loopview.AdLoopView
android:id="@+id/adloop_act_adloopview"
android:layout_width="match_parent"
android:layout_height="192dp"
kevin:loop_interval="5000"
kevin:loop_dotMargin="5dp"
kevin:loop_autoLoop="true">
</com.kevin.loopview.AdLoopView>
</RelativeLayout>
2. 代码中初始化
/**
* 初始化LoopView
*
* @return void
*/
private void initRotateView() {
mLoopView = (AdLoopView) this.findViewById(R.id.main_act_adloopview);
String json = LocalFileUtils.getStringFormAsset(this, "loopview_date.json");
LoopData loopData = JsonTool.toBean(json, LoopData.class);
if(null != loopData) {
mLoopView.refreshData(loopData);
mLoopView.startAutoLoop();
}
}
这样就可以欢快地自动跳转了,来看下json数据
{
"items": [
{
"id": "0",
"descText": "传递数据纠结吗?",
"imgUrl": "http://cms.csdnimg.cn/article/201305/21/519b0d442e3e7.jpg",
"link":"https://github.com/greenrobot/EventBus",
"type":"ad"
},
{
"id": "1",
"descText": "OOM了肿么办,不用怕!",
"imgUrl": "http://cms.csdnimg.cn/article/201305/03/51834d562e039_middle.jpg",
"link":"https://github.com/nostra13/Android-Universal-Image-Loader",
"type":"ad"
},
{
"id": "2",
"descText": "不想写sql语句唉",
"imgUrl": "http://www.strongcms.net/uploads/allimg/c120202/132Q962920TP-1W17.png",
"link":"http://ormlite.com/sqlite_java_android_orm.shtml",
"type":"ad"
}
]
}
十、源码及示例
十一、一行引入库
如果您的项目使用 Gradle 构建, 只需要在您的build.gradle
文件添加下面一行到 dependencies
:
compile 'com.kevin:loopview:1.0.6'