【Android 内存优化】自定义组件长图组件 ( 长图滚动区域解码 | 手势识别 GestureDetector | 滑动计算类 Scroller | 代码示例 )
文章目錄
- 一、GestureDetector 創建與設置
- 二、GestureDetector 觸摸事件傳遞
- 三、觸摸滑動操作
- 四、慣性滑動操作
- 五、長圖滑動組件代碼示例
- 六、運行效果
- 七、源碼及資源下載
官方文檔 API : BitmapRegionDecoder
在【Android 內存優化】自定義組件長圖組件 ( 獲取圖像寬高 | 計算解碼區域 | 設置圖像解碼屬性 復用 像素格式 | 圖像繪制 ) 博客中完成了圖像的區域解碼 , 并顯示在界面中 ; 本篇博客中主要完成長圖滑動功能 , 觸摸滑動 , 慣性滑動 , 操作 ;
一、GestureDetector 創建與設置
1 . 自定義組件中設置手勢識別類 :
① 手勢監聽器實現 : 自定義組件實現 GestureDetector.OnGestureListener 接口 , 并重寫 onDown , onShowPress , onSingleTapUp , onScroll , onLongPress , onFling 五個方法 ;
② 觸摸監聽器 : 自定義組件實現 OnTouchListener 觸摸監聽器 , 并重寫 onTouch 方法 ;
③ 創建手勢識別對象 : 創建 GestureDetector 對象 , 傳入本組件作為手勢監聽器 ;
mGestureDetector = new GestureDetector(context, this);④ 為組件設置觸摸監聽器 : 為本自定義組件設置觸摸監聽器 ;
setOnTouchListener(this);2 . 代碼示例 :
/*** 長圖展示自定義 View 組件**/ public class LongImageView extends View implements GestureDetector.OnGestureListener, View.OnTouchListener {public static final String TAG = "LongImageView";/*** 手勢識別*/private GestureDetector mGestureDetector;/*** 滑動類*/private Scroller mScroller;public LongImageView(Context context) {this(context, null, 0);}public LongImageView(Context context, @Nullable AttributeSet attrs) {this(context, attrs, 0);}public LongImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);// 手勢識別mGestureDetector = new GestureDetector(context, this);// 設置觸摸監聽器setOnTouchListener(this);// 滑動輔助類mScroller = new Scroller(context);}@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)public LongImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);}@Overridepublic void computeScroll() {}/*下面的方法是手勢識別監聽器實現的方法*/@Overridepublic boolean onDown(MotionEvent e) {return true;}@Overridepublic void onShowPress(MotionEvent e) {}@Overridepublic boolean onSingleTapUp(MotionEvent e) {return false;}/*** 手指滑動事件, 此時手指沒有離開屏蔽** 隨著滾動 , 改變圖片的解碼區域 ;** @param e1 滑動的起始按下事件 DOWN 事件* @param e2 當前事件 MOVE 事件* @param distanceX 水平方向移動距離* @param distanceY 垂直方向移動距離* @return*/@Overridepublic boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {return false;}@Overridepublic void onLongPress(MotionEvent e) {}/*** 慣性滑動** @param e1* @param e2* @param velocityX x 方向速度* @param velocityY y 方向速度* @return*/@Overridepublic boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {return false;}/*下面的方法是觸摸監聽器實現方法*/@Overridepublic boolean onTouch(View v, MotionEvent event) {// 將觸摸事件交給手勢處理return mGestureDetector.onTouchEvent(event);} }二、GestureDetector 觸摸事件傳遞
1 . 觸摸事件傳遞給 GestureDetector : 在 View.OnTouchListener 觸摸監聽器的 onTouch 觸摸回調方法中 , 將觸摸事件傳遞給 mGestureDetector 處理 ;
@Overridepublic boolean onTouch(View v, MotionEvent event) {// 將觸摸事件交給手勢處理return mGestureDetector.onTouchEvent(event);}2 . 傳遞按下后事件 : 在 GestureDetector.OnGestureListener 監聽器中的 onDown 方法中 , 要將返回值設置成 false , 此時事件才能傳遞下去 ;
@Overridepublic boolean onDown(MotionEvent e) {// 觸摸按下 , 此處注意 , 如果想要接收后續事件 , 此時需要設置成 true 返回值return true;}三、觸摸滑動操作
1 . 觸摸滑動操作 :
① onScroll 方法 : 觸摸滑動主要在 GestureDetector.OnGestureListener 監聽器中的 onScroll 方法中實現 , 該方法是觸摸滑動事件 , 手指全程沒有離開屏幕 ;
② 區域解碼操作 : 調用 mRect.offset 方法 , 重新設置解碼區域 , 該方法可以移動 x 軸 , y 軸的解碼 ,
-
向上滑動分析 : 當向上滑動時 , 觸摸坐標由大變小 , distanceY 小于 0 , 應的圖片也向上滑動 , 解碼區域的 top 和 bottom 減小 ;
-
向下滑動分析 : 當向下滑動時 , 觸摸坐標由小變大 , distanceY 大于 0 , 對應的圖片也向下滑動 , 解碼區域的 top 和 bottom 增加 ;
③ 解碼區域限制 : 解碼的最底部不能超過圖片高度 , 解碼的最頂部不能小于 0 ; 分別針對這兩種情況進行各種限制 ;
if(mRect.bottom >= mImageHeight){mRect.bottom = mImageHeight;mRect.top = (int) (mImageHeight - mViewHeight / mScale);}if(mRect.bottom <= 0){mRect.top = 0;mRect.bottom = (int) (mViewHeight / mScale);}④ 目的完成 : 該方法的目的就是重新計算 Rect 圖像解碼區域 , 計算好之后 , 調用 invalidate 方法 , 最終會在 onDraw 方法中解碼 Rect 區域圖片 , 并顯示到自定義組件中 ;
2 . 代碼示例
/*** 手指滑動事件, 此時手指沒有離開屏蔽** 隨著滾動 , 改變圖片的解碼區域 ;** @param e1 滑動的起始按下事件 DOWN 事件* @param e2 當前事件 MOVE 事件* @param distanceX 水平方向移動距離* @param distanceY 垂直方向移動距離* @return*/@Overridepublic boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {/*重新設置解碼區域 , 該方法可以移動 x 軸 , y 軸的解碼當向上滑動時 , 觸摸坐標由大變小 , distanceY 小于 0 ,對應的圖片也向上滑動 , 解碼區域的 top 和 bottom 減小 ;當向下滑動時 , 觸摸坐標由小變大 , distanceY 大于 0 ,對應的圖片也向下滑動 , 解碼區域的 top 和 bottom 增加 ;*/mRect.offset(0, (int) distanceY);/*高度都不能超出范圍*/if(mRect.bottom >= mImageHeight){mRect.bottom = mImageHeight;mRect.top = (int) (mImageHeight - mViewHeight / mScale);}if(mRect.bottom <= 0){mRect.top = 0;mRect.bottom = (int) (mViewHeight / mScale);}// 重新繪制組件invalidate();return false;}四、慣性滑動操作
慣性滑動需要借助 Scroller 進行輔助計算 ;
1 . Scroller 創建 : 在自定義組件的構造函數中創建 Scroller 對象;
mScroller = new Scroller(context);2 . 慣性滑動回調方法 : 當發生慣性滑動時 , 此時手指已經離開屏幕 , 會自動回調 GestureDetector.OnGestureListener 監聽器的 onFling 方法 , 主要在這個方法中根據監聽到的速度值 , 計算慣性滑動的量 ;
3 . 慣性滑動計算 : 調用 Scroller 的 fling 方法 , 進行計算 , 在某時刻可以調用 Scroller 對象的 getCurrY 獲取當前滑動到了哪里 ;
/*** 慣性滑動** @param e1* @param e2* @param velocityX x 方向速度* @param velocityY y 方向速度* @return*/@Overridepublic boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {/*使用 Scroller 輔助計算滑動距離這里使用 Scroller 計算 mRect 區域的 top 值*/mScroller.fling(0, mRect.top, // x , y 起始位置0, (int) -velocityY, // x , y 速度0, 0, // x 的最小值和最大值0, (int) (mImageHeight - mViewHeight / mScale)); // y 的最小值和最大值return false;}4 . 設置慣性滑動區域 : 慣性滑動后 , View 組件的 computeScroll 方法會自動回調 , 在這里計算 區域解碼的 Rect 區域 , 計算完成后重繪組件 ;
/*** View 組件方法 , 父容器請求子容器更新其 mScrollX 和 mScrollY 值*/@Overridepublic void computeScroll() {// 如果 Scroller 計算慣性滑動結束 , 就不再計算if(mScroller.isFinished()){return;}// 動畫還在繼續執行if(mScroller.computeScrollOffset()) {mRect.top = mScroller.getCurrY();mRect.bottom = mRect.top + (int) (mViewHeight / mScale);// 重新繪制組件invalidate();}}五、長圖滑動組件代碼示例
package kim.hsl.lgl;import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapRegionDecoder; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Rect; import android.os.Build; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.widget.Scroller;import androidx.annotation.Nullable; import androidx.annotation.RequiresApi;import java.io.IOException; import java.io.InputStream;/*** 長圖展示自定義 View 組件**/ public class LongImageView extends View implements GestureDetector.OnGestureListener, View.OnTouchListener {public static final String TAG = "LongImageView";/*** 矩形區域*/private Rect mRect;/*** Bitmap 解碼選項*/private BitmapFactory.Options mOptions;/*** 圖片寬度*/private int mImageWidth;/*** 圖片高度*/private int mImageHeight;/*** 組件寬度*/private int mViewWidth;/*** 組件高度*/private int mViewHeight;/*** 圖像區域解碼器*/private BitmapRegionDecoder mBitmapRegionDecoder;/*** 顯示的 Bitmap 圖像*/private Bitmap mBitmap;/*** 圖片解析的縮放因子*/private float mScale;/*** 手勢識別*/private GestureDetector mGestureDetector;/*** 滑動類*/private Scroller mScroller;/*** 代碼中創建組件調用該方法* @param context View 組件運行的上下文對象 , 一般是 Activity ,* 可以通過該上下獲取當前主題 , 資源等*/public LongImageView(Context context) {this(context, null, 0);}/*** 布局文件中使用組件調用該方法 ;* 當 View 組件從 XML 布局文件中構造時 , 調用該方法* 提供的 AttributeSet 屬性在 XML 文件中指定 ;* 該方法使用默認的風格 defStyleAttr = 0 ,* 該組件的屬性設置只有 Context 中的主題和 XML 中的屬性 ;** @param context View 組件運行的上下文環境 ,* 通過該對象可以獲取當前主題 , 資源等* @param attrs XML 布局文件中的 View 組件標簽中的屬性值*/public LongImageView(Context context, @Nullable AttributeSet attrs) {this(context, attrs, 0);}/*** 布局文件中加載組件 , 并提供一個主題屬性風格 ;* View 組件使用該構造方法 , 從布局中加載時 , 允許使用一個特定風格 ;* 如 : 按鈕類的構造函數會傳入 defStyleAttr = R.attr.buttonStyle 風格作為參數 ;** @param context View 組件運行的上下文環境 ,* 通過該對象可以獲取當前主題 , 資源等* @param attrs XML 布局文件中的 View 組件標簽中的屬性值* @param defStyleAttr 默認的 Style 風格* 當前的應用 Application 或 Activity 設置了風格主題后 , 才生效*/public LongImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);// 解碼區域mRect = new Rect();// 解碼選項mOptions = new BitmapFactory.Options();// 手勢識別mGestureDetector = new GestureDetector(context, this);// 設置觸摸監聽器setOnTouchListener(this);// 滑動輔助類mScroller = new Scroller(context);}/*** 布局文件中加載組件 , 并提供一個主題屬性屬性 , 或風格資源 ;* 該構造方法允許組件在加載時使用自己的風格 ;** 屬性設置優先級 ( 優先級從高到低 )* 1. 布局文件中的標簽屬性 AttributeSet* 2. defStyleAttr 指定的默認風格* 3. defStyleRes 指定的默認風格* 4. 主題的屬性值** @param context View 組件運行的上下文環境 ,* 通過該對象可以獲取當前主題 , 資源等* @param attrs XML 布局文件中的 View 組件標簽中的屬性值* @param defStyleAttr 默認的 Style 風格* 當前的應用 Application 或 Activity 設置了風格主題后 , 才生效* @param defStyleRes style 資源的 id 標識符 , 提供組件的默認值 ,* 只有當 defStyleAttr 參數是 0 時 , 或者主題中沒有 style 設置 ;* 默認可以設置成 0 ;*/@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)public LongImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);}/*** 設置顯示的圖片* @param inputStream*/public void setImage(InputStream inputStream){// 讀取圖片的尺寸數據mOptions.inJustDecodeBounds = true;// 解碼圖片 , 圖片相關的尺寸數據保存到了 mOptions 選項中BitmapFactory.decodeStream(inputStream, null, mOptions);// 獲取圖片寬高mImageWidth = mOptions.outWidth;mImageHeight = mOptions.outHeight;// 設置 Bitmap 內存復用mOptions.inMutable = true; // 設置可變mOptions.inPreferredConfig = Bitmap.Config.RGB_565; // 設置像素格式 RGB 565mOptions.inJustDecodeBounds = false; // 讀取完畢之后, 就需要解析實際的 Bitmap 圖像數據了try {// Bitmap 區域解碼器mBitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);} catch (IOException e) {e.printStackTrace();}// 設置圖片完畢后 , 刷新自定義組件requestLayout();}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);// 獲取測量的自定義 View 組件寬高mViewWidth = getMeasuredWidth();mViewHeight = getMeasuredHeight();// 根據組件的寬高 , 確定要加載的圖像的寬高if(mBitmapRegionDecoder != null){mRect.left = 0;mRect.top = 0;// 繪制的寬度就是圖像的寬度mRect.right = mImageWidth;// 根據圖像寬度 和 組件寬度 , 計算出縮放比例// 組件寬度 / 圖像寬度 = 縮放因子mScale = (float)mViewWidth / (float)mImageWidth;/*加載的圖像高度寬度 , 與組件的高度寬度比例一致mViewWidth / 加載的圖像寬度 = mViewHeight / 加載的圖像高度此處加載的圖像寬度就是實際的寬度加載的圖像高度 = mViewHeight / ( mViewWidth / 加載的圖像寬度 )mViewWidth / 加載的圖像寬度 就是縮放因子加載的圖像高度 = mViewHeight / 縮放因子*/// 根據縮放因子計算解碼高度mRect.bottom = (int) (mViewHeight / mScale);}}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);if(mBitmapRegionDecoder == null) return;// 內存復用mOptions.inBitmap = mBitmap;// 解碼圖片mBitmap = mBitmapRegionDecoder.decodeRegion(mRect, mOptions);// 設置繪制的圖像縮放 , x 軸和 y 軸都在 Bitmap 大小的區域基礎上 , 縮放 mScale 倍Matrix matrix = new Matrix();matrix.setScale(mScale, mScale);canvas.drawBitmap(mBitmap, matrix, null);}/*** View 組件方法 , 父容器請求子容器更新其 mScrollX 和 mScrollY 值*/@Overridepublic void computeScroll() {// 如果 Scroller 計算慣性滑動結束 , 就不再計算if(mScroller.isFinished()){return;}// 動畫還在繼續執行if(mScroller.computeScrollOffset()) {mRect.top = mScroller.getCurrY();mRect.bottom = mRect.top + (int) (mViewHeight / mScale);// 重新繪制組件invalidate();}}/*下面的方法是手勢識別監聽器實現的方法*/@Overridepublic boolean onDown(MotionEvent e) {// 觸摸按下之后 , 就不能在滑動了 , 如果圖片還在按之前的慣性滑動 , 此時需要強行終止滑動if(!mScroller.isFinished()){// 強制終止 Scroller 滑動mScroller.forceFinished(true);}// 觸摸按下 , 此處注意 , 如果想要接收后續事件 , 此時需要設置成 true 返回值return true;}@Overridepublic void onShowPress(MotionEvent e) {}@Overridepublic boolean onSingleTapUp(MotionEvent e) {return false;}/*** 手指滑動事件, 此時手指沒有離開屏蔽** 隨著滾動 , 改變圖片的解碼區域 ;** @param e1 滑動的起始按下事件 DOWN 事件* @param e2 當前事件 MOVE 事件* @param distanceX 水平方向移動距離* @param distanceY 垂直方向移動距離* @return*/@Overridepublic boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {/*重新設置解碼區域 , 該方法可以移動 x 軸 , y 軸的解碼當向上滑動時 , 觸摸坐標由大變小 , distanceY 小于 0 ,對應的圖片也向上滑動 , 解碼區域的 top 和 bottom 減小 ;當向下滑動時 , 觸摸坐標由小變大 , distanceY 大于 0 ,對應的圖片也向下滑動 , 解碼區域的 top 和 bottom 增加 ;*/mRect.offset(0, (int) distanceY);/*高度都不能超出范圍*/if(mRect.bottom >= mImageHeight){mRect.bottom = mImageHeight;mRect.top = (int) (mImageHeight - mViewHeight / mScale);}if(mRect.bottom <= 0){mRect.top = 0;mRect.bottom = (int) (mViewHeight / mScale);}// 重新繪制組件invalidate();return false;}@Overridepublic void onLongPress(MotionEvent e) {}/*** 慣性滑動** @param e1* @param e2* @param velocityX x 方向速度* @param velocityY y 方向速度* @return*/@Overridepublic boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {/*使用 Scroller 輔助計算滑動距離這里使用 Scroller 計算 mRect 區域的 top 值*/mScroller.fling(0, mRect.top, // x , y 起始位置0, (int) -velocityY, // x , y 速度0, 0, // x 的最小值和最大值0, (int) (mImageHeight - mViewHeight / mScale)); // y 的最小值和最大值return false;}/*下面的方法是觸摸監聽器實現方法*/@Overridepublic boolean onTouch(View v, MotionEvent event) {// 將觸摸事件交給手勢處理return mGestureDetector.onTouchEvent(event);} }
六、運行效果
橫屏長圖滾動效果 :
豎屏長圖滾動效果 :
七、源碼及資源下載
源碼及資源下載地址 :
-
① GitHub 工程地址 : Long_Graph_Loading
-
② LongImageView.java 主界面代碼地址 : LongImageView.java , 這是上述示自定義組件代碼 ;
總結
以上是生活随笔為你收集整理的【Android 内存优化】自定义组件长图组件 ( 长图滚动区域解码 | 手势识别 GestureDetector | 滑动计算类 Scroller | 代码示例 )的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【Android 内存优化】自定义组件长
- 下一篇: 【Android 电量优化】电量优化 (