先看效果图:
本篇我们分为三部分来讲:
- 图片的滑动实现
- 动态改变缩放值和透明度
- 实现右下角滚轮滑动
- 补充
一、图片的滑动实现
在上图中,一个屏幕里同时可以存在三张图片,这意味着每次滑动实际只滑动了1/3的屏幕宽度。当然,我们也可以只让屏幕显示一张图片,或是同时显示5张等等。我们应该设立一个变量onPagerNum来表示当前屏幕显示的图片数量
在本文中我们令onPagerNum=3,即一个屏幕出现三张图片。下面我们要计算每个图片应分配的最大宽度,先来看下图:
从图中就能很明显的得出结论,每张图片的最大宽度viewWidth=屏幕宽度/onPagerNum-padding*2
我们来添加ImageView,并重写onLayout:
private ImageView[] images;
//获取外界传来的图片
public void setImages(int[] imgs){
this.images=new ImageView[imgs.length];
if(onPagerNum>1)setBlankView();
for(int i=0;i<images.length;i++){
images[i]=new ImageView(getContext());
images[i].setImageResource(imgs[i]);
this.addView(images[i]);
}
setImgsTypes(1f,nowPosition+1);
//......
}
//设置空白的View
private void setBlankView(){
View view=new View(getContext());
this.addView(view);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount=getChildCount();
for(int i=0;i<childCount;i+=onPagerNum){
for(int j=0;j<onPagerNum;j++){
if(i+j>=childCount)break;
View childView=getChildAt(i+j);
//layout:相对于父布局的位置
childView.layout(i/onPagerNum*getWidth()+j*getWidth()/onPagerNum+paddingPic ,0 ,i/onPagerNum*getWidth()+(j+1)*getWidth()/onPagerNum-paddingPic ,getHeight());
}
}
}
我们把六张图片前后分成两组
i/onPagerNum*getWidth():表示当前在哪一组
j*getWidth()/onPagerNum:表示在一组中的哪一个
为什么要添加空白的View:当我们移动到第一张图片时,第一张图片在中间,左边是没有图片的,所以要用空白View代替
接下来,我们要重写onTouchEvent,并调用GestureDetector.onTouchEvent()来实现页面滑动
@Override
public boolean onTouchEvent(MotionEvent event) {
//将触摸事件传递手势识别器
gestureDetector.onTouchEvent(event);
return true;
}
private GestureDetector gestureDetector=new GestureDetector(new GestureDetector.SimpleOnGestureListener(){
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
//X方向滑动,view跟着滑动
scrollBy((int)distanceX/onPagerNum,0);
return super.onScroll(e1, e2, distanceX, distanceY);
}
});
注意scrollBy((int)distanceX/onPagerNum,0)这个方法中的第一个参数,由于我们默认是滑动一整个屏幕的,但现在只需要滑动1/onPagerNum个屏幕,所以要除以一个onPagerNum
当前实现的效果如下:
然后我们来实现滑动一定距离松手后,自动滑动到下一张图片的功能
因为我们现在一次只滑动1/3个屏幕,所以我们立一个规定:
我们如果一次性滑动的距离超过了1/3屏幕的1/3,那么我们松手后,就会自动滑动到前一张,或者后一张,否则返回到当前图片
代码实现如下:
private ImageView[] images;
private Scroller mScroller; //用于平滑过渡
private int nowPosition=0;//当前显示哪一张图片
@Override
public boolean onTouchEvent(MotionEvent event) {
if(images==null)return false;
//将触摸事件传递手势识别器
gestureDetector.onTouchEvent(event);
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastPosX=event.getX();
break;
case MotionEvent.ACTION_MOVE:
isMoving=true;
if(lastPosX<event.getX()){ //手指向右滑
nowPosition=(getScrollX()+getWidth()/(3*onPagerNum))/(getWidth()/onPagerNum);
}else{ //手指向左滑
nowPosition=(getScrollX()+2*getWidth()/(3*onPagerNum))/(getWidth()/onPagerNum);
}
if(nowPosition>=images.length)nowPosition=images.length-1;
if(nowPosition<0)nowPosition=0;
break;
case MotionEvent.ACTION_UP:
isMoving=false;
move();
break;
}
return true;
}
//移动页面
private void move(){
mScroller.startScroll(getScrollX(),0,nowPosition*getWidth()/onPagerNum-getScrollX(),0);
invalidate(); //使用invalidate会执行回调方法computeScroll
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), 0);
postInvalidateDelayed(10);
}
}
mScroller.startScroll():我们松手后,不希望一下子跳到下一张图片,而是有一个平滑过渡的移动,就需要用到Scroller类
当前实现的效果如下:
二、动态改变缩放值和透明度
在第一部分中,我们实现了图片的滑动,但是显然只有单纯的滑动,整体效果并不柔和与美观。所以本部分,我们来加上一些图片的动画效果
依照我们的惯例,当我们在滑动到下一张的过程中,前一张的图片应该逐渐变小,变得透明,而后一张的图片逐渐变大,变得不透明。当我们松手后,下一张图片完全显示,上一张图片缩放到最小比例
假设默认情况下,显示在中间的图片透明度为无透明,缩放比例为1;它左右两边的图片透明度为1~255中的100,缩放比例为0.5
我们需要一个变量来实时改变透明度和缩放比例。来设想一下:手指按下去的时候,会产生一个坐标点,将它的x坐标记为lastPosX;手指向左移动,每时每刻都会产生移动后的位置坐标,即event.getX();我们用event.getX()/lastPosX会得到一个比例值,这个比例值就可以充当我们动态改变图片属性值的变量。(手指向右滑动就是lastPosX/event.getX())
代码表示如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
//将触摸事件传递手势识别器
gestureDetector.onTouchEvent(event);
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastPosX=event.getX();
break;
case MotionEvent.ACTION_MOVE:
//......
if(lastPosX!=0&&event.getX()!=0){
if(lastPosX>event.getX())setImgsTypes(event.getX()/lastPosX,pastPosition+1);
else setImgsTypes(lastPosX/event.getX(),pastPosition-1);
}
break;
case MotionEvent.ACTION_UP:
move();
break;
}
return true;
}
//移动页面
private void move(){
//如果上一次位置比当前位置小,说明是移动到了后一张图片
if(pastPosition<nowPosition){
pastPosition=nowPosition;
setImgsTypes(1f,nowPosition+1);
//如果上一次位置比当前位置大,说明是移动到了前一张图片
}else{
pastPosition=nowPosition;
setImgsTypes(1f,nowPosition-1);
}
//......
}
//获取外界传来的图片
public void setImages(int[] imgs){
//......
setImgsTypes(1f,nowPosition+1);
}
//设置图片属性
/*
* changePer:动态改变图片属性的比例
* nextPos:下一张图片的下标
* */
private void setImgsTypes(float changePer,int nextPos){
if(images!=null){
for(int i=0;i<images.length;i++){
images[i].setImageAlpha(100);
images[i].setScaleX(0.5f);
images[i].setScaleY(0.5f);
}
//改变前一张图的属性
if(255*changePer>=100)images[pastPosition].setImageAlpha((int)(255*changePer));
if(1f*changePer>=0.5f){
images[pastPosition].setScaleX(1f*changePer);
images[pastPosition].setScaleY(1f*changePer);
}
//改变当前图的属性
if(nextPos < images.length && nextPos>=0){
images[nextPos].setImageAlpha(100+(int)(155*(1-changePer)));
images[nextPos].setScaleX(0.5f+0.5f*(1-changePer));
images[nextPos].setScaleY(0.5f+0.5f*(1-changePer));
}
}
}
三、实现右下角滚轮滑动
滚轮可以由外界自定义实现,内部只需要提供滑动的监听器:
public interface OnScrollListener {
/**
* offsetPer:x轴滑动的距离比上屏幕宽度
*/
void onScrolled(float offsetPer);
void onSelected(int position);
}
private OnScrollListener onScrollListener;
public void setOnPageScrollListener(OnScrollListener onScrollListener){
this.onScrollListener=onScrollListener;
}
外部Activity中创建该监听:
public class MyViewPagerAct extends AppCompatActivity {
private MyViewPagerViewGroup vpager;
private int[] images = {R.mipmap.arti, R.mipmap.avatar, R.mipmap.butterfly, R.mipmap.head,R.mipmap.carousel_1,R.mipmap.bg_article};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_viewpager);
init();
}
private void init(){
vpager=findViewById(R.id.vpager);
vpager.setOnPageScrollListener(new MyViewPagerViewGroup.OnScrollListener() {
@Override
public void onScrolled(float offsetPer) {
}
@Override
public void onSelected(int position) {
}
});
vpager.setImages(images);
}
}
当前滑动的小球和固定的灰色小球创建的方式不同,我们在XML布局文件中添加滑动小球的布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.hualinfo.myviewtext.MyViewPagerViewGroup
android:id="@+id/vpager"
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_centerInParent="true"/>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_marginBottom="20dp"
android:layout_marginRight="60dp">
<LinearLayout
android:id="@+id/ll_p"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"></LinearLayout>
<View
android:id="@+id/view_point"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginLeft="5dp"
android:background="@drawable/black_point"/>
</RelativeLayout>
</RelativeLayout>
我们在代码里动态生成固定的灰色小球:
public class MyViewPagerAct extends AppCompatActivity {
private LinearLayout ll_p;
private MyViewPagerViewGroup vpager;
private int[] images = {R.mipmap.arti, R.mipmap.avatar, R.mipmap.butterfly, R.mipmap.head,R.mipmap.carousel_1,R.mipmap.bg_article};
private View view_point;
private int pointMargin,pointDiameter; //滑动点之间的边距,滑动点的直径
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_viewpager);
init();
}
private void init(){
pointMargin=px2dp(5);
pointDiameter=px2dp(10);
view_point=findViewById(R.id.view_point);
//......
ll_p=findViewById(R.id.ll_p);
addView();
}
private void addView(){ //右下角生成滑动点
LinearLayout.LayoutParams params=new LinearLayout.LayoutParams(pointDiameter,pointDiameter);
params.leftMargin=pointMargin;
for(int i=0;i<images.length;i++){
View view=new View(this);
view.setBackground(getDrawable(R.drawable.light_black_point));
view.setLayoutParams(params);
ll_p.addView(view);
}
}
private int px2dp(float spValue){
return (int)(getResources().getDisplayMetrics().scaledDensity*spValue+0.5f);
}
}
滑动小球会随着图片的移动而移动,即在onScroll方法中改变小球的leftMargin左边距来实现移动效果
public class MyViewPagerAct extends AppCompatActivity {
private MyViewPagerViewGroup vpager;
private View view_point;
private int pointMargin,pointDiameter; //滑动点之间的边距,滑动点的直径
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_viewpager);
init();
}
private void init(){
pointMargin=px2dp(5);
pointDiameter=px2dp(10);
vpager=findViewById(R.id.vpager);
view_point=findViewById(R.id.view_point);
vpager.setOnPageScrollListener(new MyViewPagerViewGroup.OnScrollListener() {
@Override
public void onScrolled(float offsetPer) {
float leftMargin=offsetPer*(pointMargin+pointDiameter)+pointMargin;
RelativeLayout.LayoutParams params=(RelativeLayout.LayoutParams)view_point.getLayoutParams();
params.leftMargin=(int) leftMargin;
view_point.setLayoutParams(params);
}
@Override
public void onSelected(int position) {
}
});
vpager.setImages(images);
//......
}
private int px2dp(float spValue){
return (int)(getResources().getDisplayMetrics().scaledDensity*spValue+0.5f);
}
}
在自定义ViewGroup内部调用监听方法:
@Override
public boolean onTouchEvent(MotionEvent event) {
//将触摸事件传递手势识别器
gestureDetector.onTouchEvent(event);
switch (event.getAction()){
case MotionEvent.ACTION_MOVE:
//......
if(onScrollListener!=null)
onScrollListener.onScrolled(getScrollX()*1.0f/getWidth()*onPagerNum);
break;
}
return true;
}
//移动页面
private void move(){
//......
if (onScrollListener != null) {
onScrollListener.onSelected(nowPosition);
}
}
@Override
public void computeScroll() {
//......
if(onScrollListener!=null)onScrollListener.onScrolled(getScrollX()*1.0f/getWidth()*onPagerNum);
}
getScrollX()*1.0f/getWidth()*onPagerNum:getScrollX()*1.0f/getWidth()表示已滑动的距离比上ViewGroup宽度,最后乘以onPagerNum代表的是每滑过1/3的ViewGroup宽度,小球就移动到下一个默认的灰球位置。
这样子小球就能随着图片的滑动而滑动了。
四、补充
如果我们想让图片每隔一段时间自动滑动到下一张,可以按以下步骤实现:
1、创建TimerTask和Timer类
Timer moveTimer=new Timer();
TimerTask moveTask=new TimerTask() { //创建一个任务,每隔一段时间自动滑动到下一页
@Override
public void run() {
if(!isMoving){ //手指是否在移动屏幕
nowPosition=(nowPosition+1)%images.length;
move();
}
}
};
2、在外部设置图片的时间,开启TimerTask任务线程,并每隔一段时间执行一次
//获取外界传来的图片
public void setImages(int[] imgs){
//......
moveTimer.schedule(moveTask,4000,4000);
}
下面是自定义ViewGroup的源码:
import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.Scroller;
import java.util.Timer;
import java.util.TimerTask;
public class MyViewPagerViewGroup extends ViewGroup {
private ImageView[] images;
private float lastPosX; //最后手指在屏幕的x坐标
private Scroller mScroller; //用于平滑过渡
private int onPagerNum=3; //屏幕中出现的图片数量
private int paddingPic=80/onPagerNum;
private int nowPosition=0,pastPosition=0; //当前显示哪一张图片,上一次显示哪一张图片
private boolean isMoving=false; //是否手指正在移动
public MyViewPagerViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){
mScroller=new Scroller(getContext());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
//如果是view:触发view的测量;如果是ViewGroup,触发测量ViewGroup中的子view
measureChild(getChildAt(i),widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount=getChildCount();
for(int i=0;i<childCount;i+=onPagerNum){
for(int j=0;j<onPagerNum;j++){
if(i+j>=childCount)break;
View childView=getChildAt(i+j);
//layout:相对于父布局的位置
childView.layout(i/onPagerNum*getWidth()+j*getWidth()/onPagerNum+paddingPic,0,i/onPagerNum*getWidth()+(j+1)*getWidth()/onPagerNum-paddingPic,getHeight());
}
}
}
private GestureDetector gestureDetector=new GestureDetector(new GestureDetector.SimpleOnGestureListener(){
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
//相对滑动:X方向滑动多少距离,view就跟着滑动多少距离
scrollBy((int)distanceX/onPagerNum,0);
return super.onScroll(e1, e2, distanceX, distanceY);
}
});
@Override
public boolean onTouchEvent(MotionEvent event) {
if(images==null)return false;
//将触摸事件传递手势识别器
gestureDetector.onTouchEvent(event);
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastPosX=event.getX();
break;
case MotionEvent.ACTION_MOVE:
isMoving=true;
if(lastPosX<event.getX()){ //手指向右滑
nowPosition=(getScrollX()+getWidth()/(3*onPagerNum))/(getWidth()/onPagerNum);
}else{ //手指向左滑
nowPosition=(getScrollX()+2*getWidth()/(3*onPagerNum))/(getWidth()/onPagerNum);
}
if(nowPosition>=images.length)nowPosition=images.length-1;
if(nowPosition<0)nowPosition=0;
if(onScrollListener!=null)onScrollListener.onScrolled(getScrollX()*1.0f/getWidth()*onPagerNum);
if(lastPosX!=0&&event.getX()!=0){
if(lastPosX>event.getX())setImgsTypes(event.getX()/lastPosX,pastPosition+1);
else setImgsTypes(lastPosX/event.getX(),pastPosition-1);
}
break;
case MotionEvent.ACTION_UP:
isMoving=false;
move();
break;
}
return true;
}
//移动页面
private void move(){
//如果上一次位置比当前位置小,说明是移动到了后一张图片
if(pastPosition<nowPosition){
pastPosition=nowPosition;
setImgsTypes(1f,nowPosition+1);
//如果上一次位置比当前位置大,说明是移动到了前一张图片
}else{
pastPosition=nowPosition;
setImgsTypes(1f,nowPosition-1);
}
mScroller.startScroll(getScrollX(),0,nowPosition*getWidth()/onPagerNum-getScrollX(),0);
invalidate(); //使用invalidate会执行回调方法computeScroll
if (onScrollListener != null) {
onScrollListener.onSelected(nowPosition);
}
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), 0);
if(onScrollListener!=null)onScrollListener.onScrolled(getScrollX()*1.0f/getWidth()*onPagerNum);
postInvalidateDelayed(10);
}
}
public interface OnScrollListener {
/**
* offsetPer:x轴滑动的距离比上屏幕宽度
*/
void onScrolled(float offsetPer);
void onSelected(int position);
}
private OnScrollListener onScrollListener;
public void setOnPageScrollListener(OnScrollListener onScrollListener){
this.onScrollListener=onScrollListener;
}
Timer moveTimer=new Timer();
TimerTask moveTask=new TimerTask() { //创建一个任务,每隔一段时间自动滑动到下一页
@Override
public void run() {
if(!isMoving){
nowPosition=(nowPosition+1)%images.length;
move();
}
}
};
//获取外界传来的图片
public void setImages(int[] imgs){
this.images=new ImageView[imgs.length];
if(onPagerNum>1)setBlankView();
for(int i=0;i<images.length;i++){
images[i]=new ImageView(getContext());
images[i].setImageResource(imgs[i]);
this.addView(images[i]);
}
setImgsTypes(1f,nowPosition+1);
moveTimer.schedule(moveTask,4000,4000);
}
//设置空白的View
private void setBlankView(){
View view=new View(getContext());
this.addView(view);
}
//设置图片属性
/*
* changePer:动态改变图片属性的比例
* nextPos:下一张图片的下标
* */
private void setImgsTypes(float changePer,int nextPos){
if(images!=null){
for(int i=0;i<images.length;i++){
images[i].setImageAlpha(100);
images[i].setScaleX(0.5f);
images[i].setScaleY(0.5f);
}
//缩小
if(255*changePer>=100)images[pastPosition].setImageAlpha((int)(255*changePer));
if(1f*changePer>=0.5f){
images[pastPosition].setScaleX(1f*changePer);
images[pastPosition].setScaleY(1f*changePer);
}
//放大
if(nextPos < images.length && nextPos>=0){
images[nextPos].setImageAlpha(100+(int)(155*(1-changePer)));
images[nextPos].setScaleX(0.5f+0.5f*(1-changePer));
images[nextPos].setScaleY(0.5f+0.5f*(1-changePer));
}
}
}
}
外部Activity类:
public class MyViewPagerAct extends AppCompatActivity {
private LinearLayout ll_p;
private MyViewPagerViewGroup vpager;
private int[] images = {R.mipmap.arti, R.mipmap.avatar, R.mipmap.butterfly, R.mipmap.head,R.mipmap.carousel_1,R.mipmap.bg_article};
private View view_point;
private int pointMargin,pointDiameter; //滑动点之间的边距,滑动点的直径
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_viewpager);
init();
}
private void init(){
pointMargin=px2dp(5);
pointDiameter=px2dp(10);
view_point=findViewById(R.id.view_point);
vpager=findViewById(R.id.vpager);
vpager.setOnPageScrollListener(new MyViewPagerViewGroup.OnScrollListener() {
@Override
public void onScrolled(float offsetPer) {
float leftMargin=offsetPer*(pointMargin+pointDiameter)+pointMargin;
RelativeLayout.LayoutParams params=(RelativeLayout.LayoutParams)view_point.getLayoutParams();
params.leftMargin=(int) leftMargin;
view_point.setLayoutParams(params);
}
@Override
public void onSelected(int position) {
}
});
vpager.setImages(images);
ll_p=findViewById(R.id.ll_p);
addView();
}
private void addView(){ //右下角生成滑动点
LinearLayout.LayoutParams params=new LinearLayout.LayoutParams(pointDiameter,pointDiameter);
params.leftMargin=pointMargin;
for(int i=0;i<images.length;i++){
View view=new View(this);
view.setBackground(getDrawable(R.drawable.light_black_point));
view.setLayoutParams(params);
ll_p.addView(view);
}
}
private int px2dp(float spValue){
return (int)(getResources().getDisplayMetrics().scaledDensity*spValue+0.5f);
}
}