【Android 內存優化】自定義組件長圖組件 ( 長圖滾動區域解碼 | 手勢識別 GestureDetector | 滑動計算類 Scroller | 代碼示例 )



官方文檔 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);
    }
	
    @Override
    public void computeScroll() {
    }



    /*
        下面的方法是手勢識別監聽器實現的方法
     */


    @Override
    public boolean onDown(MotionEvent e) {
        return true;
    }

    @Override
    public void onShowPress(MotionEvent e) {

    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    /**
     * 手指滑動事件, 此時手指沒有離開屏蔽
     *
     * 隨着滾動 , 改變圖片的解碼區域 ;
     *
     * @param e1 滑動的起始按下事件 DOWN 事件
     * @param e2 當前事件 MOVE 事件
     * @param distanceX 水平方向移動距離
     * @param distanceY 垂直方向移動距離
     * @return
     */
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {

    }

    /**
     * 慣性滑動
     *
     * @param e1
     * @param e2
     * @param velocityX x 方向速度
     * @param velocityY y 方向速度
     * @return
     */
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return false;
    }



    /*
        下面的方法是觸摸監聽器實現方法
     */

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        // 將觸摸事件交給手勢處理
        return mGestureDetector.onTouchEvent(event);
    }
}




二、GestureDetector 觸摸事件傳遞



1 . 觸摸事件傳遞給 GestureDetector : 在 View.OnTouchListener 觸摸監聽器的 onTouch 觸摸回調方法中 , 將觸摸事件傳遞給 mGestureDetector 處理 ;

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        // 將觸摸事件交給手勢處理
        return mGestureDetector.onTouchEvent(event);
    }

2 . 傳遞按下後事件 :GestureDetector.OnGestureListener 監聽器中的 onDown 方法中 , 要將返回值設置成 false , 此時事件才能傳遞下去 ;

    @Override
    public 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
     */
    @Override
    public 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
     */
    @Override
    public 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 值
     */
    @Override
    public 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 565
        mOptions.inJustDecodeBounds = false; // 讀取完畢之後, 就需要解析實際的 Bitmap 圖像數據了

        try {
            // Bitmap 區域解碼器
            mBitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 設置圖片完畢後 , 刷新自定義組件
        requestLayout();
    }

    @Override
    protected 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);
        }
    }


    @Override
    protected 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 值
     */
    @Override
    public void computeScroll() {
        // 如果 Scroller 計算慣性滑動結束 , 就不再計算
        if(mScroller.isFinished()){
            return;
        }

        // 動畫還在繼續執行
        if(mScroller.computeScrollOffset()) {
            mRect.top = mScroller.getCurrY();
            mRect.bottom = mRect.top + (int) (mViewHeight / mScale);
            // 重新繪製組件
            invalidate();
        }
    }



    /*
        下面的方法是手勢識別監聽器實現的方法
     */


    @Override
    public boolean onDown(MotionEvent e) {
        // 觸摸按下之後 , 就不能在滑動了 , 如果圖片還在按之前的慣性滑動 , 此時需要強行終止滑動
        if(!mScroller.isFinished()){
            // 強制終止 Scroller 滑動
            mScroller.forceFinished(true);
        }
        // 觸摸按下 , 此處注意 , 如果想要接收後續事件 , 此時需要設置成 true 返回值
        return true;
    }

    @Override
    public void onShowPress(MotionEvent e) {

    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    /**
     * 手指滑動事件, 此時手指沒有離開屏蔽
     *
     * 隨着滾動 , 改變圖片的解碼區域 ;
     *
     * @param e1 滑動的起始按下事件 DOWN 事件
     * @param e2 當前事件 MOVE 事件
     * @param distanceX 水平方向移動距離
     * @param distanceY 垂直方向移動距離
     * @return
     */
    @Override
    public 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;
    }

    @Override
    public void onLongPress(MotionEvent e) {

    }

    /**
     * 慣性滑動
     *
     * @param e1
     * @param e2
     * @param velocityX x 方向速度
     * @param velocityY y 方向速度
     * @return
     */
    @Override
    public 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;
    }



    /*
        下面的方法是觸摸監聽器實現方法
     */

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        // 將觸摸事件交給手勢處理
        return mGestureDetector.onTouchEvent(event);
    }
}





六、運行效果



橫屏長圖滾動效果 :

在這裏插入圖片描述


豎屏長圖滾動效果 :

在這裏插入圖片描述





七、源碼及資源下載



源碼及資源下載地址 :

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章