官方文档 API : BitmapRegionDecoder
在【Android 内存优化】自定义组件长图组件 ( 获取图像宽高 | 计算解码区域 | 设置图像解码属性 复用 像素格式 | 图像绘制 ) 博客中完成了图像的区域解码 , 并显示在界面中 ; 本篇博客中主要完成长图滑动功能 , 触摸滑动 , 惯性滑动 , 操作 ;
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); } }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(); } }横屏长图滚动效果 :
竖屏长图滚动效果 :
源码及资源下载地址 :
① GitHub 工程地址 : Long_Graph_Loading
② LongImageView.java 主界面代码地址 : LongImageView.java , 这是上述示自定义组件代码 ;
