Android多點觸控技術(shù)實戰(zhàn) 針對圖片自由縮放和移動
在上一篇文章中我?guī)е蠹乙黄饘崿F(xiàn)了Android瀑布流照片墻的效果,雖然這種效果很炫很酷,但其實還只能算是一個半成品,因為照片墻中所有的圖片都是只能看不能點的。因此本篇文章中,我們就來對這一功能進(jìn)行完善,加入點擊圖片就能瀏覽大圖的功能,并且在瀏覽大圖的時候還可以通過多點觸控的方式對圖片進(jìn)行縮放。
如果你還沒有看過 Android瀑布流照片墻實現(xiàn),體驗不規(guī)則排列的美感 這篇文章,請盡量先去閱讀完再來看本篇文章,因為這次的代碼完全是在上次的基礎(chǔ)上進(jìn)行開發(fā)的。
那我們現(xiàn)在就開始動手吧,首先打開上次的PhotoWallFallsDemo項目,在里面加入一個ZoomImageView類,這個類就是用于進(jìn)行大圖展示和多點觸控縮放的,代碼如下所示:
public class ZoomImageView extends View { /** * 初始化狀態(tài)常量 */ public static final int STATUS_INIT = 1; /** * 圖片放大狀態(tài)常量 */ public static final int STATUS_ZOOM_OUT = 2; /** * 圖片縮小狀態(tài)常量 */ public static final int STATUS_ZOOM_IN = 3; /** * 圖片拖動狀態(tài)常量 */ public static final int STATUS_MOVE = 4; /** * 用于對圖片進(jìn)行移動和縮放變換的矩陣 */ private Matrix matrix = new Matrix(); /** * 待展示的Bitmap對象 */ private Bitmap sourceBitmap; /** * 記錄當(dāng)前操作的狀態(tài),可選值為STATUS_INIT、STATUS_ZOOM_OUT、STATUS_ZOOM_IN和STATUS_MOVE */ private int currentStatus; /** * ZoomImageView控件的寬度 */ private int width; /** * ZoomImageView控件的高度 */ private int height; /** * 記錄兩指同時放在屏幕上時,中心點的橫坐標(biāo)值 */ private float centerPointX; /** * 記錄兩指同時放在屏幕上時,中心點的縱坐標(biāo)值 */ private float centerPointY; /** * 記錄當(dāng)前圖片的寬度,圖片被縮放時,這個值會一起變動 */ private float currentBitmapWidth; /** * 記錄當(dāng)前圖片的高度,圖片被縮放時,這個值會一起變動 */ private float currentBitmapHeight; /** * 記錄上次手指移動時的橫坐標(biāo) */ private float lastXMove = -1; /** * 記錄上次手指移動時的縱坐標(biāo) */ private float lastYMove = -1; /** * 記錄手指在橫坐標(biāo)方向上的移動距離 */ private float movedDistanceX; /** * 記錄手指在縱坐標(biāo)方向上的移動距離 */ private float movedDistanceY; /** * 記錄圖片在矩陣上的橫向偏移值 */ private float totalTranslateX; /** * 記錄圖片在矩陣上的縱向偏移值 */ private float totalTranslateY; /** * 記錄圖片在矩陣上的總縮放比例 */ private float totalRatio; /** * 記錄手指移動的距離所造成的縮放比例 */ private float scaledRatio; /** * 記錄圖片初始化時的縮放比例 */ private float initRatio; /** * 記錄上次兩指之間的距離 */ private double lastFingerDis; /** * ZoomImageView構(gòu)造函數(shù),將當(dāng)前操作狀態(tài)設(shè)為STATUS_INIT。 * * @param context * @param attrs */ public ZoomImageView(Context context, AttributeSet attrs) { super(context, attrs); currentStatus = STATUS_INIT; } /** * 將待展示的圖片設(shè)置進(jìn)來。 * * @param bitmap * 待展示的Bitmap對象 */ public void setImageBitmap(Bitmap bitmap) { sourceBitmap = bitmap; invalidate(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (changed) { // 分別獲取到ZoomImageView的寬度和高度 width = getWidth(); height = getHeight(); } } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getActionMasked()) { case MotionEvent.ACTION_POINTER_DOWN: if (event.getPointerCount() == 2) { // 當(dāng)有兩個手指按在屏幕上時,計算兩指之間的距離 lastFingerDis = distanceBetweenFingers(event); } break; case MotionEvent.ACTION_MOVE: if (event.getPointerCount() == 1) { // 只有單指按在屏幕上移動時,為拖動狀態(tài) float xMove = event.getX(); float yMove = event.getY(); if (lastXMove == -1 && lastYMove == -1) { lastXMove = xMove; lastYMove = yMove; } currentStatus = STATUS_MOVE; movedDistanceX = xMove - lastXMove; movedDistanceY = yMove - lastYMove; // 進(jìn)行邊界檢查,不允許將圖片拖出邊界 if (totalTranslateX + movedDistanceX > 0) { movedDistanceX = 0; } else if (width - (totalTranslateX + movedDistanceX) > currentBitmapWidth) { movedDistanceX = 0; } if (totalTranslateY + movedDistanceY > 0) { movedDistanceY = 0; } else if (height - (totalTranslateY + movedDistanceY) > currentBitmapHeight) { movedDistanceY = 0; } // 調(diào)用onDraw()方法繪制圖片 invalidate(); lastXMove = xMove; lastYMove = yMove; } else if (event.getPointerCount() == 2) { // 有兩個手指按在屏幕上移動時,為縮放狀態(tài) centerPointBetweenFingers(event); double fingerDis = distanceBetweenFingers(event); if (fingerDis > lastFingerDis) { currentStatus = STATUS_ZOOM_OUT; } else { currentStatus = STATUS_ZOOM_IN; } // 進(jìn)行縮放倍數(shù)檢查,最大只允許將圖片放大4倍,最小可以縮小到初始化比例 if ((currentStatus == STATUS_ZOOM_OUT && totalRatio < 4 * initRatio) || (currentStatus == STATUS_ZOOM_IN && totalRatio > initRatio)) { scaledRatio = (float) (fingerDis / lastFingerDis); totalRatio = totalRatio * scaledRatio; if (totalRatio > 4 * initRatio) { totalRatio = 4 * initRatio; } else if (totalRatio < initRatio) { totalRatio = initRatio; } // 調(diào)用onDraw()方法繪制圖片 invalidate(); lastFingerDis = fingerDis; } } break; case MotionEvent.ACTION_POINTER_UP: if (event.getPointerCount() == 2) { // 手指離開屏幕時將臨時值還原 lastXMove = -1; lastYMove = -1; } break; case MotionEvent.ACTION_UP: // 手指離開屏幕時將臨時值還原 lastXMove = -1; lastYMove = -1; break; default: break; } return true; } /** * 根據(jù)currentStatus的值來決定對圖片進(jìn)行什么樣的繪制操作。 */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); switch (currentStatus) { case STATUS_ZOOM_OUT: case STATUS_ZOOM_IN: zoom(canvas); break; case STATUS_MOVE: move(canvas); break; case STATUS_INIT: initBitmap(canvas); default: canvas.drawBitmap(sourceBitmap, matrix, null); break; } } /** * 對圖片進(jìn)行縮放處理。 * * @param canvas */ private void zoom(Canvas canvas) { matrix.reset(); // 將圖片按總縮放比例進(jìn)行縮放 matrix.postScale(totalRatio, totalRatio); float scaledWidth = sourceBitmap.getWidth() * totalRatio; float scaledHeight = sourceBitmap.getHeight() * totalRatio; float translateX = 0f; float translateY = 0f; // 如果當(dāng)前圖片寬度小于屏幕寬度,則按屏幕中心的橫坐標(biāo)進(jìn)行水平縮放。否則按兩指的中心點的橫坐標(biāo)進(jìn)行水平縮放 if (currentBitmapWidth < width) { translateX = (width - scaledWidth) / 2f; } else { translateX = totalTranslateX * scaledRatio + centerPointX * (1 - scaledRatio); // 進(jìn)行邊界檢查,保證圖片縮放后在水平方向上不會偏移出屏幕 if (translateX > 0) { translateX = 0; } else if (width - translateX > scaledWidth) { translateX = width - scaledWidth; } } // 如果當(dāng)前圖片高度小于屏幕高度,則按屏幕中心的縱坐標(biāo)進(jìn)行垂直縮放。否則按兩指的中心點的縱坐標(biāo)進(jìn)行垂直縮放 if (currentBitmapHeight < height) { translateY = (height - scaledHeight) / 2f; } else { translateY = totalTranslateY * scaledRatio + centerPointY * (1 - scaledRatio); // 進(jìn)行邊界檢查,保證圖片縮放后在垂直方向上不會偏移出屏幕 if (translateY > 0) { translateY = 0; } else if (height - translateY > scaledHeight) { translateY = height - scaledHeight; } } // 縮放后對圖片進(jìn)行偏移,以保證縮放后中心點位置不變 matrix.postTranslate(translateX, translateY); totalTranslateX = translateX; totalTranslateY = translateY; currentBitmapWidth = scaledWidth; currentBitmapHeight = scaledHeight; canvas.drawBitmap(sourceBitmap, matrix, null); } /** * 對圖片進(jìn)行平移處理 * * @param canvas */ private void move(Canvas canvas) { matrix.reset(); // 根據(jù)手指移動的距離計算出總偏移值 float translateX = totalTranslateX + movedDistanceX; float translateY = totalTranslateY + movedDistanceY; // 先按照已有的縮放比例對圖片進(jìn)行縮放 matrix.postScale(totalRatio, totalRatio); // 再根據(jù)移動距離進(jìn)行偏移 matrix.postTranslate(translateX, translateY); totalTranslateX = translateX; totalTranslateY = translateY; canvas.drawBitmap(sourceBitmap, matrix, null); } /** * 對圖片進(jìn)行初始化操作,包括讓圖片居中,以及當(dāng)圖片大于屏幕寬高時對圖片進(jìn)行壓縮。 * * @param canvas */ private void initBitmap(Canvas canvas) { if (sourceBitmap != null) { matrix.reset(); int bitmapWidth = sourceBitmap.getWidth(); int bitmapHeight = sourceBitmap.getHeight(); if (bitmapWidth > width || bitmapHeight > height) { if (bitmapWidth - width > bitmapHeight - height) { // 當(dāng)圖片寬度大于屏幕寬度時,將圖片等比例壓縮,使它可以完全顯示出來 float ratio = width / (bitmapWidth * 1.0f); matrix.postScale(ratio, ratio); float translateY = (height - (bitmapHeight * ratio)) / 2f; // 在縱坐標(biāo)方向上進(jìn)行偏移,以保證圖片居中顯示 matrix.postTranslate(0, translateY); totalTranslateY = translateY; totalRatio = initRatio = ratio; } else { // 當(dāng)圖片高度大于屏幕高度時,將圖片等比例壓縮,使它可以完全顯示出來 float ratio = height / (bitmapHeight * 1.0f); matrix.postScale(ratio, ratio); float translateX = (width - (bitmapWidth * ratio)) / 2f; // 在橫坐標(biāo)方向上進(jìn)行偏移,以保證圖片居中顯示 matrix.postTranslate(translateX, 0); totalTranslateX = translateX; totalRatio = initRatio = ratio; } currentBitmapWidth = bitmapWidth * initRatio; currentBitmapHeight = bitmapHeight * initRatio; } else { // 當(dāng)圖片的寬高都小于屏幕寬高時,直接讓圖片居中顯示 float translateX = (width - sourceBitmap.getWidth()) / 2f; float translateY = (height - sourceBitmap.getHeight()) / 2f; matrix.postTranslate(translateX, translateY); totalTranslateX = translateX; totalTranslateY = translateY; totalRatio = initRatio = 1f; currentBitmapWidth = bitmapWidth; currentBitmapHeight = bitmapHeight; } canvas.drawBitmap(sourceBitmap, matrix, null); } } /** * 計算兩個手指之間的距離。 * * @param event * @return 兩個手指之間的距離 */ private double distanceBetweenFingers(MotionEvent event) { float disX = Math.abs(event.getX(0) - event.getX(1)); float disY = Math.abs(event.getY(0) - event.getY(1)); return Math.sqrt(disX * disX + disY * disY); } /** * 計算兩個手指之間中心點的坐標(biāo)。 * * @param event */ private void centerPointBetweenFingers(MotionEvent event) { float xPoint0 = event.getX(0); float yPoint0 = event.getY(0); float xPoint1 = event.getX(1); float yPoint1 = event.getY(1); centerPointX = (xPoint0 + xPoint1) / 2; centerPointY = (yPoint0 + yPoint1) / 2; } }
由于這個類是整個多點觸控縮放功能最核心的一個類,我在這里給大家詳細(xì)的講解一下。首先在ZoomImageView里我們定義了四種狀態(tài),STATUS_INIT、STATUS_ZOOM_OUT、STATUS_ZOOM_IN和STATUS_MOVE,這四個狀態(tài)分別代表初始化、放大、縮小和移動這幾個動作,然后在構(gòu)造函數(shù)里我們將當(dāng)前狀態(tài)置為初始化狀態(tài)。接著我們可以調(diào)用setImageBitmap()方法把要顯示的圖片對象傳進(jìn)去,這個方法會invalidate一下當(dāng)前的View,因此onDraw()方法就會得到執(zhí)行。然后在onDraw()方法里判斷出當(dāng)前的狀態(tài)是初始化狀態(tài),所以就會調(diào)用initBitmap()方法進(jìn)行初始化操作。
那我們就來看一下initBitmap()方法,在這個方法中首先對圖片的大小進(jìn)行了判斷,如果圖片的寬和高都是小于屏幕的寬和高的,則直接將這張圖片進(jìn)行偏移,讓它能夠居中顯示在屏幕上。如果圖片的寬度大于屏幕的寬度,或者圖片的高度大于屏幕的高度,則將圖片進(jìn)行等比例壓縮,讓圖片的的寬或高正好等同于屏幕的寬或高,保證在初始化狀態(tài)下圖片一定能完整地顯示出來。這里所有的偏移和縮放操作都是通過矩陣來完成的,我們把要縮放和偏移的值都存放在矩陣中,然后在繪制圖片的時候傳入這個矩陣對象就可以了。
圖片初始化完成之后,就可以對圖片進(jìn)行縮放處理了。這里在onTouchEvent()方法來對點擊事件進(jìn)行判斷,如果發(fā)現(xiàn)有兩個手指同時按在屏幕上(使用event.getPointerCount()判斷)就將當(dāng)前狀態(tài)置為縮放狀態(tài),并調(diào)用distanceBetweenFingers()來得到兩指之間的距離,以計算出縮放比例。然后invalidate一下,就會在onDraw()方法中就會調(diào)用zoom()方法。之后就在這個方法里根據(jù)當(dāng)前的縮放比例以及中心點的位置對圖片進(jìn)行縮放和偏移,具體的邏輯大家請仔細(xì)閱讀代碼,注釋已經(jīng)寫得非常清楚。
然后當(dāng)只有一個手指按在屏幕上時,就把當(dāng)前狀態(tài)置為移動狀態(tài),之后會對手指的移動距離進(jìn)行計算,并處理了邊界檢查的工作,以防止圖片偏移出屏幕。然后invalidate一下當(dāng)前的view,又會進(jìn)入到onDraw()方法中,這里判斷出當(dāng)前是移動狀態(tài),于是會調(diào)用move()方法。move()方法中的代碼非常簡單,就是根據(jù)手指移動的距離對圖片進(jìn)行偏移就可以了。
介紹完了ZoomImageView,然后我們新建一個布局image_details.xml,在布局中直接引用創(chuàng)建好的ZoomImageView:
<?xml version="1.0" encoding="utf-8"?> <com.example.photowallfallsdemo.ZoomImageView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/zoom_image_view" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#000000" > </com.example.photowallfallsdemo.ZoomImageView>
接著創(chuàng)建一個Activity,在這個Activity中來加載image_details布局。新建ImageDetailsActivity,代碼如下所示:
public class ImageDetailsActivity extends Activity { private ZoomImageView zoomImageView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.image_details); zoomImageView = (ZoomImageView) findViewById(R.id.zoom_image_view); String imagePath = getIntent().getStringExtra("image_path"); Bitmap bitmap = BitmapFactory.decodeFile(imagePath); zoomImageView.setImageBitmap(bitmap); } }
可以看到,首先我們獲取到了ZoomImageView的實例,然后又通過Intent得到了需要展示的圖片路徑,接著使用BitmapFactory將路徑下的圖片加載到內(nèi)存中,然后調(diào)用ZoomImageView的setImageBitmap()方法將圖片傳入,就可以讓這張圖片展示出來了。
接下來我們需要考慮的,就是如何在照片墻上給圖片增加點擊事件,讓它能夠啟動ImageDetailsActivity了。其實這也很簡單,只需要在動態(tài)添加圖片的時候給每個ImageView的實例注冊一下點擊事件就好了,修改MyScrollView中addImage()方法的代碼,如下所示:
private void addImage(Bitmap bitmap, int imageWidth, int imageHeight) { LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(imageWidth, imageHeight); if (mImageView != null) { mImageView.setImageBitmap(bitmap); } else { ImageView imageView = new ImageView(getContext()); imageView.setLayoutParams(params); imageView.setImageBitmap(bitmap); imageView.setScaleType(ScaleType.FIT_XY); imageView.setPadding(5, 5, 5, 5); imageView.setTag(R.string.image_url, mImageUrl); imageView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(getContext(), ImageDetailsActivity.class); intent.putExtra("image_path", getImagePath(mImageUrl)); getContext().startActivity(intent); } }); findColumnToAdd(imageView, imageHeight).addView(imageView); imageViewList.add(imageView); } }
可以看到,這里我們調(diào)用了ImageView的setOnClickListener()方法來給圖片增加點擊事件,當(dāng)用戶點擊了照片墻中的任意圖片時,就會啟動ImageDetailsActivity,并將圖片的路徑傳遞過去。
由于我們添加了一個新的Activity,別忘了在AndroidManifest.xml文件里注冊一下:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.photowallfallsdemo" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="17" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.INTERNET" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name="com.example.photowallfallsdemo.MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name="com.example.photowallfallsdemo.ImageDetailsActivity" > </activity> </application> </manifest>
這樣所有的編碼工作就已經(jīng)完成了,現(xiàn)在我們運(yùn)行一下程序,又會看到熟悉的照片墻界面,點擊任意一張圖片會進(jìn)入到相應(yīng)的大圖界面,并且可以通過多點觸控的方式對圖片進(jìn)行縮放,放大后還可以通過單指來移動圖片,如下圖所示。
源碼下載:http://xiazai.jb51.net/201610/yuanma/androidPhotoWall(jb51.net).rar
好了,以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android開發(fā)ThreadPoolExecutor與自定義線程池詳解
這篇文章主要為大家介紹了Android開發(fā)ThreadPoolExecutor與自定義線程池詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11Android手機(jī)聯(lián)系人帶字母索引的快速查找
這篇文章主要為大家詳細(xì)介紹了Android手機(jī)聯(lián)系人帶字母索引的快速查找實現(xiàn)方法,感興趣的小伙伴們可以參考一下2016-03-03Android App數(shù)據(jù)格式Json解析方法和常見問題
JSON數(shù)據(jù)格式,在Android中被廣泛運(yùn)用于客戶端和網(wǎng)絡(luò)(或者說服務(wù)器)通信,非常有必要系統(tǒng)的了解學(xué)習(xí)。恰逢本人最近對json做了一個簡單的學(xué)習(xí),特此總結(jié)一下,以饗各位2014-03-03Android使用文件進(jìn)行數(shù)據(jù)存儲的方法
這篇文章主要介紹了Android使用文件進(jìn)行數(shù)據(jù)存儲的方法,較為詳細(xì)的分析了Android基于文件實現(xiàn)數(shù)據(jù)存儲所涉及的相關(guān)概念與使用技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-09-09ImageView點擊可變暗的實例代碼(android代碼技巧)
本文給大家分享一段實例代碼給大家介紹ImageView點擊可變暗的實例代碼,非常不錯,具有參考借鑒價值,需要的的朋友參考下吧2017-02-02Android實現(xiàn)listview動態(tài)加載數(shù)據(jù)分頁的兩種方法
這篇文章主要為大家詳細(xì)介紹了Android實現(xiàn)listview動態(tài)加載的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-06-06Android設(shè)置當(dāng)TextView中的文字超過TextView的容量時用省略號代替
這篇文章主要介紹了Android設(shè)置當(dāng)TextView中的文字超過TextView的容量時用省略號代替 ,需要的朋友可以參考下2017-03-03為Android Studio編寫自定義Gradle插件的教程
這篇文章主要介紹了為Android Studio編寫自定義Gradle插件的教程,Android Studio現(xiàn)在基本上已經(jīng)成為了安卓開發(fā)的標(biāo)配IDE,友可以參考下2016-02-02Android Service判斷設(shè)備聯(lián)網(wǎng)狀態(tài)詳解
本文主要介紹Android Service判斷聯(lián)網(wǎng)狀態(tài),這里提供了相關(guān)資料并附有示例代碼,有興趣的小伙伴可以參考下,幫助開發(fā)相關(guān)應(yīng)用功能2016-08-08Android開發(fā)實現(xiàn)ListView點擊展開收起效果示例
這篇文章主要介紹了Android開發(fā)實現(xiàn)ListView點擊展開收起效果,結(jié)合實例形式分析了Android ListView控件的布局及事件響應(yīng)相關(guān)操作技巧,需要的朋友可以參考下2019-03-03