相信大家对轮播插件并不陌生,它频繁地出现在网页端,移动端的广告推广banner中,宣传效果和视觉非常不错,于是我又稍微思考了一下它实现的原理,恩,又完成了一个自制的插件,效果还行,就是差一点:无限轮播!不管怎样,通过脑力劳动产生的硕果总是让人感到分外喜悦。
1.实际效果
网易云音乐APP广告轮播:
自制的轮播:
2.原理机制
这是一个自定义ViewPager(继承自RelativeLayout),子View为ViewPage和Indicator(继承自RelativeLayout)。通过Indicator, 动态添加其子View:Dot(自定义View,继承自ImageView),作为圆点指示器。通过子View :ViewPager的OnPageChangeListener监听其滑动状态, 进而动态绘制圆点指示器,达到指示效果;通过添加线程,隔一段时间跳转ViewPager到下一页面达到播放效果。
实际上实现起来并不难,可以媲美那些流行的轮播框架了。
3.代码解析
完成这个小插件重点在于圆点指示器的重绘工作,在自定义View:Dot中,有两种显示状态(选中与非选中),即当ViewPager播放到指定页时,圆点指示器的指定圆点处于选中状态(这时要对其进行重绘了),当离开此页时,圆点变为非选中状态(又要进行重绘)。下面来看源码:
package chen.capton.custom;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.widget.ImageView;
import static android.content.ContentValues.TAG;
/**
* Created by CAPTON on 2017/1/15.
*/
public class Dot extends ImageView {
private int dotColor= Color.WHITE; //默认颜色,白色
private final int RADIUS_SMALL=6;
private final int RADIUS_NORMAL=8;
private final int RADIUS_LARGE=12;
private int diameter =RADIUS_NORMAL;//默认尺寸,正常
private Paint paint;
private int width,height;
public Dot(Context context) {
super(context);
}
public Dot(Context context, AttributeSet attrs) {
super(context, attrs);
/* 测试的时候在xml文件中实例化时调用此方法
width= (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,this.diameter,getResources().getDisplayMetrics());
height=(int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,this.diameter,getResources().getDisplayMetrics());
paint=new Paint();
paint.setColor(dotColor);
paint.setAntiAlias(true);
*/
}
/*
* 被Indicator创建对象时调用,获取指定的直径大小和颜色
* */
public Dot(Context context, int dotColor, int diameter ) {
super(context);
this.dotColor=dotColor;
this.diameter =diameter;
/*
* 将dp转为px,获取宽高
* */
width= (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,this.diameter,getResources().getDisplayMetrics());
height=(int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,this.diameter,getResources().getDisplayMetrics());
/*
* 设置画笔颜色,抗锯齿
* */
paint=new Paint();
paint.setColor(this.dotColor);
paint.setAntiAlias(true);
}
/*
* 设置选择与非选中的关键,改变其透明度,=1:选中,<1:非选中
* */
public void setAlpha(float alpha){
int tureAlpha= (int) (255*alpha);
paint.setAlpha(tureAlpha);
invalidate(); //使画面失效,系统调用onDraw方法
}
/*
* 用户通过Java代码改变圆点颜色的方法
* */
public void setColor(int color){
paint.setColor(color);
invalidate();
}
/*
* 用户通过Java代码改变圆点尺寸的方法
* */
public void setSize(int size){
switch (size){
case RADIUS_SMALL:
case RADIUS_NORMAL:
case RADIUS_LARGE:
diameter=size;
width= (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,diameter,getResources().getDisplayMetrics());
height=(int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,diameter,getResources().getDisplayMetrics());
measure(width,height);
invalidate();
break;
}
}
/*
* 关键,重绘方法
* */
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
canvas.drawCircle(width/2,height/2,width/2,paint);
canvas.restore();
}
/*
* 测量自身宽高
* */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize=MeasureSpec.getSize(widthMeasureSpec);
int heightSize=MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width,height);
}
是不是很简单?呵呵,主要是我还没有贴出逻辑代码(在AutoPlayViewpager这个类中),这里讲轮播逻辑已经没有意义了,实际业务中不可能思维定式,这里贴出的实现思路是自我提升的最好选择。
接下来看看Indicator类,作为Dot(ImageView)的父布局,主要起到包裹Dot,确定Dot位置的作用
package chen.capton.custom;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.widget.RelativeLayout;
import java.util.ArrayList;
import java.util.List;
/**
* Created by CAPTON on 2017/1/15.
*/
public class Indicator extends RelativeLayout {
private int dotCount=3; //测试用的数据
private int dotColor= Color.WHITE; //默认颜色
private final int GAP_SMALL=2;
private final int GAP_NORMAL=3;
private final int GAP_LARGE=4;
private int dotGap= GAP_NORMAL; //默认圆点间距
private final int DOT_SMALL=6;
private final int DOT_NORMAL=8;
private final int DOT_LARGE=12;
private int diameter=DOT_NORMAL; //默认尺寸(直径)
private List<Dot> dotList=new ArrayList<>(); //保存圆点对象的集合
/*
* 通过java代码改变圆点颜色的方法
* */
public void setDotColor(int dotColor){
this.dotColor=dotColor;
for (int i = 0; i <dotList.size(); i++) {
dotList.get(i).setColor(dotColor);
dotList.get(i).setAlpha(0.4f);
}
dotList.get(0).setAlpha(1f);
}
/*
* 通过java代码改变圆点尺寸的方法
* */
public void setDotSize(int dotSize){
//这里我只提供三种尺寸,如果用户输入其他尺寸,则使用默认尺寸
if(dotSize==DOT_SMALL||dotSize==DOT_NORMAL||dotSize==DOT_LARGE) {
this.diameter =dotSize;
for (int i = 0; i <dotList.size(); i++) {
dotList.get(i).setSize(dotSize);
}
switch (dotSize){
case DOT_SMALL:dotGap=GAP_SMALL;break;
case DOT_NORMAL:dotGap=GAP_NORMAL;break;
case DOT_LARGE:dotGap=GAP_LARGE;break;
}
}else {
Log.e("setDotSize", " Wrong setting!Please input a correct dotSize:'Indicator." +
"DOT_SMALL','Indicator.DOT_NORMAL'or'Indicator.DOT_NORMAL'");
}
}
/*
* 通过AutoPlayViewpager逻辑判断后调用此方法,以重绘Dot,达到指示的显示效果
* */
public void setDotChecked(int position){
for (int i = 0; i <dotList.size(); i++) {
dotList.get(i).setAlpha(0.4f);
}
dotList.get(position).setAlpha(1f);
}
public Indicator(Context context, AttributeSet attrs) {
super(context, attrs);
/* 测试时调用的方法(同下),弃用
for (int i = 0; i < dotCount; i++) {
Dot dot = new Dot(context, dotColor, diameter);
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(diameter, diameter);
dot.setLayoutParams(lp);
dotList.add(dot);
addView(dot);
}*/
}
/*
* 构建对象时,初添加指定大小,颜色的圆点到自身布局内
* */
public Indicator(Context context, int dotCount,int dotColor) {
super(context);
this.dotColor=dotColor;
this.dotCount=dotCount;
for (int i = 0; i < this.dotCount; i++) {
Dot dot = new Dot(context,this.dotColor, diameter);
//圆点(ImageView)宽高设置为上一级传来的直径
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(diameter, diameter);
dot.setLayoutParams(lp);
dotList.add(dot);
addView(dot);
}
setDotColor(this.dotColor);
}
/*
* 根据上一级传来的圆点的直径,圆点(页面)数量确定自身宽高
* */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width=(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,diameter*dotCount+2*dotGap*dotCount,getResources().getDisplayMetrics());
int height=(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,diameter,getResources().getDisplayMetrics());
setMeasuredDimension(width,height);
}
/*
* 确定所有圆点的显示位置的方法
* */
private void setChildLayout(){
for (int i = 0; i < dotList.size(); i++) {
int left = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dotGap + (diameter + dotGap * 2) * i, getResources().getDisplayMetrics());
int right = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dotGap + (diameter + dotGap * 2) * i + diameter, getResources().getDisplayMetrics());
int top = 0;
int bottom = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, diameter, getResources().getDisplayMetrics());
dotList.get(i).layout(left, top, right, bottom);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
setChildLayout(); //调用上文确定圆点位置的方法
}
}
是不是感觉没头绪?那是因为兄台还不太了解自定义ViewGroup和自定义View的用法,建议去看看鸿洋大神的关于自定义ViewGroup与自定义View的文章。
接下来提出AutoPlayViewpager的代码,关于这部分,我不想多说了,大家能看懂多少是多少,因为我的水平也是有限。大家主要能理解,核心思路就行了。
package chen.capton.custom;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.RelativeLayout;
/**
* Created by CAPTON on 2017/1/15.
*/
public class AutoPlayViewpager extends RelativeLayout implements ViewPager.OnPageChangeListener{
private Context context;
public static final String INDICATOR_LOCATION_LEFT="left";
public static final String INDICATOR_LOCATION_CENTER="center";
public static final String INDICATOR_LOCATION_RIGHT="right";
public static final int DOT_SMALL=6;
public static final int DOT_NORMAL=8;
public static final int DOT_LARGE=12;
private String indicatorLocation=INDICATOR_LOCATION_CENTER;
private int dotSize=DOT_NORMAL;
private int interval=3000;
private ViewPager mViewPager;
private Indicator mIndicator;
private int dotColor= Color.WHITE;
private Handler handler;
public AutoPlayViewpager(Context context) {this(context,null);}
public AutoPlayViewpager(Context context, AttributeSet attrs) {this(context, attrs,0);}
public AutoPlayViewpager(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context=context;
handler=new Handler(){
public void handleMessage(Message msg){
if(msg.what==mViewPager.getAdapter().getCount()-1){
mViewPager.setCurrentItem(0,false);
}else {
mViewPager.setCurrentItem(msg.what + 1);
}
}
};
obtainAttrs(attrs);
mViewPager=new ViewPager(context);
}
/*
* 获取xml中各种属性
* */
private void obtainAttrs(AttributeSet attrs){
TypedArray ta=context.obtainStyledAttributes(attrs,R.styleable.AutoPlayViewpager);
if(ta.getString(R.styleable.AutoPlayViewpager_incator_location)!=null) {
if(ta.getString(R.styleable.AutoPlayViewpager_incator_location).equals(INDICATOR_LOCATION_LEFT)
||ta.getString(R.styleable.AutoPlayViewpager_incator_location).equals(INDICATOR_LOCATION_CENTER)
||ta.getString(R.styleable.AutoPlayViewpager_incator_location).equals(INDICATOR_LOCATION_RIGHT)) {
indicatorLocation = ta.getString(R.styleable.AutoPlayViewpager_incator_location);
}else {
indicatorLocation=INDICATOR_LOCATION_CENTER;
Log.w("setIndicatorLocation", " Wrong setting!Please input a correct indicatorLocation:'AutoPlayViewpager." +
"INDICATOR_LOCATION_LEFT','AutoPlayViewpager.INDICATOR_LOCATION_CENTER'or'AutoPlayViewpager.INDICATOR_LOCATION_RIGHT'");
}
}
dotSize=ta.getInt(R.styleable.AutoPlayViewpager_dot_size,dotSize);
dotColor=ta.getColor(R.styleable.AutoPlayViewpager_dot_color,dotColor);
interval=ta.getInt(R.styleable.AutoPlayViewpager_interval,interval);
ta.recycle();
}
boolean once;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize=MeasureSpec.getSize(widthMeasureSpec);
int heightSize=MeasureSpec.getSize(heightMeasureSpec);
/*
* 设置内部ViewPager的大小与自身一致,添加内部ViewPager与Indicator指示器到自身布局内,测量自身宽高
* */
if(!once) {
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(widthSize, heightSize);
lp.width=widthSize;
lp.height=heightSize;
mViewPager.setLayoutParams(lp);
addView(mViewPager);
addView(mIndicator);
setMeasuredDimension(widthSize, heightSize);
once=true;
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
setIndicatorLayout();
}
/*
* 设置Indicatord位置,私有方法
* */
private void setIndicatorLayout(){
int indicatorWidth=mIndicator.getWidth();
int indicatorHeight=mIndicator.getHeight();
int left,top,right,bottom;
switch (indicatorLocation){
case INDICATOR_LOCATION_CENTER:
left=(getMeasuredWidth()-indicatorWidth)/2;
top=getMeasuredHeight()-indicatorHeight*2;
right= (getMeasuredWidth()+indicatorWidth)/2;
bottom=getMeasuredHeight()-indicatorHeight;
mIndicator.layout(left,top,right,bottom);
break;
case INDICATOR_LOCATION_LEFT:
left=indicatorWidth/2;
top=getMeasuredHeight()-indicatorHeight*2;
right=indicatorWidth*3/2;
bottom=getMeasuredHeight()-indicatorHeight;
mIndicator.layout(left,top,right,bottom);
break;
case INDICATOR_LOCATION_RIGHT:
left=getMeasuredWidth()-indicatorWidth*3/2;
top=getMeasuredHeight()-indicatorHeight*2;
right= getMeasuredWidth()-indicatorWidth/2;
bottom=getMeasuredHeight()-indicatorHeight;
mIndicator.layout(left,top,right,bottom);
break;
}
}
/*
* 设置适配器,初始化指示器,开启轮播线程
* */
public void setAdapter(PagerAdapter adapter){
mViewPager.setAdapter(adapter);
mViewPager.setOnPageChangeListener(this);
mIndicator=new Indicator(context,mViewPager.getAdapter().getCount(),dotColor);
mIndicator.setDotSize(dotSize);
setIndicatorLocation(indicatorLocation);
new AutoPlayThread(interval).start();
}
/*
* 用户通过Java代码改变指示器位置的方法(与私有方法setIndicatorLocation()要区分开来)
* */
public AutoPlayViewpager setIndicatorLocation(String location){
if(location!=null) {
if (location.equals(INDICATOR_LOCATION_LEFT)
|| location.equals(INDICATOR_LOCATION_CENTER)
|| location.equals(INDICATOR_LOCATION_RIGHT)) {
indicatorLocation = location;
} else {
indicatorLocation = INDICATOR_LOCATION_CENTER;
Log.e("setIndicatorLocation", " Wrong setting!Please input a correct indicatorLocation:'AutoPlayViewpager." +
"INDICATOR_LOCATION_LEFT','AutoPlayViewpager.INDICATOR_LOCATION_CENTER'or'AutoPlayViewpager.INDICATOR_LOCATION_RIGHT'");
}
setIndicatorLayout();
}else {
Log.e("setIndicatorLocation", " Wrong setting!Please input a correct indicatorLocation:'AutoPlayViewpager." +
"INDICATOR_LOCATION_LEFT','AutoPlayViewpager.INDICATOR_LOCATION_CENTER'or'AutoPlayViewpager.INDICATOR_LOCATION_RIGHT'");
}
return this;
}
//设置圆点尺寸(直径)
public AutoPlayViewpager setDotSize(int dotSize) {
mIndicator.setDotSize(dotSize);
return this;
}
//设置圆点颜色
public AutoPlayViewpager setDotColor(int color) {
mIndicator.setDotColor(color);
return this;
}
//设置播放时间间隔
public AutoPlayViewpager setInterval(int interval){
this.interval=interval;
return this;
}
@Override
public void onPageSelected(int position) {
mIndicator.setDotChecked(position);
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageScrollStateChanged(int state) {
/*
* 当页面滑动时,根据ViewPager的滑动状态控制轮播的播放状态。
* 这个状态可以自己测试来得到结果,state 一共 1,2,0 三个状态
* 分别对应:滑动中,滑动快要结束时(手指滑动屏幕后,离开屏幕时),滑动结束
* */
switch (state){
//滑动结束
case 0:
if(!autoPlaying){
autoPlaying = true;
}
break;
//滑动中(手指与屏幕接触中)
case 1:
if(autoPlaying) {
autoPlaying = false;
}
break;
}
}
/*
* 设置轮播线程,通过autoPlaying标志控制轮播的暂停与播放
* */
boolean autoPlaying=true;
int current;
class AutoPlayThread extends Thread{
private long interval;
public AutoPlayThread(long interval) {
this.interval=interval;
}
public void run(){
while (true) {
try {
Thread.sleep(interval);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(autoPlaying) {
current = mViewPager.getCurrentItem();
handler.sendEmptyMessage(current);
}
}
}
}
/*
* 下面是一大堆对ViewPager公共方法的引用。
* */
public int getPageMargin() {
return mViewPager.getPageMargin();
}
public int getOffscreenPageLimit() {
return mViewPager.getOffscreenPageLimit();
}
public PagerAdapter getAdapter() {
return mViewPager.getAdapter();
}
public int getCurrentItem() {
return mViewPager.getCurrentItem();
}
public int getchildCount() {
return mViewPager.getChildCount();
}
public void setPageMarginDrawable(Drawable drawable) {
mViewPager.setPageMarginDrawable(drawable);
}
public void setPageMarginDrawable(int resId) {
mViewPager.setPageMarginDrawable(resId);
}
public void setPageMargin(int margin) {
mViewPager.setPageMargin(margin);
}
public void setCurrentItem(int item) {
mViewPager.setCurrentItem(item);
}
public ViewPager getViewPager(){
return mViewPager;
}
}
哈哈,一脸懵逼?很简单的一个自定义插件,也花费了我不少时间。通过对自定义ViewGroup的深入,相信大家都可以做出自己想要的效果。
4.相关文件
关于怎么使用我的插件,请参考我的项目