Android自定义HorizontalScrollView和ImageView实现不一样的轮播图效果
需求效果
最近在项目中需要实现一种跟大家在平时看到的大部分项目中不一样的轮播图的效果,图片在屏幕中可快速滑动切换显示,并且在滑动过程中,图片旋转角度并且逐渐变黑白图片,滑动停止后通过计算图片中心点距离屏幕中心距离最近的图片居中显示,显示正常彩色和不旋转角度;展示然后花了一些时间通过自定义HorizontalScrollView和ImageView最终实现了效果,特此进行记录下来,效果如下面所示:
HorizontalScrollView实现滚动状态监听
首先自定义HorizontalScrollView有三种滚动状态,包括IDLE(滚动停止),TOUCH_SCROLL(手指拖动滚动)和手指拖动离开界面后自动滚动 FLING(滚动);接着重写onTouchEvent事件,MotionEvent.ACTION_MOVE表示手指在屏幕上滑动,此时滚动状态为ScrollStatus.TOUCH_SCROLL;MotionEvent.ACTION_UP表示手指离开了屏幕,通过mHandler.post(scrollRunnable)进行滚动状态监听,此时滚动状态有两种情况,一个是ScrollStatus.IDLE滚动停止了,另一个是ScrollStatus.FLING还在自动滚动中,间隔一段时间通过判断getScrollX() == currentX是否为true,如果为true则表示当前滚动停止了,如果为false则表示还在滚动中,代码如下:
/**
* 滚动状态:
* IDLE=滚动停止
* TOUCH_SCROLL=手指拖动滚动
* FLING=滚动
*/
public enum ScrollStatus {
IDLE,
TOUCH_SCROLL,
FLING
}
/**
* 记录当前滚动的距离
*/
private int currentX = 0;
/**
* 当前滚动状态
*/
private ScrollStatus scrollStatus = ScrollStatus.IDLE;
private ScrollStatusListener mScrollStatusListener;
public interface ScrollStatusListener {
void onScrollChanged(ScrollStatus scrollStatus);
}
public void setScrollStatusListener(ScrollStatusListener scrollStatusListener) {
this.mScrollStatusListener = scrollStatusListener;
}
/**
* 滚动状态监听runnable
*/
private Runnable scrollRunnable = new Runnable() {
@Override
public void run() {
if (getScrollX() == currentX) {
//滚动停止,取消监听线程
scrollStatus = ScrollStatus.IDLE;
if (mScrollStatusListener != null) {
mScrollStatusListener.onScrollChanged(scrollStatus);
}
mHandler.removeCallbacks(this);
return;
} else {
//手指离开屏幕,但是view还在滚动
scrollStatus = ScrollStatus.FLING;
if (mScrollStatusListener != null) {
mScrollStatusListener.onScrollChanged(scrollStatus);
}
}
currentX = getScrollX();
//滚动监听间隔:milliseconds
mHandler.postDelayed(this, 50);
}
};
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
this.scrollStatus = ScrollStatus.TOUCH_SCROLL;
if (mScrollStatusListener != null) {
mScrollStatusListener.onScrollChanged(scrollStatus);
}
mHandler.removeCallbacks(scrollRunnable);
break;
case MotionEvent.ACTION_UP:
mHandler.post(scrollRunnable);
break;
}
return super.onTouchEvent(ev);
}
在实现HorizontalScrollView状态监听的同时,还需要实现它的滚动监听,以便得到它的滚动值scrollX来计算得到每个ImageView需要的旋转角度;如果调用ScrollVew.setScrollViewListener实现滚动监听,则需要Android6.0及6.0以上
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
hsScroll.setOnScrollChangeListener(new View.OnScrollChangeListener() {
@Override
public void onScrollChange(View view, int scrollX, int scrollY, int oldScrollX,
int oldScrollY) {
}
});
}
所以需要向下做兼容处理,ScrollView不能像其他组件一样使用onScrollChanged()方法是因为它被protected修饰了,所以只需要在CustomHorizontalScrollView做下封装即可
private ScrollViewListener scrollViewListener = null;
//滚动监听
public interface ScrollViewListener {
void onScrollChanged(View chsv, int scrollX, int scrollY, int oldScrollX, int oldScrollY);
}
public void setScrollViewListener(ScrollViewListener scrollViewListener) {
this.scrollViewListener = scrollViewListener;
}
@Override
protected void onScrollChanged(int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
super.onScrollChanged(scrollX, scrollY, oldScrollX, oldScrollY);
if (scrollViewListener != null) {
scrollViewListener.onScrollChanged(this, scrollX, scrollY, oldScrollX, oldScrollY);
}
}
自定义ImageView实现旋转和黑白处理
ImageView旋转可以通过矩阵Matrix来实现,对于矩阵大家应该都知道,百度或Google也可以搜到一大把的资料,这边我就不说了,图片彩色和黑白的处理切换可以通过颜色矩阵ColorMatrix设置饱和度来处理,当然效果和完全的黑白图片还是有一点点差距的,但是如果真的要使用彩色图片变成黑白图片想到有两种实现方案,一种是通过对像素点进行处理,我也试过很多的方法来处理,但是效率都是太低且会出现卡顿,这样就无法实现丝滑快速滚动切换了;还有一种就是使用黑白图片和彩色图片叠在一起,然后给彩色图片根据旋转角度值进行设置透明度,这种方案实现也不好,实现麻烦并且图片在项目中大部分都是需要从网络上进行加载的,这样处理起来的效率会很低了;用设置饱和度的方法我觉得是最好的了,可见上面的演示或者在底部点击下载Demo查看效果
@Override
protected void onDraw(Canvas canvas) {
int width = getWidth();
int height = getHeight();
if (mNewBmp == null) {
mNewBmp = createBmp(mShowBmp, width, height);
centerX = mNewBmp.getWidth() / 2;
centerY = mNewBmp.getHeight() / 2;
}
mCamera.save();
//绕Y轴翻转
mCamera.rotateY(mDegree);
//设置camera作用矩阵
mCamera.getMatrix(mMatrix);
mCamera.restore();
//设置翻转中心点
mMatrix.preTranslate(-this.centerX, -this.centerY);
mMatrix.postTranslate(this.centerX, this.centerY);
Paint paint = new Paint();
// 透明度
paint.setAlpha((int) (255 - Math.abs(mDegree) * 6));
// 灰色
ColorMatrix colorMatrix = new ColorMatrix();
float grey = 1f - Math.abs((float) mDegree / 15f);
if (grey < 0) {
grey = 0;
}
colorMatrix.setSaturation(grey);
ColorMatrixColorFilter colorMatrixFilter = new ColorMatrixColorFilter(colorMatrix);
paint.setColorFilter(colorMatrixFilter);
canvas.drawBitmap(mNewBmp, mMatrix, paint);
}
public Bitmap createBmp(Bitmap bm, int newWidth, int newHeight) {
// 获得图片的宽高
int width = bm.getWidth();
int height = bm.getHeight();
// 计算缩放比例
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
// 取得想要缩放的matrix参数
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
// 得到新的图片
Bitmap newBmp = Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true);
return newBmp;
}
/**
* 设置旋转角度
* @param degree
*/
public void setDegree(float degree) {
mDegree = degree;
invalidate();
}
主工程main
直接上代码,代码中已经进行了很详细的注释了,甚至有点啰嗦了,但为了能够一眼看懂,我也是操碎了心的。
layout.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF99CC"
android:orientation="vertical">
<com.clay.slideshowdemo.view.CustomHorizontalScrollView
android:id="@+id/hs_scroll"
android:layout_width="match_parent"
android:layout_height="350dp"
android:layout_marginTop="50dp"
android:scrollbars="none">
<LinearLayout
android:id="@+id/ll_shifting"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal">
<com.clay.slideshowdemo.view.RotateImageView
android:id="@+id/riv_first"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/picture_a" />
<com.clay.slideshowdemo.view.RotateImageView
android:id="@+id/riv_second"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/picture_b" />
<com.clay.slideshowdemo.view.RotateImageView
android:id="@+id/riv_third"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/picture_c" />
<com.clay.slideshowdemo.view.RotateImageView
android:id="@+id/riv_fourth"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/picture_d" />
<com.clay.slideshowdemo.view.RotateImageView
android:id="@+id/riv_fifth"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/picture_e" />
<com.clay.slideshowdemo.view.RotateImageView
android:id="@+id/riv_sixth"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/picture_f" />
<com.clay.slideshowdemo.view.RotateImageView
android:id="@+id/riv_seventh"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/picture_g" />
<com.clay.slideshowdemo.view.RotateImageView
android:id="@+id/riv_eighth"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/picture_h" />
</LinearLayout>
</com.clay.slideshowdemo.view.CustomHorizontalScrollView>
<LinearLayout
android:id="@+id/dot_layout"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_marginTop="6dp"
android:gravity="center"
android:orientation="horizontal" />
</LinearLayout>
在Activity中使用
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
RotateImageView[] arrImgs = new RotateImageView[8];
private CustomHorizontalScrollView chsvScroll;
private LinearLayout llShifting;
private LinearLayout dotLayout; //轮播图跟着滑动的点
private int mLocation = 0; //位置居中的条目
private boolean mPositiveCycle = true; //循环的方向
private int mScreenWidth; // 获取屏幕宽度
private static final int ROTATE_ANGLE = 15; //角度
private int mScreenWidthHalf = 0;
Map<Integer, Integer> mLeftMap = new HashMap<>(); //保存每条条目中心距离HorizontalScrollView的最左边的距离
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 1) {
if (mLocation == 0) {
mPositiveCycle = true;
} else if (mLocation == arrImgs.length - 1) {
mPositiveCycle = false;
}
if (mPositiveCycle) {
setCurrentCenter(mLocation + 1);
} else {
setCurrentCenter(mLocation - 1);
}
mHandler.sendEmptyMessageDelayed(1, 6000);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initData();
initListener();
}
private void initView() {
chsvScroll = (CustomHorizontalScrollView) findViewById(R.id.hs_scroll);
llShifting = (LinearLayout) findViewById(R.id.ll_shifting);
dotLayout = (LinearLayout) findViewById(R.id.dot_layout);
arrImgs[0] = (RotateImageView) findViewById(R.id.riv_first);
arrImgs[1] = (RotateImageView) findViewById(R.id.riv_second);
arrImgs[2] = (RotateImageView) findViewById(R.id.riv_third);
arrImgs[3] = (RotateImageView) findViewById(R.id.riv_fourth);
arrImgs[4] = (RotateImageView) findViewById(R.id.riv_fifth);
arrImgs[5] = (RotateImageView) findViewById(R.id.riv_sixth);
arrImgs[6] = (RotateImageView) findViewById(R.id.riv_seventh);
arrImgs[7] = (RotateImageView) findViewById(R.id.riv_eighth);
}
private void initData() {
mScreenWidth = getResources().getDisplayMetrics().widthPixels;
mScreenWidthHalf = mScreenWidth / 2;
// 设置每个条目的宽为屏幕的一半
for (int i = 0; i < arrImgs.length; i++) {
ViewGroup.LayoutParams layoutParams = arrImgs[i].getLayoutParams();
layoutParams.width = mScreenWidthHalf;
arrImgs[i].setLayoutParams(layoutParams);
}
//设置llShifting的leftPadding和rightPadding值为屏幕宽度的1/4,使RotateImageView居中显示
int shifting = mScreenWidth / 4;
llShifting.setPadding(shifting, 0, shifting, 0);
//记录每个RotateImageView的中心到其父view的左边的距离,是为了以后滑动时计算每个RotateImageView的旋转角度
for (int i = 0; i < arrImgs.length; i++) {
int left = arrImgs[i].getLeft();
//因为view在oncreate中还没绘制,所以无法获取arrImgs[i].getLeft的值,
//可通过View.Post(Runnable)等到绘制完成后获取arrImgs[i].getLeft
//但在此项目中,arrImgs[i].getLeft是可预知的,因为已设置了每个arrImgs[i]的宽度和其在屏幕中的显示位置,
mLeftMap.put(i, mScreenWidthHalf * (i + 1));
}
//初始化条目所在的位置点
initDots();
//设置每个RotateImageView的旋转角度
setRotateAngle(0);
//设置第一个RotateImageView居中
setCurrentCenter(0);
}
private void initListener() {
arrImgs[0].setOnClickListener(this);
arrImgs[1].setOnClickListener(this);
arrImgs[2].setOnClickListener(this);
arrImgs[3].setOnClickListener(this);
arrImgs[4].setOnClickListener(this);
arrImgs[5].setOnClickListener(this);
arrImgs[6].setOnClickListener(this);
arrImgs[7].setOnClickListener(this);
// 滚动状态监听
chsvScroll.setScrollStatusListener(new CustomHorizontalScrollView.ScrollStatusListener() {
@Override
public void onScrollChanged(CustomHorizontalScrollView.ScrollStatus scrollStatus) {
//手势滚动时不要进行自动轮播
mHandler.removeMessages(1);
//滚动停止,然后计算每个arrImgs[i]的中心到屏幕的mScreenWidthHalf的距离,对比得到最小值,从而求出滑动停止后需要居中不旋转角度的arrImgs[mLocation]
if (scrollStatus == CustomHorizontalScrollView.ScrollStatus.IDLE) {
int scrollX = chsvScroll.getScrollX();
int position = 0;
//计算屏幕中心距离CustomHorizontalScrollView左边的距离
int stopCenterScroll = mScreenWidthHalf + scrollX;
int distanceCenterMin = Math.abs(mLeftMap.get(0) - stopCenterScroll);
for (int i = 1; i < arrImgs.length; i++) {
int left = mLeftMap.get(i);
int value = Math.abs(stopCenterScroll - left);
if (value < distanceCenterMin) {
position = i;
distanceCenterMin = value;
}
}
setCurrentCenter(position);
//滚动停止后继续进行自动轮播
mHandler.sendEmptyMessageDelayed(1, 6000);
}
}
});
//滚动监听
chsvScroll.setScrollViewListener(new CustomHorizontalScrollView.ScrollViewListener() {
@Override
public void onScrollChanged(View chsv, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
setRotateAngle(scrollX);
}
});
}
/**
* 初始化跟条目相对应的点
*/
private void initDots() {
for (int i = 0; i < arrImgs.length; i++) {
View view = new View(this);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(Utils.dp2Px(this, 14), Utils.dp2Px(this, 6));
if (i != 0) {
params.leftMargin = Utils.dp2Px(this, 5);
}
view.setLayoutParams(params);
view.setBackgroundResource(R.drawable.selector_dot);
dotLayout.addView(view);
}
mHandler.sendEmptyMessageDelayed(1, 6000);
}
/**
* 更新点
*/
private void updateIntroAndDot(int position) {
for (int i = 0; i < dotLayout.getChildCount(); i++) {
dotLayout.getChildAt(i).setEnabled(i == position);
}
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.riv_first:
setCurrentCenter(0);
Toast.makeText(this, "position : 0", Toast.LENGTH_SHORT).show();
break;
case R.id.riv_second:
setCurrentCenter(1);
Toast.makeText(this, "position : 1", Toast.LENGTH_SHORT).show();
break;
case R.id.riv_third:
setCurrentCenter(2);
Toast.makeText(this, "position : 2", Toast.LENGTH_SHORT).show();
break;
case R.id.riv_fourth:
setCurrentCenter(3);
Toast.makeText(this, "position : 3", Toast.LENGTH_SHORT).show();
break;
case R.id.riv_fifth:
setCurrentCenter(4);
Toast.makeText(this, "position : 4", Toast.LENGTH_SHORT).show();
break;
case R.id.riv_sixth:
setCurrentCenter(5);
Toast.makeText(this, "position : 5", Toast.LENGTH_SHORT).show();
break;
case R.id.riv_seventh:
setCurrentCenter(6);
Toast.makeText(this, "position : 6", Toast.LENGTH_SHORT).show();
break;
case R.id.riv_eighth:
setCurrentCenter(7);
Toast.makeText(this, "position : 7", Toast.LENGTH_SHORT).show();
break;
}
}
/**
* 设置每个RotateImageView的旋转角度
*
* @param scrollX
*/
public void setRotateAngle(int scrollX) {
for (int i = 0; i < arrImgs.length; i++) {
//旋转角度 = (HorizontalScrollView的x轴方向偏移量 + 屏幕宽度的一半 - RotateImageView的中心距父view的最左边距离) * 旋转角度 / 屏幕宽度的一半
int degree = (scrollX + mScreenWidthHalf - mLeftMap.get(i)) * ROTATE_ANGLE / mScreenWidthHalf;
arrImgs[i].setDegree(degree);
}
}
/**
* 设置滚动停止后需要居中的position以及更新点
*
* @param position
*/
public void setCurrentCenter(int position) {
//记录当前条目
mLocation = position;
//计算控件居正中时距离左侧屏幕的距离
int middleLeftPosition = (mScreenWidth - arrImgs[position].getWidth()) / 2;
//需要显示在正中间位置的position需要向左偏移的距离
int left = arrImgs[position].getLeft();
int offset = left - middleLeftPosition;
//让水平的滚动视图按照执行的x的偏移量进行移动
chsvScroll.smoothScrollTo(offset, 0);
//更新点
updateIntroAndDot(position);
}
}
可结合下面这张图可以更好的进行理解
大家可以想象成一个长方形的长纸条放在手机屏幕上进行左右方向的拖动,其中长方形左右两边各留出宽度是屏幕四分之一的空白处(这是为了第一个和最后一个能够居中显示),其余部分等分成多个小长方形,其中每个小长方形的宽度都是屏幕的一半(这个都是可以自己调节,只需要保证每个小长方形的宽度相同即可,逻辑通了,其它就好办多了),然后给手机屏幕中心和各个小长方形中心进行瞄点,然后拖动,当拖动停止后通过判断长纸条中哪个小长方形的中心距离屏幕中心点最近,即可设置滚动停止后需要居中的小长方形(这就是设置滚动状态监听停止后的逻辑代码需要实现的结果);至于小长方形的旋转角度则根据小长方形左右拖动时小长方形中心点距离原来未拖动时的中心点所在位置的的距离进行计算的(即是HorizontalScroll的scrollX)。
GitHub传送门,如何觉得还可以,欢迎star~