文章目录

  • 一、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 | 代码示例 )相关推荐

  1. 【Android 内存优化】自定义组件长图组件 ( 获取图像宽高 | 计算解码区域 | 设置图像解码属性 复用 像素格式 | 图像绘制 )

    文章目录 一.获取图像真实宽高 二.计算解码区域 三.设置解码参数 内存复用 像素格式 四.图像绘制 五.执行效果 六.源码及资源下载 官方文档 API : BitmapRegionDecoder 在 ...

  2. 深入探索Android内存优化

    前言 成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~. 本篇是Android内存优化的进阶篇,难度会比较大,建议对内存优化不是非常熟悉的前仔细看看在 ...

  3. Android内存优化汇总

    写在最前: 本文的思路主要借鉴了2014年AnDevCon开发者大会的一个演讲PPT,加上把网上搜集的各种内存零散知识点进行汇总.挑选.简化后整理而成. 所以我将本文定义为一个工具类的文章,如果你在A ...

  4. 深入探索Android内存优化(炼狱级别)

    本文由 jsonchao投稿 微信:bcce5360 前言 成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~. 本篇是 Android 内存优化的进阶 ...

  5. 关于Android内存优化

    介绍 在Android系统中,内存分配与释放分配在一定程度上会影响App性能的-鉴于其使用的是类似于Java的GC回收机制,因此系统会以消耗一定的效率为代价,进行垃圾回收. 在中国有句老话:" ...

  6. 深入探索 Android 内存优化(炼狱级别)

    前言 成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~. 本篇是 Android 内存优化的进阶篇,难度可以说达到了炼狱级别,建议对内存优化不是非常熟 ...

  7. ANDROID内存优化(大汇总——中)

    转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! 写在最前: 本文的思路主要借鉴了2014年AnDevCon开发者大会的一个演讲PPT,加上 ...

  8. 【Android 内存优化】内存抖动 ( 垃圾回收算法总结 | 分代收集算法补充 | 内存抖动排查 | 内存抖动操作 | 集合选择 )

    文章目录 一. 垃圾回收算法总结 二. 分代收集算法补充 三. 查看 Java 虚拟机 四. 获取 Android 应用可使用最大内存 五. 内存抖动标志 六. 排查内存抖动 七. 常见的造成内存抖动 ...

  9. 【腾讯Bugly干货分享】Android内存优化总结实践

    本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:https://mp.weixin.qq.com/s/2MsEAR9pQfMr1Sfs7cPdWQ 导语 智 ...

最新文章

  1. ASP.net远程调试笔记
  2. Linux之CentOS安装composer与git
  3. 九宫格C语言递归程序,[置顶] C语言递归实现N宫格(九宫格)源码
  4. 【208天】黑马程序员27天视频学习笔记【Day21-中】
  5. r生成新的dataframe_2020-08-11R语言中dataframe与list的转换方法
  6. MySQL系列(一) MySQL体系结构概述
  7. 深度优化LNMP之Nginx [1]
  8. python-双层嵌套循环-打印小星星
  9. [转]程序员真实写真:35岁前成功的12条黄金法则
  10. sed修炼系列(三):sed高级应用之实现窗口滑动技术
  11. python save保存图片到本地_python爬取网站上的图片并保存到本地
  12. JLink重刷固件(win7/win8/win10亲测可用)
  13. Yolov4部署到ZYNQ系列1-USB转UART驱动不在COM和LPT显示问题的解决方案
  14. 用计算机怎么按e,在计算器上e的多少次方怎样按
  15. android qq 功能,Android-类qq功能(一)
  16. win10远程桌面_怎么选择Win10系统版本?家庭版与专业版的对比介绍
  17. HIT-哈工大数据结构-作业5(C++)
  18. Linux系统编程 50 -stat和stat函数 穿透和非穿透
  19. 通信类会议期刊排名(转)
  20. Python 并行编程教程 | Lynda教程 中文字幕

热门文章

  1. 响应键盘delete键的删除功能
  2. 转 在b/s开发中经常用到的javaScript技术
  3. legend3---PHP使用阿里云短信服务
  4. Android性能测试-分析工具
  5. python测试开发django-35.xadmin注册表信息
  6. table表格固定前几列,其余的滚动
  7. 【代码笔记】Web-JavaScript-JavaScript调试
  8. [改善Java代码]适时选择不同的线程池来实现
  9. 与html相关的知识点整理
  10. Swift项目,超美的动画和tableView,collectionView,轮播图的使用,网络请求的封装等