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~