Android轮播图Banner的实现
从慕课网上学了一门叫做“不一样的自定义实现轮播图效果”的课程,感觉实用性较强,而且循序渐进,很适合初学者。在此对该课程做一个小小的笔记。
实现轮播思路:
1、一般轮播图是由一组图片和底部轮播圆点组成,要想组成这种圆点在图片之上的效果,首先我们应当想到FrameLayout布局。最外层应该是一个FrameLayout布局,将轮播图片和圆点添加到这个布局中,并且需要设置圆点的位置在下部正中间(当然视需求而定)。
2、轮播图片组应该是一个ViewGroup,我们需要对该ViewGroup进行测量、布局等过程。
3、圆点集合应该是一个LinearLayout。
4、要想实现自动轮播效果,可以结合Timer、TimerTask以及Handler的配合使用。
5、要想实现手动滑动轮播图,可以利用Scroller。
6、实现底部圆点随图片切换变化的过程,实现每张图片的点击事件。
首先我们来实现图片组ViewGroup的绘制过程。实现思路很简单,对于整个ViewGroup,其高度应该是子View的高度(假设我们的轮播图效果类似于淘宝首页的头部效果),宽度应该是所有子View的宽度之和。抓住这个方向,不难实现测量过程。
#ImaBannerViewGroup @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); child = getChildCount(); if (0 == child) { setMeasuredDimension(0, 0); } else { measureChildren(widthMeasureSpec, heightMeasureSpec); View view = getChildAt(0); childHeight = view.getMeasuredHeight(); childWidth = view.getMeasuredWidth(); int height = childHeight; int width = childWidth * child; setMeasuredDimension(width, height); } }
从上面代码可以看出,我们假定每个子View的宽度都和第一个子View的宽度相等,当后面的图片宽度小于第一张图时就会出现留白现象。一般轮播图每个子View的宽度都是整个屏幕的宽度,我们可以写一个全局的静态变量,然后给该变量赋值为屏幕宽度,在需要的时候进行调用就好。
然后重写onLayout()方法。该方法遍历子视图,设置每个子视图的位置,即在ViewGroup中水平依次平铺。该例子实际上是不用考虑y方向的,x方向后子视图应该是在前一个子视图的位置加上前一个子视图的宽度。
protected void onLayout(boolean change, int left, int top, int right, int bottom) { if (change) {//当Viewgroup发生改变时为true int leftMargin = 0; for (int i = 0; i < child; i++) { View view = getChildAt(i); view.layout(leftMargin, 0, leftMargin + childWidth, childHeight); leftMargin += childWidth; } } }
接下来就是实现手动轮播了。可以使用ScrollTo,ScrollBy,也可以使用Scroller。既然需要实现手动轮播,自然那涉及到分发过程,也就是需要重写onInterceptTouchEvent()返回true,并重写onTouchEvent()方法。
/** * 1)我们在滑动屏幕图片的过程中,其实就是我们自定义ViewGroup的子视图的移动过程,那么,我们只需要知道滑动之前横坐标和滑动之后的横坐标,此时,我们就可以 * 求出此次过程中我们滑动的距离,我们利用scrollBy方法实现图片的滑动,所以,此时我们需要两个值,移动之前和移动之后的横坐标。 * 2)在我们第一次按下的那一瞬,此时的移动之前和移动之后的距离是相等的,也就是我们此时按下那一瞬的那个点的横坐标的值 * 3)不断滑动过程会不断调用ACTION_MOVE,所以需要保存移动 值。 * 4)当我们抬起那一瞬,需要知道具体滑到哪一页 * * @param event * @return 返回true的目的是告诉我们该ViewGroup容器的父View我们已经处理好了该事件 */ @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: isClick = true; if (!scroller.isFinished()) { scroller.abortAnimation(); } x = (int) event.getX(); stopAuto(); break; case MotionEvent.ACTION_MOVE: int moveX = (int) event.getX(); int distance = moveX - x; if (Math.abs(distance) > filter){ isClick = false; } scrollBy(-distance, 0); x = moveX; break; case MotionEvent.ACTION_UP: startAuto(); int scrollX = getScrollX(); index = (scrollX + childWidth / 2) / childWidth; if (index < 0) { index = 0; } else if (index > child - 1) { index = child - 1; } int dx = index * childWidth - scrollX; if (isClick) { listener.clickImageIndex(index); } else { /** * public void startScroll(int startX, int startY, int dx, int dy) * startX:水平方向滚动的偏移值,以像素为单位,。正值标明向左滚动 * startY:垂直方向滚动的偏移值,正值标明向上滚动 * dx:水平方向滑动的距离,正值向左滚动 * dy:垂直方向滑动的距离,正值向上滚动。 * * startX 表示起点在水平方向到原点的距离(可以理解为X轴坐标,但与X轴相反),正值表示在原点左边,负值表示在原点右边。 dx 表示滑动的距离,正值向左滑,负值向右滑。 */ scroller.startScroll(scrollX, 0, dx, 0); /** * postInvalidate() 方法在非 UI 线程中调用,通知 UI 线程重绘,(当然也可以在UI线程中调用)。 * invalidate() 方法在 UI 线程中调用,重绘当前 UI。 */ postInvalidate(); changeLisener.selectImage(index); } break; default: break; } return true; }
利用Timer、TimerTask、Handler实现自动轮播。
1. 需要两个方法来控制自动轮播的启动和关闭,我们称之为自动轮播的开关,分别为
startAuto(),stopAuto();还需要一个标志来表明当前自动轮播的状态时开启还是关闭,设为布尔类
型isAuto,true表示自动轮播启动,false表示自动轮播关闭。
2. 在ImaBannerViewGroup 的构造方法中,设置一个定时任务,如果自动轮播处于开启状态,则利用
Handler每间隔一段时间发送一个空的消息,而在Handler接受消息后利用scrollTo()方法实现图片的
轮播,相关代码如下:
private void intiObj() { scroller = new Scroller(getContext()); task = new TimerTask() { @Override public void run() { if (isAuto) { autoHander.sendEmptyMessage(0); } } }; timer.schedule(task, 100, 3000); } private Handler autoHander = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case 0: if (++index >= child) { index = 0; } scrollTo(childWidth * index, 0); changeLisener.selectImage(index); break; default: break; } } };
在点击发生时,关闭自动轮播,抬起手后需要开启自动轮播,实现过程只需要在onTouchEvent()方法中,
当MotionEvent.ACTION_DOWN时,调用stopAuto();当MotionEvent.ACTION_UP时,调用
startAuto()即可。同时,利用一个布尔型变量isClick判断点击事件,当用户离开屏幕的一瞬间,来判断是点击事件还是移动事件。当按下即触发ACTION_DOWN时,设置为true,ACTION_MOVE时,若移动距离大于最小移动距离,则设置为false,当ACTION_UP的时候,根据isClick值判断是移动还是点击,进行相应的操作即可。
最后实现底部轮播圆点的布局以及切换的过程,首先我们需要自定义一个FrameLayout布局,代码如下:
public class ImageBannerFrameLayout extends FrameLayout implements DotChangeLisener, ImageBannerListener { private ImaBannerViewGroup imaBannerViewGroup; private LinearLayout linearLayout; private FrameLayoutListenenr layoutListenenr; public FrameLayoutListenenr getLayoutListenenr() { return layoutListenenr; } public void setLayoutListenenr(FrameLayoutListenenr layoutListenenr) { this.layoutListenenr = layoutListenenr; } public ImageBannerFrameLayout(Context context) { super(context); initBannerViewGroup(); initDotLayout(); } public ImageBannerFrameLayout(Context context, AttributeSet attrs) { super(context, attrs); initBannerViewGroup(); initDotLayout(); } public ImageBannerFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initBannerViewGroup(); initDotLayout(); } private void initBannerViewGroup() { imaBannerViewGroup = new ImaBannerViewGroup(getContext()); LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); imaBannerViewGroup.setLayoutParams(lp); imaBannerViewGroup.setChangeLisener(this); imaBannerViewGroup.setListener(this); addView(imaBannerViewGroup); } private void initDotLayout() { linearLayout = new LinearLayout(getContext()); LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 40); linearLayout.setLayoutParams(lp); linearLayout.setOrientation(LinearLayout.HORIZONTAL); linearLayout.setGravity(Gravity.CENTER); linearLayout.setBackgroundColor(Color.RED); addView(linearLayout); LayoutParams layoutParams = (LayoutParams) linearLayout.getLayoutParams(); layoutParams.gravity = Gravity.BOTTOM; linearLayout.setLayoutParams(layoutParams); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { linearLayout.setAlpha(0.5f); } else { linearLayout.getBackground().setAlpha(100); } } public void addBitmap(List<Bitmap> list) { for (int i = 0; i < list.size(); i++) { Bitmap bitmap = list.get(i); addBitmapToBannerViewGroup(bitmap); addDotToLayout(); } } private void addDotToLayout() { ImageView imageView = new ImageView(getContext()); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); lp.setMargins(5, 5, 5, 5); imageView.setLayoutParams(lp); imageView.setImageResource(R.drawable.doc_normal); linearLayout.addView(imageView); } private void addBitmapToBannerViewGroup(Bitmap bitmap) { ImageView imageView = new ImageView(getContext()); imageView.setScaleType(ImageView.ScaleType.FIT_XY); imageView.setLayoutParams(new ViewGroup.LayoutParams(Constant.WIDTH, ViewGroup.LayoutParams.WRAP_CONTENT)); imageView.setImageBitmap(bitmap); imaBannerViewGroup.addView(imageView); } @Override public void selectImage(int index) { int count = linearLayout.getChildCount(); for (int i = 0; i < count; i++) { ImageView imageView = (ImageView) linearLayout.getChildAt(i); if (i == index) { imageView.setImageResource(R.drawable.doc_normal); } else { imageView.setImageResource(R.drawable.doc_unnormal); } } } @Override public void clickImageIndex(int pos) { layoutListenenr.clickImageByIndex(pos); } }
关于自定的FrameLayout布局,我们需要先加载图片的ViewGroup,然后加载点集的LinearLayout;需要对图片进行监听,添加接口进行相应的处理;提供addBitmap方法,以便调用者设置轮播图片,并每张图片和每个圆点一一映射。
MainActivity中的调用方法如下:
public class MainActivity extends AppCompatActivity implements ImageBannerListener, FrameLayoutListenenr, DotChangeLisener { private ImageBannerViewGroup group; private ImaBannerViewGroup viewGroup; private ImageBannerFrameLayout frameLayout; private int[] ids = new int[]{R.mipmap.top, R.mipmap.second, R.mipmap.qq}; private int[] idss = new int[]{R.mipmap.top, R.mipmap.second, R.mipmap.qq}; private int[] idds = new int[]{R.mipmap.top, R.mipmap.second, R.mipmap.qq}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); DisplayMetrics dm = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(dm); Constant.WIDTH = dm.widthPixels; group = findViewById(R.id.group); viewGroup = findViewById(R.id.scrollViewGroup); frameLayout = findViewById(R.id.contentGroup); List<Bitmap> list = new ArrayList<>(); for (int i = 0; i < idds.length; i++){ Bitmap bitmap = BitmapFactory.decodeResource(getResources(), idds[i]); list.add(bitmap); } frameLayout.addBitmap(list); for (int i = 0; i < ids.length; i++) { ImageView imageView = new ImageView(this); /** * 解决空白的bug */ imageView.setScaleType(ImageView.ScaleType.FIT_XY); imageView.setLayoutParams(new ViewGroup.LayoutParams(Constant.WIDTH, ViewGroup.LayoutParams.WRAP_CONTENT)); imageView.setImageResource(ids[i]); group.addView(imageView); } for (int i = 0; i < idss.length; i++) { ImageView imageView = new ImageView(this); imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); imageView.setLayoutParams(new ViewGroup.LayoutParams(Constant.WIDTH, ViewGroup.LayoutParams.WRAP_CONTENT)); imageView.setImageResource(idss[i]); viewGroup.addView(imageView); } viewGroup.setListener(this); viewGroup.setChangeLisener(this); frameLayout.setLayoutListenenr(this); } @Override public void clickImageIndex(int pos) { Toast.makeText(this, "pos = " + pos, Toast.LENGTH_LONG).show(); } @Override public void clickImageByIndex(int pos) { Toast.makeText(this, "pos = " + pos, Toast.LENGTH_LONG).show(); } @Override public void selectImage(int index) { } }
因为我实现的是个循序渐进的过程,所有有多余代码,大家可以仅参考FrameLayout部分。
代码补充:
//分页符的监听 public interface DotChangeLisener { void selectImage(int index); } //FrameLayout上面点击每张图片的监听 public interface FrameLayoutListenenr { void clickImageByIndex(int pos); } //点击每张图片的监听 public interface ImageBannerListener { void clickImageIndex(int pos); } //定义一个全局变量,即每张图片的宽度 public class Constant { public static int WIDTH = 0; }
上述代码基本就可以实现轮播效果了。