Android從零開始實現可以雙指縮放,自由移動,雙擊放大的自定義ImageView
之前一直都是用別人的輪子,總感覺心裏不踏實,所以自己也嘗試這造一些別人造過的輪子,提升自己,也可以在平時開發中工作中更熟練的使用。
這一次從零開始,擼一個可單指移動,雙擊放大,雙指縮放,雙指移動的ImageView
這篇博文較長,如果向直接看完整代碼,請直接拉到最後。
我們先來看看效果吧
第一步:繼承ImageView,並讓圖片能等比縮放後居中顯示
public class ZoomImageView extends ImageView {
private Matrix matrix;
public ZoomImageView(Context context) {
super(context);
init();
}
public ZoomImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public ZoomImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
setScaleType(ScaleType.MATRIX);
matrix = new Matrix();
}
}
佈局時這樣的
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.zoomimagetest.view.ZoomImageView
android:id="@+id/my_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:src="@drawable/test5"/>
</androidx.constraintlayout.widget.ConstraintLayout>
運行後是這樣的
圖片沒有顯示全,那麼接下來就讓圖片默認等比縮放然後居中顯示
如果要實現等比縮放,需要知道ImageView 的寬高,以及圖片的原始寬高,ImageView 的寬高這些信息可以再onMeasure方法中獲取,而圖片的寬高可以通過Drawable這個類拿到,下面代碼中,獲取圖片的寬高時還有一個坑,這個後面再說。
//imageView的大小
private PointF viewSize;
//圖片的大小
private PointF imageSize;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
viewSize = new PointF(width,height);
Drawable drawable = getDrawable();
imageSize = new PointF(drawable.getMinimumWidth(),drawable.getMinimumHeight());
}
然後我們ImageView的寬與圖片的寬的比例,以及ImageView的高與圖片的高的比例,對比寬和高的縮放比例,取較小的那個做爲圖片的縮放比例。
這裏解釋下爲什麼要用較小的那個做爲圖片的縮放比例,因爲我們需要圖片等比縮放,並且顯示全,這裏在同一部手機上,相同的這個自定義ZoomImageView中,可以認爲viewSize的x,y是固定的,也就是在下面的式子中,分子是固定的,所以,如果整個值就越小,說明分母就越大,也就說明圖片的這條邊與這個view的這條邊的差距越大,所以按照相差較大的這條邊的縮放比例來縮放,肯定能保證在view中顯示全。
float scalex = viewSize.x/imageSize.x;
float scaley = viewSize.y/imageSize.y;
float scale = scalex<scaley?scalex:scaley;
得到我們的縮放比例後就可以對圖片進行縮放了
//縮放後圖片的大小
private PointF scaleSize = new PointF();
public void scaleImage(PointF scaleXY){
matrix.setScale(scaleXY.x,scaleXY.y);
//這裏將縮放後的圖片大小保存一下
scaleSize.set(scaleXY.x * imageSize.x,scaleXY.y * imageSize.y);
setImageMatrix(matrix);
}
到這一步的完整代碼如下
@SuppressLint("AppCompatCustomView")
public class ZoomImageView extends ImageView {
private Matrix matrix;
//imageView的大小
private PointF viewSize;
//圖片的大小
private PointF imageSize;
//縮放後圖片的大小
private PointF scaleSize = new PointF();
//最初的寬高的縮放比例
private PointF originScale = new PointF();
public ZoomImageView(Context context) {
super(context);
init();
}
public ZoomImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public ZoomImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
setScaleType(ScaleType.MATRIX);
matrix = new Matrix();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
viewSize = new PointF(width,height);
Drawable drawable = getDrawable();
imageSize = new PointF(drawable.getMinimumWidth(),drawable.getMinimumHeight());
showCenter();
}
/**
* 設置圖片居中等比顯示
*/
private void showCenter(){
float scalex = viewSize.x/imageSize.x;
float scaley = viewSize.y/imageSize.y;
float scale = scalex<scaley?scalex:scaley;
scaleImage(new PointF(scale,scale));
}
/**
* 將圖片按照比例縮放,這裏寬高縮放比例相等,所以PoinF 裏面的x,y是一樣的
* @param scaleXY
*/
public void scaleImage(PointF scaleXY){
matrix.setScale(scaleXY.x,scaleXY.y);
scaleSize.set(scaleXY.x * imageSize.x,scaleXY.y * imageSize.y);
setImageMatrix(matrix);
}
運行效果如下
到這裏,圖片是顯示全了,但是並沒有居中顯示,那麼接下來就來處理圖片居中顯示的問題。
/**
* 對圖片進行x和y軸方向的平移
* @param pointF
*/
public void translationImage(PointF pointF){
matrix.postTranslate(pointF.x,pointF.y);
setImageMatrix(matrix);
}
上面是對圖片進行平移的方法
接下來需要計算,如果要圖片在ImageView中居中顯示,需要的平移量
if (scalex<scaley){
translationImage(new PointF(0,viewSize.y/2 - scaleSize.y/2));
}else {
translationImage(new PointF(viewSize.x/2 - scaleSize.x/2,0));
}
前面講過,兩個方向的縮放值越小,說明那個方向的view的值與圖片的值差距就越大,按照這個較小縮放值,縮放後,這個方向就正好充滿view,那麼此方向就不需要平移,平移另外一個方向即可。這裏如果沒有理解的,可以自己在紙上畫個圖。
那麼到這裏,第一步就等比縮放,居中顯示就完成了。此時的全部代碼如下
@SuppressLint("AppCompatCustomView")
public class ZoomImageView extends ImageView {
private Matrix matrix;
//imageView的大小
private PointF viewSize;
//圖片的大小
private PointF imageSize;
//縮放後圖片的大小
private PointF scaleSize = new PointF();
//最初的寬高的縮放比例
private PointF originScale = new PointF();
//imageview中bitmap的xy實時座標
private PointF bitmapOriginPoint = new PointF();
public ZoomImageView(Context context) {
super(context);
init();
}
public ZoomImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public ZoomImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
setScaleType(ScaleType.MATRIX);
matrix = new Matrix();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
viewSize = new PointF(width,height);
Drawable drawable = getDrawable();
imageSize = new PointF(drawable.getMinimumWidth(),drawable.getMinimumHeight());
showCenter();
}
/**
* 設置圖片居中等比顯示
*/
private void showCenter(){
float scalex = viewSize.x/imageSize.x;
float scaley = viewSize.y/imageSize.y;
float scale = scalex<scaley?scalex:scaley;
scaleImage(new PointF(scale,scale));
//移動圖片,並保存最初的圖片左上角(即原點)所在座標
if (scalex<scaley){
translationImage(new PointF(0,viewSize.y/2 - scaleSize.y/2));
bitmapOriginPoint.x = 0;
bitmapOriginPoint.y = viewSize.y/2 - scaleSize.y/2;
}else {
translationImage(new PointF(viewSize.x/2 - scaleSize.x/2,0));
bitmapOriginPoint.x = viewSize.x/2 - scaleSize.x/2;
bitmapOriginPoint.y = 0;
}
//保存下最初的縮放比例
originScale.set(scale,scale);
}
public void scaleImage(PointF scaleXY){
matrix.setScale(scaleXY.x,scaleXY.y);
scaleSize.set(scaleXY.x * imageSize.x,scaleXY.y * imageSize.y);
setImageMatrix(matrix);
}
/**
* 對圖片進行x和y軸方向的平移
* @param pointF
*/
public void translationImage(PointF pointF){
matrix.postTranslate(pointF.x,pointF.y);
setImageMatrix(matrix);
}
效果圖如下
第二步:雙擊縮放
具體需要實現的功能是,雙擊圖片放大一倍,在放大的時候再雙擊,就回到最初的大小,並且時點哪裏放大哪裏,即放大後,雙擊的點,位置座標不變。
那麼接下來就會要實現OnTouchListener 實現onTouch方法,並做一些事件的判斷了
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
//手指按下事件
break;
case MotionEvent.ACTION_POINTER_DOWN:
//屏幕上已經有一個點被按住了 第二個點被按下時觸發該事件
break;
case MotionEvent.ACTION_POINTER_UP:
//屏幕上已經有兩個點按住 再鬆開一個點時觸發該事件
break;
case MotionEvent.ACTION_MOVE:
//手指移動時觸發事件
break;
case MotionEvent.ACTION_UP:
//手指鬆開時觸發事件
break;
}
return true;
}
在正式處理事件之前,我們先定義幾個變量,並定義雙擊能放大一倍還有縮放的三個狀態
//縮放的三個狀態
public class ZoomMode{
public final static int Ordinary=0;//普通
public final static int ZoomIn=1;//雙擊放大
public final static int TowFingerZoom = 2;//雙指縮放
}
//點擊的點
private PointF clickPoint = new PointF();
//設置的雙擊檢查時間間隔
private long doubleClickTimeSpan = 250;
//上次點擊的時間
private long lastClickTime = 0;
//雙擊放大的倍數
private int doubleClickZoom = 2;
//當前縮放的模式
private int zoomInMode = ZoomMode.Ordinary;
//臨時座標比例數據
private PointF tempPoint = new PointF();
下面是屏幕被按下事件的處理
case MotionEvent.ACTION_DOWN:
//手指按下事件
//記錄被點擊的點的座標
clickPoint.set(event.getX(),event.getY());
//判斷屏幕上此時被按住的點的個數,當前屏幕只有一個點被點擊的時候觸發
if (event.getPointerCount() == 1) {
//設置一個點擊的間隔時長,來判斷是不是雙擊
if (System.currentTimeMillis() - lastClickTime <= doubleClickTimeSpan) {
//如果圖片此時縮放模式是普通模式,就觸發雙擊放大
if (zoomInMode == ZoomMode.Ordinary) {
//分別記錄被點擊的點到圖片左上角x,y軸的距離與圖片x,y軸邊長的比例,方便在進行縮放後,算出這 個點對應的座標點
tempPoint.set((clickPoint.x - bitmapOriginPoint.x) / scaleSize.x, (clickPoint.y - bitmapOriginPoint.y) / scaleSize.y);
//進行縮放
scaleImage(new PointF(originScale.x * doubleClickZoom, originScale.y * doubleClickZoom));
//獲取縮放後,圖片左上角的xy座標
getBitmapOffset();
//平移圖片,使得被點擊的點的位置不變。這裏是計算縮放後被點擊的xy座標,與原始點擊的位置的xy 座標值,計算出差值,然後做平移動作
translationImage(new PointF(clickPoint.x - (bitmapOriginPoint.x + tempPoint.x * scaleSize.x), clickPoint.y - (bitmapOriginPoint.y + tempPoint.y * scaleSize.y)));
zoomInMode = ZoomMode.ZoomIn;
} else {
//雙擊還原
showCenter();
zoomInMode = ZoomMode.Ordinary;
}
} else {
lastClickTime = System.currentTimeMillis();
}
}
break;
/**
* 獲取view中bitmap的座標點
*/
public void getBitmapOffset(){
float[] value = new float[9];
float[] offset = new float[2];
Matrix imageMatrix = getImageMatrix();
imageMatrix.getValues(value);
offset[0] = value[2];
offset[1] = value[5];
bitmapOriginPoint.set(offset[0],offset[1]);
}
運行效果如下,也就實現了雙擊哪裏放大哪裏的效果
第三步:雙指縮放
雙指縮放其實和雙擊放大的原理是一樣的,只是由一個點的操作,變成兩個點的操作,並且縮放的倍數也是任意變化的。
先來看第一個問題,如果將兩個手指的縮放操作向一個點的操作轉變。這個其實很簡單,我們只需要算出兩個手指的之間的中點,來當作實際縮放的那個點,就像雙擊放大的那個點一樣。
再來看第二個問題,縮放的倍數。這個也簡單,只需要獲取兩個手指之間最開始是相聚多少,然後監測兩個手指間變化後的距離佔最開始的距離的百分比就可以了,都不需要單獨判斷是縮小還是放大。
下面看實際代碼
//最大縮放比例
private float maxScrole = 4;
//兩點之間的距離
private float doublePointDistance = 0;
//雙指縮放時候的中心點
private PointF doublePointCenter = new PointF();
//兩指縮放的比例
private float doubleFingerScrole = 0;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
//手指按下事件
//這裏省略上面實現的雙擊放大的代碼
...
//並在雙擊放大後記錄縮放比例
doubleFingerScrole = originScale.x*doubleClickZoom;
break;
case MotionEvent.ACTION_POINTER_DOWN:
//屏幕上已經有一個點按住 再按下一點時觸發該事件
//計算最初的兩個手指之間的距離
doublePointDistance = getDoubleFingerDistance(event);
break;
case MotionEvent.ACTION_POINTER_UP:
//屏幕上已經有兩個點按住 再鬆開一點時觸發該事件
//當有一個手指離開屏幕後,就修改狀態,這樣如果雙擊屏幕就能恢復到初始大小
zoomInMode = ZoomMode.ZoomIn;
//記錄此時的雙指縮放比例
doubleFingerScrole =scaleSize.x/imageSize.x;
break;
case MotionEvent.ACTION_MOVE:
//手指移動時觸發事件
/**************************************縮放 *******************************************/
//判斷當前是兩個手指接觸到屏幕才處理縮放事件
if (event.getPointerCount() == 2){
//如果此時縮放後的大小,大於等於了設置的最大縮放的大小,就不處理
if ((scaleSize.x/imageSize.x >= originScale.x * maxScrole
|| scaleSize.y/imageSize.y >= originScale.y * maxScrole)
&& getDoubleFingerDistance(event) - doublePointDistance > 0){
break;
}
//這裏設置當雙指縮放的的距離變化量大於50,並且當前不是在雙指縮放狀態下,
//就計算中心點,等一些操作
if (Math.abs(getDoubleFingerDistance(event) - doublePointDistance) > 50
&& zoomInMode != ZoomMode.TowFingerZoom){
//計算兩個手指之間的中心點,當作放大的中心點
doublePointCenter.set((event.getX(0) + event.getX(1))/2,
(event.getY(0) + event.getY(1))/2);
/將雙指的中心點就假設爲點擊的點
clickPoint.set(doublePointCenter);
//下面就和雙擊放大基本一樣
getBitmapOffset();
//分別記錄被點擊的點到圖片左上角x,y軸的距離與圖片x,y軸邊長的比例,
//方便在進行縮放後,算出這個點對應的座標點
tempPoint.set((clickPoint.x - bitmapOriginPoint.x)/scaleSize.x,
(clickPoint.y - bitmapOriginPoint.y)/scaleSize.y);
//設置進入雙指縮放狀態
zoomInMode = ZoomMode.TowFingerZoom;
}
//如果已經進入雙指縮放狀態,就直接計算縮放的比例,並進行位移
if (zoomInMode == ZoomMode.TowFingerZoom){
//用當前的縮放比例與此時雙指間距離的縮放比例相乘,就得到對應的圖片應該縮放的比例
float scrole =
doubleFingerScrole*getDoubleFingerDistance(event)/doublePointDistance;
//這裏也是和雙擊放大時一樣的
scaleImage(new PointF(scrole,scrole));
getBitmapOffset();
+bitmapOriginPoint);
translationImage(
new PointF(clickPoint.x - (bitmapOriginPoint.x + tempPoint.x*scaleSize.x),
clickPoint.y - (bitmapOriginPoint.y + tempPoint.y*scaleSize.y))
);
}
}
break;
case MotionEvent.ACTION_UP:
//手指鬆開時觸發事件
Log.e("kzg","***********************ACTION_UP");
break;
}
return true;
}
/**
* 計算零個手指間的距離
* @param event
* @return
*/
public static float getDoubleFingerDistance(MotionEvent event){
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return (float)Math.sqrt(x * x + y * y) ;
}
此外還需要再showCenter 方法中將最初的縮放比例賦值給doubleFingerScrole
doubleFingerScrole = scale;
好啦,來看看效果吧
第四步:單指或者雙指移動圖片
這裏也時比較簡單的,單指移動圖片就不用多說,雙指移動就算出兩指之間的中點,後面就和單指移動一樣了
//上次觸碰的手指數量
private int lastFingerNum = 0;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
//手指按下事件
//這裏省略上面實現的雙擊放大的代碼
...
break;
case MotionEvent.ACTION_POINTER_DOWN:
//屏幕上已經有一個點按住 再按下一點時觸發該事件
//計算最初的兩個手指之間的距離
doublePointDistance = getDoubleFingerDistance(event);
break;
case MotionEvent.ACTION_POINTER_UP:
//屏幕上已經有兩個點按住 再鬆開一點時觸發該事件
...
//記錄此時屏幕觸碰的點的數量
lastFingerNum = 1;
break;
case MotionEvent.ACTION_MOVE:
//手指移動時觸發事件
/**************************************移動
*******************************************/
if (zoomInMode != ZoomMode.Ordinary) {
//如果是多指,計算中心點爲假設的點擊的點
float currentX = 0;
float currentY = 0;
//獲取此時屏幕上被觸碰的點有多少個
int pointCount = event.getPointerCount();
//計算出中間點所在的座標
for (int i = 0; i < pointCount; i++) {
currentX += event.getX(i);
currentY += event.getY(i);
}
currentX /= pointCount;
currentY /= pointCount;
//當屏幕被觸碰的點的數量變化時,將最新算出來的中心點看作是被點擊的點
if (lastFingerNum != event.getPointerCount()) {
clickPoint.x = currentX;
clickPoint.y = currentY;
lastFingerNum = event.getPointerCount();
}
//將移動手指時,實時計算出來的中心點座標,減去被點擊點的座標就得到了需要移動的距離
float moveX = currentX - clickPoint.x;
float moveY = currentY - clickPoint.y;
//計算邊界,使得不能已出邊界,但是如果是雙指縮放時移動,因爲存在縮放效果,
//所以此時的邊界判斷無效
float[] moveFloat = moveBorderDistance(moveX, moveY);
//處理移動圖片的事件
translationImage(new PointF(moveFloat[0], moveFloat[1]));
clickPoint.set(currentX, currentY);
}
/**************************************縮放 *******************************************/
//下面省略之前的縮放的代碼
...
break;
case MotionEvent.ACTION_UP:
//手指鬆開時觸發事件
lastFingerNum = 0;
break;
}
return true;
}
/**
* 防止移動圖片超過邊界,計算邊界情況
* @param moveX
* @param moveY
* @return
*/
public float[] moveBorderDistance(float moveX,float moveY){
//計算bitmap的左上角座標
getBitmapOffset();
//計算bitmap的右下角座標
float bitmapRightBottomX = bitmapOriginPoint.x + scaleSize.x;
float bitmapRightBottomY = bitmapOriginPoint.y + scaleSize.y;
if (moveY > 0){
//向下滑
if (bitmapOriginPoint.y + moveY > 0){
if (bitmapOriginPoint.y < 0){
moveY = -bitmapOriginPoint.y;
}else {
moveY = 0;
}
}
}else if (moveY < 0){
//向上滑
if (bitmapRightBottomY + moveY < viewSize.y){
if (bitmapRightBottomY > viewSize.y){
moveY = -(bitmapRightBottomY - viewSize.y);
}else {
moveY = 0;
}
}
}
if (moveX > 0){
//向右滑
if (bitmapOriginPoint.x + moveX > 0){
if (bitmapOriginPoint.x < 0){
moveX = -bitmapOriginPoint.x;
}else {
moveX = 0;
}
}
}else if (moveX < 0){
//向左滑
if (bitmapRightBottomX + moveX < viewSize.x){
if (bitmapRightBottomX > viewSize.x){
moveX = -(bitmapRightBottomX - viewSize.x);
}else {
moveX = 0;
}
}
}
return new float[]{moveX,moveY};
}
再來看看效果吧
到這裏還差最後一個小功能,當圖片縮放的比例比最開始的比例要小時,讓其自動恢復到普通的縮放比例
代碼很簡單,在onTouch方法的MotionEvent.ACTION_POINTER_UP分支中添加
//判斷縮放後的比例,如果小於最初的那個比例,就恢復到最初的大小
if (scaleSize.x<viewSize.x && scaleSize.y<viewSize.y){
zoomInMode = ZoomMode.Ordinary;
showCenter();
}
到這裏功能都已經完成了,但是還記得我一開始說的一個坑嗎?那就是在onMeasure方法中調用 getDrawable() 來獲取圖片的Drawable對象,這裏一不小心就會獲取到空的Drawable,我們這裏沒有問題時因爲我們直接在佈局中就將要顯示的圖片賦值了,而實際開發中很有可能是代碼運行後動態獲取的,這個時候這個自定義的View一加載就會報錯了。
這裏解決起來也很簡單,加一個判空就可以了,如果不爲空才執行後面的操作。我就不單獨貼代碼了,直接將全部代碼貼出來
@SuppressLint("AppCompatCustomView")
public class ZoomImageView extends ImageView implements View.OnTouchListener {
public class ZoomMode{
public final static int Ordinary=0;
public final static int ZoomIn=1;
public final static int TowFingerZoom = 2;
}
private Matrix matrix;
//imageView的大小
private PointF viewSize;
//圖片的大小
private PointF imageSize;
//縮放後圖片的大小
private PointF scaleSize = new PointF();
//最初的寬高的縮放比例
private PointF originScale = new PointF();
//imageview中bitmap的xy實時座標
private PointF bitmapOriginPoint = new PointF();
//點擊的點
private PointF clickPoint = new PointF();
//設置的雙擊檢查時間限制
private long doubleClickTimeSpan = 250;
//上次點擊的時間
private long lastClickTime = 0;
//雙擊放大的倍數
private int doubleClickZoom = 2;
//當前縮放的模式
private int zoomInMode = ZoomMode.Ordinary;
//臨時座標比例數據
private PointF tempPoint = new PointF();
//最大縮放比例
private float maxScrole = 4;
//兩點之間的距離
private float doublePointDistance = 0;
//雙指縮放時候的中心點
private PointF doublePointCenter = new PointF();
//兩指縮放的比例
private float doubleFingerScrole = 0;
//上次觸碰的手指數量
private int lastFingerNum = 0;
public ZoomImageView(Context context) {
super(context);
init();
}
public ZoomImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public ZoomImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
setOnTouchListener(this);
setScaleType(ScaleType.MATRIX);
matrix = new Matrix();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
viewSize = new PointF(width,height);
Drawable drawable = getDrawable();
if (drawable != null){
imageSize = new PointF(drawable.getMinimumWidth(),drawable.getMinimumHeight());
showCenter();
}
}
/**
* 設置圖片居中等比顯示
*/
private void showCenter(){
float scalex = viewSize.x/imageSize.x;
float scaley = viewSize.y/imageSize.y;
float scale = scalex<scaley?scalex:scaley;
scaleImage(new PointF(scale,scale));
//移動圖片,並保存最初的圖片左上角(即原點)所在座標
if (scalex<scaley){
translationImage(new PointF(0,viewSize.y/2 - scaleSize.y/2));
bitmapOriginPoint.x = 0;
bitmapOriginPoint.y = viewSize.y/2 - scaleSize.y/2;
}else {
translationImage(new PointF(viewSize.x/2 - scaleSize.x/2,0));
bitmapOriginPoint.x = viewSize.x/2 - scaleSize.x/2;
bitmapOriginPoint.y = 0;
}
//保存下最初的縮放比例
originScale.set(scale,scale);
doubleFingerScrole = scale;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
//手指按下事件
//記錄被點擊的點的座標
clickPoint.set(event.getX(),event.getY());
//判斷屏幕上此時被按住的點的個數,當前屏幕只有一個點被點擊的時候觸發
if (event.getPointerCount() == 1) {
//設置一個點擊的間隔時長,來判斷是不是雙擊
if (System.currentTimeMillis() - lastClickTime <= doubleClickTimeSpan) {
//如果圖片此時縮放模式是普通模式,就觸發雙擊放大
if (zoomInMode == ZoomMode.Ordinary) {
//分別記錄被點擊的點到圖片左上角x,y軸的距離與圖片x,y軸邊長的比例,
//方便在進行縮放後,算出這個點對應的座標點
tempPoint.set((clickPoint.x - bitmapOriginPoint.x) / scaleSize.x,
(clickPoint.y - bitmapOriginPoint.y) / scaleSize.y);
//進行縮放
scaleImage(new PointF(originScale.x * doubleClickZoom,
originScale.y * doubleClickZoom));
//獲取縮放後,圖片左上角的xy座標
getBitmapOffset();
//平移圖片,使得被點擊的點的位置不變。這裏是計算縮放後被點擊的xy座標,
//與原始點擊的位置的xy座標值,計算出差值,然後做平移動作
translationImage(
new PointF(
clickPoint.x - (bitmapOriginPoint.x + tempPoint.x * scaleSize.x),
clickPoint.y - (bitmapOriginPoint.y + tempPoint.y * scaleSize.y))
);
zoomInMode = ZoomMode.ZoomIn;
doubleFingerScrole = originScale.x*doubleClickZoom;
} else {
//雙擊還原
showCenter();
zoomInMode = ZoomMode.Ordinary;
}
} else {
lastClickTime = System.currentTimeMillis();
}
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
//屏幕上已經有一個點按住 再按下一點時觸發該事件
//計算最初的兩個手指之間的距離
doublePointDistance = getDoubleFingerDistance(event);
break;
case MotionEvent.ACTION_POINTER_UP:
//屏幕上已經有兩個點按住 再鬆開一點時觸發該事件
//當有一個手指離開屏幕後,就修改狀態,這樣如果雙擊屏幕就能恢復到初始大小
zoomInMode = ZoomMode.ZoomIn;
//記錄此時的雙指縮放比例
doubleFingerScrole =scaleSize.x/imageSize.x;
//記錄此時屏幕觸碰的點的數量
lastFingerNum = 1;
//判斷縮放後的比例,如果小於最初的那個比例,就恢復到最初的大小
if (scaleSize.x<viewSize.x && scaleSize.y<viewSize.y){
zoomInMode = ZoomMode.Ordinary;
showCenter();
}
break;
case MotionEvent.ACTION_MOVE:
//手指移動時觸發事件
/**************************************移動
*******************************************/
if (zoomInMode != ZoomMode.Ordinary) {
//如果是多指,計算中心點爲假設的點擊的點
float currentX = 0;
float currentY = 0;
//獲取此時屏幕上被觸碰的點有多少個
int pointCount = event.getPointerCount();
//計算出中間點所在的座標
for (int i = 0; i < pointCount; i++) {
currentX += event.getX(i);
currentY += event.getY(i);
}
currentX /= pointCount;
currentY /= pointCount;
//當屏幕被觸碰的點的數量變化時,將最新算出來的中心點看作是被點擊的點
if (lastFingerNum != event.getPointerCount()) {
clickPoint.x = currentX;
clickPoint.y = currentY;
lastFingerNum = event.getPointerCount();
}
//將移動手指時,實時計算出來的中心點座標,減去被點擊點的座標就得到了需要移動的距離
float moveX = currentX - clickPoint.x;
float moveY = currentY - clickPoint.y;
//計算邊界,使得不能已出邊界,但是如果是雙指縮放時移動,因爲存在縮放效果,
//所以此時的邊界判斷無效
float[] moveFloat = moveBorderDistance(moveX, moveY);
//處理移動圖片的事件
translationImage(new PointF(moveFloat[0], moveFloat[1]));
clickPoint.set(currentX, currentY);
}
/**************************************縮放
*******************************************/
//判斷當前是兩個手指接觸到屏幕才處理縮放事件
if (event.getPointerCount() == 2){
//如果此時縮放後的大小,大於等於了設置的最大縮放的大小,就不處理
if ((scaleSize.x/imageSize.x >= originScale.x * maxScrole
|| scaleSize.y/imageSize.y >= originScale.y * maxScrole)
&& getDoubleFingerDistance(event) - doublePointDistance > 0){
break;
}
//這裏設置當雙指縮放的的距離變化量大於50,並且當前不是在雙指縮放狀態下,就計算中心點,等一些操作
if (Math.abs(getDoubleFingerDistance(event) - doublePointDistance) > 50
&& zoomInMode != ZoomMode.TowFingerZoom){
//計算兩個手指之間的中心點,當作放大的中心點
doublePointCenter.set((event.getX(0) + event.getX(1))/2,
(event.getY(0) + event.getY(1))/2);
//將雙指的中心點就假設爲點擊的點
clickPoint.set(doublePointCenter);
//下面就和雙擊放大基本一樣
getBitmapOffset();
//分別記錄被點擊的點到圖片左上角x,y軸的距離與圖片x,y軸邊長的比例,
//方便在進行縮放後,算出這個點對應的座標點
tempPoint.set((clickPoint.x - bitmapOriginPoint.x)/scaleSize.x,
(clickPoint.y - bitmapOriginPoint.y)/scaleSize.y);
//設置進入雙指縮放狀態
zoomInMode = ZoomMode.TowFingerZoom;
}
//如果已經進入雙指縮放狀態,就直接計算縮放的比例,並進行位移
if (zoomInMode == ZoomMode.TowFingerZoom){
//用當前的縮放比例與此時雙指間距離的縮放比例相乘,就得到對應的圖片應該縮放的比例
float scrole =
doubleFingerScrole*getDoubleFingerDistance(event)/doublePointDistance;
//這裏也是和雙擊放大時一樣的
scaleImage(new PointF(scrole,scrole));
getBitmapOffset();
translationImage(
new PointF(
clickPoint.x - (bitmapOriginPoint.x + tempPoint.x*scaleSize.x),
clickPoint.y - (bitmapOriginPoint.y + tempPoint.y*scaleSize.y))
);
}
}
break;
case MotionEvent.ACTION_UP:
//手指鬆開時觸發事件
Log.e("kzg","***********************ACTION_UP");
lastFingerNum = 0;
break;
}
return true;
}
public void scaleImage(PointF scaleXY){
matrix.setScale(scaleXY.x,scaleXY.y);
scaleSize.set(scaleXY.x * imageSize.x,scaleXY.y * imageSize.y);
setImageMatrix(matrix);
}
/**
* 對圖片進行x和y軸方向的平移
* @param pointF
*/
public void translationImage(PointF pointF){
matrix.postTranslate(pointF.x,pointF.y);
setImageMatrix(matrix);
}
/**
* 防止移動圖片超過邊界,計算邊界情況
* @param moveX
* @param moveY
* @return
*/
public float[] moveBorderDistance(float moveX,float moveY){
//計算bitmap的左上角座標
getBitmapOffset();
Log.e("kzg","**********************moveBorderDistance--
bitmapOriginPoint:"+bitmapOriginPoint);
//計算bitmap的右下角座標
float bitmapRightBottomX = bitmapOriginPoint.x + scaleSize.x;
float bitmapRightBottomY = bitmapOriginPoint.y + scaleSize.y;
if (moveY > 0){
//向下滑
if (bitmapOriginPoint.y + moveY > 0){
if (bitmapOriginPoint.y < 0){
moveY = -bitmapOriginPoint.y;
}else {
moveY = 0;
}
}
}else if (moveY < 0){
//向上滑
if (bitmapRightBottomY + moveY < viewSize.y){
if (bitmapRightBottomY > viewSize.y){
moveY = -(bitmapRightBottomY - viewSize.y);
}else {
moveY = 0;
}
}
}
if (moveX > 0){
//向右滑
if (bitmapOriginPoint.x + moveX > 0){
if (bitmapOriginPoint.x < 0){
moveX = -bitmapOriginPoint.x;
}else {
moveX = 0;
}
}
}else if (moveX < 0){
//向左滑
if (bitmapRightBottomX + moveX < viewSize.x){
if (bitmapRightBottomX > viewSize.x){
moveX = -(bitmapRightBottomX - viewSize.x);
}else {
moveX = 0;
}
}
}
return new float[]{moveX,moveY};
}
/**
* 獲取view中bitmap的座標點
*/
public void getBitmapOffset(){
float[] value = new float[9];
float[] offset = new float[2];
Matrix imageMatrix = getImageMatrix();
imageMatrix.getValues(value);
offset[0] = value[2];
offset[1] = value[5];
bitmapOriginPoint.set(offset[0],offset[1]);
}
/**
* 計算零個手指間的距離
* @param event
* @return
*/
public static float getDoubleFingerDistance(MotionEvent event){
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return (float)Math.sqrt(x * x + y * y) ;
}
}
至此這個簡單的自定義View就完成了,文中很多實現都不是最優的實現思路,但是是普通人最容易理解的思路,在此歡迎大家指出文中的不足,如果有更優的方案歡迎交流。
本文GitHub地址 https://github.com/Destroyer716/ZoomImageTest