[一千个类] | Touch事件到Matrix变换的转换器
- 关键词:矩阵约束,任务分配
- 本文约1400字(不含代码),建议阅读时间15分钟,
需要知识背景:移动设备的交互界面编程经验
移动客户端界面开发时不可避免地需要处理Touch事件。在Android中,Touch事件相应的类叫<code>MotionEvent</code>。通过追踪<code>MotionEvent</code>的变化并将这些变化映射为UI控件的交互操作,也就是我们俗称的“手势”,相信各位都有过这样的编程经验。在实现这类交互逻辑的时候,我们最常做的一件事是写一个自定义的<code>View</code>子类,重写它的<code>onTouchEvent(MotionEvent event)</code>方法,然后在这个方法里实现各种<code>MotionEvent</code>事件到UI控件属性的转换,这期间常常还伴随着<code>android.graphics.Matrix</code>的使用。有没有想过,这里面其实有能够复用的逻辑可供封装。文猫君在本文提供一种思路,可以让我们减少在<code>onTouchEvent</code>方法里重复书写相同或者相似的代码的体力活。
矩阵约束
首先引入一个概念叫矩阵约束,文猫君在这里用这个表述来指代操作一个<code>Matrix</code>的过程中需要满足的一些数学关系。矩阵约束类的设计大致如下:(为了缩减篇幅,方法体和部分注释排版做了省略。)
/**
* 操作一个矩阵时要满足的约束条件
*/
public class MatrixTransformConstraint {
/** 旋转中心点的x坐标 */
private float pivotX = 0.f;
/** 旋转中心点的y坐标 */
private float pivotY = 0.f;
/** 矩阵空间的缩放比,用于保证原始矩阵与投射矩阵之间按照各自的空间大小同比的变换 */
private float spaceScale = 1.f;
/** 矩阵缩放支持的最大值 */
private float maxScale = 4f;
/** 矩阵缩放支持的最小值 */
private float minScale = 0.5f;
/** 矩阵缩放如果超出限定值是否需要回弹到限定值 */
private boolean reboundToLimitedScale = true;
/** 旋转中心允许活动的范围 */
protected RectF pivotAccessibleBound = new RectF();
/** 矩阵约束的名称,用于具体场景中方便标识和索引 */
public String name;
/**
* 设置矩阵的旋转中心
*
* @param pivotX 旋转中心点的x坐标
* @param pivotY 旋转中心点的y坐标
*
* @return 矩阵约束
*/
public MatrixTransformConstraint setPivot(float pivotX, float pivotY);
/**
* 设置矩阵缩放的最大限定值
*
* @param maxScale 矩阵缩放的最大限定值
*
* @return 矩阵约束
*/
public MatrixTransformConstraint setMaxScale(float maxScale);
/**
* 设置矩阵缩放的最小限定值
*
* @param minScale 矩阵缩放的最小限定值
*
* @return 矩阵约束
*/
public MatrixTransformConstraint setMinScale(float minScale);
/**
* 设置是否需要回弹到限定值
*
* @param reboundToLimitedScale 是否需要回弹到限定值
*
* @return 矩阵约束
*/
public MatrixTransformConstraint setReboundToLimitedScale(boolean reboundToLimitedScale);
/**
* 以缩放的方式投射出另一个矩阵约束
*
* @param dstInfo 要投射的矩阵约束
* @param scale 投射执行的缩放
*/
public void scaleRejection(MatrixTransformConstraint dstInfo, float scale);
/**
* 设置矩阵空间的缩放比
*
* @param spaceScale 矩阵空间的缩放比
*
* @return 矩阵约束
*/
public MatrixTransformConstraint setSpaceScale(float spaceScale);
/** 获取矩阵空间到屏幕的投射比例,即矩阵空间的缩放比 */
public float getSpaceScaleToTouchScreen();
/** 获取矩阵旋转中心点的X坐标 */
public float getPivotX();
/** 获取矩阵旋转中心点的Y坐标 */
public float getPivotY();
/** 获取矩阵缩放的最大限定值 */
public float getMaxScale();
/** 获取矩阵缩放的最小限定值 */
public float getMinScale();
/** 获取矩阵是否需要回弹到限定值 */
public boolean isReboundToLimitedScale();
/**
* 设置矩阵旋转中心点可以活动的区域
*
* @param pivotAccessibleBound 矩阵旋转中心点可以活动的区域
*/
public void setPivotAccessibleBound(RectF pivotAccessibleBound);
/** 确保矩阵旋转中心点在既定范围内 */
public void ensureInsideAccessibleBound();
}
为什么会有这个类的设计呢?因为约束是客观存在的,并且同时满足可抽象,数量有限两个特点。所以我们可以把这些约束固化下来,以便复用。稍后在说明Touch事件到Matrix的转换逻辑时会再体现约束的用途。
我们定义出了矩阵约束,要如何应用在矩阵操作中呢?不妨阅读一下下面这段代码,它是以矩阵约束的写法来实现矩阵平移的变换。
// 从经过简单解析的Touch事件中取得原始的X,Y位移量
float rawDisplacementX = touchEventParsed.getDisplacementX();
float rawDisplacementY = touchEventParsed.getDisplacementY();
// 根据位移量判断位置是否发生变化
boolean positionChanged = (rawDisplacementX != 0 || rawDisplacementY != 0);
// 如果位置发生变化,我们将对某一个Matrix做平移变换
if (positionChanged) {
// 取得这个Matrix,至于它是怎么存放的这里先不关心
Matrix matrix = getMatrix();
// 取得这个Matrix对应的约束,约束是怎么存放的这里也先不关心
MatrixTransformConstraint constraint = getConstraint();
// 通过约束取得旧的中心点
float oldPivotX = constraint.getPivotX();
float oldPivotY = constraint.getPivotY();
// 通过Touch事件的原始位移量乘以约束中矩阵空间和触摸屏的空间比来计算出要映射到矩阵中的位移量
float newDisplacementX = oldPivotX + rawDisplacementX * constraint.getSpaceScaleToTouchScreen();
float newDisplacementY = oldPivotY + rawDisplacementY * constraint.getSpaceScaleToTouchScreen();
// 矩阵发生了平移变换,它的中心点自然也要跟着平移
constraint.setPivot(newDisplacementX, newDisplacementY);
// 检查新的中心点是否还在受限的范围里面,比如我们约定中心点是不能被移出屏幕之外的
constraint.ensureInsideAccessibleBound();
// 综合中心点受限条件重新修正矩阵位移量
float revisedDisplacementX = constraint.getPivotX() - oldPivotX;
float revisedDisplacementY = constraint.getPivotY() - oldPivotY;
// 平移变换矩阵
matrix.postTranslate(revisedDisplacementX, revisedDisplacementY);
}
把矩阵变换相关的数学关系封装到矩阵约束里,我们就可以不用重复书写这些变量啦。平移涉及的是最简单的加减计算,对应的手势一般就是单指滑动。此外我们常用的矩阵操作还有缩放和旋转,对应的手势有Pinch和Zoom。看过平移的代码,聪明的程序员读者应该也能想到,缩放和旋转用矩阵约束来书写要怎么实现。
Touch事件到Matrix变换的转换器
当我们可以清楚地描述矩阵变换需要满足的数学关系,也就是前面定义的矩阵约束的时候,我们只需要再满足一个预设前提,矩阵变换这项任务将会变成可委托的。在这个委托关系中,委托者是程序员,因为他不想重复书写代码,而被委托方就是接下来将要实现的转换器。那么需要额外满足的预设前提又是什么呢? 我们来分析看看,转换器的输入端是touch事件,输出端是对矩阵执行的变换,其中的变数包括之前定义的矩阵变换需要满足的数学关系—矩阵约束,以及—touch事件被映射为一个矩阵变换的具体规则。以整体的视角来观察,在具体的场景下,每一次这样的映射过程都会有各自差异的地方,所以我们中的许多人没有去复用代码,而是在需要的时候单独去书写一遍<code>onTouchEvent</code>的处理逻辑。
但是我们知道,在单一的变换中,比如平移,旋转,缩放,touch事件和这些变换的映射关系几乎可以说是约定成俗的,这也正是我们常说的“手势”。而基于简单的数学原理,我们发现复合的映射关系,其实是可以拆分成各个独立的手势单独工作的。它对应的是矩阵的乘法。
让我们把变换的任务拆分成一个个单一的变换,并且为每一个变换设置约束。当所有这些变换都作用于同一个矩阵时,就可以组合出复合的效果。
/** Touch事件到矩阵变换的转换器 */
public abstract class TouchEventToMatrixTransform {
/** 转换器链表,包含了当前转换器所有前置的转换器,通过矩阵乘法将转换器协同起来达到复合的效果 */
protected List<TouchEventToMatrixTransform> mPrecursoryTransforms;
/** 矩阵变换任务, 是一个矩阵变换约束到矩阵的映射表 */
protected HashMap<MatrixTransformConstraint, Matrix> mMatrixTransformAssignments = new HashMap<>;
public TouchEventToMatrixTransform() {
mPrecursoryTransforms = new ArrayList<>();
}
/**
* 判定当前的转换器中是否已经包含给定的矩阵约束
*
* @param matrixTransformConstraint 矩阵约束
* @return 是否已经包含给定的矩阵约束
*/
public boolean containsMatrixTransform(MatrixTransformConstraint matrixTransformConstraint) {
return mMatrixTransformAssignments.containsKey(matrixTransformConstraint);
}
/**
* 为自己指派矩阵变换的任务
*
* @param matrix 要执行变换的矩阵
* @param constraint 变换时需要遵守的约束
*/
void assignMatrixTransformInner(Matrix matrix, MatrixTransformConstraint constraint) {
if (matrix == null || constraint == null) {
return;
}
if (!containsMatrixTransform(constraint)) {
mMatrixTransformAssignments.put(constraint, matrix);
}
}
/**
* 为自己及所有前置的转换器指派矩阵变换的任务
*
* @param matrix 要执行变换的矩阵
* @param constraint 变换时需要遵守的约束
*/
public void assignMatrixTransform(Matrix matrix, MatrixTransformConstraint constraint) {
if (matrix == null || constraint == null) {
return;
}
// 为自己指派矩阵变换任务
assignMatrixTransformInner(matrix, constraint);
// 为协同工作的所有前置转换器指派同一个任务(包含约束),怎么执行任务是各转换器自己控制的
if (mPrecursoryTransforms != null && !mPrecursoryTransforms.isEmpty()) {
for (int i = mPrecursoryTransforms.size() - 1; i >= 0; i--) {
TouchEventToMatrixTransform precursor = mPrecursoryTransforms.get(i);
if (precursor != null) {
precursor.assignMatrixTransformInner(matrix, constraint);
}
}
}
}
/**
* 增加前置的touch事件到矩阵变换的转换器
*
* @param precursoryTransform 前置的转换器
* @return 当前touch事件到矩阵变换的转换器
*/
public TouchEventToMatrixTransform appendPrecursoryTransform(TouchEventToMatrixTransform precursoryTransform) {
for (MatrixTransformConstraint constraint : mMatrixTransformAssignments.keySet()) {
precursoryTransform.assignMatrixTransformInner(mMatrixTransformAssignments.get(constraint), constraint);
}
mPrecursoryTransforms.add(precursoryTransform);
return this;
}
/**
* @link { MotionEvent#ACTION_DOWN } 事件的处理过程
*/
public boolean parse_ACTION_DOWN(@NonNull MotionEvent event,
@NonNull TouchEventParsed touchState) { ... }
/**
* @link { MotionEvent#ACTION_POINTER_DOWN } 事件的处理过程
*/
public boolean parse_ACTION_POINTER_DOWN(@NonNull MotionEvent event,
@NonNull TouchEventParsed touchState) { ... }
/**
* @link { MotionEvent#ACTION_MOVE } 事件的处理过程
*
* @param event MotionEvent事件
* @param touchState 经过简单解析,得出一些数学关系的MotionEvent事件的衍生数据
*
* @return 是否处理了MotionEvent事件并对矩阵做了变换
*/
public boolean parse_ACTION_MOVE(@NonNull MotionEvent event,
@NonNull TouchEventParsed touchState) {
// 标记是否应用了有效的变换
boolean transformed = false;
if (mPrecursoryTransforms != null && !mPrecursoryTransforms.isEmpty()) {
for (int i = mPrecursoryTransforms.size() - 1; i >= 0; i--) {
TouchEventToMatrixTransform precursor = mPrecursoryTransforms.get(i);
if (precursor != null) {
if (precursor.parse_ACTION_MOVE(event, touchState)) {
transformed = true;
}
}
}
}
return transformed;
}
/**
* @link { MotionEvent#ACTION_POINTER_UP } 事件的处理过程
*/
public boolean parse_ACTION_POINTER_UP(@NonNull MotionEvent event,
@NonNull TouchEventParsed touchState) { ... }
/**
* @link { MotionEvent#ACTION_UP } 事件的处理过程
*/
public boolean touchEdit_ACTION_UP(@NonNull MotionEvent event,
@NonNull TouchEventParsed touchState) { ... }
}
基于这个抽象类转换器的基础设计,我们可以具体实现如下几个单一矩阵变换的转换器,它们是前面提到的平移,缩放和旋转变换。简单展开平移的转换器,具体内部如何实现可以回顾第一节矩阵约束,其他转换器的实现略过不表,有兴趣的读者可以自己尝试一下。
/** Touch事件到平移变换的转换器 */
class TouchEventToTranslate extends TouchEventToMatrixTransform {
/** 是否支持双指操作来平移 */
private boolean mSupportDualTouchTranslate = false;
@Override
public boolean parse_ACTION_MOVE(@NonNull MotionEvent event, @NonNull TouchEventParsed touchState) {
// 关键逻辑,使得链接起来的转换器能够协同完成复合的矩阵变换任务
boolean superChanged = super.parse_ACTION_MOVE(event, touchState);
if (mSupportDualTouchTranslate || !touchState.isDualTouch()) {
float rawDisplacementX = touchState.getDisplacementX();
float rawDisplacementY = touchState.getDisplacementY();
boolean positionChanged = (rawDisplacementX != 0 || rawDisplacementY != 0);
if (positionChanged) {
// 参考第一节中演示的基于矩阵约束的平移变换实现
}
return positionChanged || superChanged;
}
return superChanged;
}
}
/** Touch事件到缩放变换的转换器 */
class TouchEventToScale extends TouchEventToMatrixTransform;
/** Touch事件到旋转变换的转换器 */
class TouchEventToRotate extends TouchEventToMatrixTransform;
// 一个具体场景中的组合实现的Touch事件到矩阵变换的转换器,支持平移,旋转和缩放
TouchEventToMatrixTransform mTranslator = new TouchEventToTranslate()
.appendPrecursoryTransform(new TouchEventToRotate())
.appendPrecursoryTransform(new TouchEventToScale());
// 给转换器分配矩阵变换任务,需要保证矩阵缩放不会超过8倍大,8分之一小
mTranslator.assignMatrixTransform(someImageView.getContentMatrix(),
new MatrixTransformConstraint("foo").setMaxScale(8f).setMinScale(0.125f));
然后到了真正使用的环节,采用转换器实现的代码是长下面这个样子的。转换器一次书写可以多处使用,再也不用书写又长又臭的<code>onTouchEvent</code>方法体啦。
@Override
public boolean onTouchEvent(MotionEvent event) {
TouchEventParsed touchState = mTouchParser.parseMotionEvent(event);
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mTranslator.parse_ACTION_DOWN(event, touchState);
invalidate();
break;
case MotionEvent.ACTION_POINTER_DOWN:
mTranslator.parse_ACTION_POINTER_DOWN(event, touchState);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
mTranslator.parse_ACTION_MOVE(event, touchState);
invalidate();
break;
case MotionEvent.ACTION_POINTER_UP:
mTranslator.parse_ACTION_POINTER_UP(event, touchState);
invalidate();
break;
case MotionEvent.ACTION_UP:
mTranslator.parse_ACTION_UP(event, touchState);
invalidate();
break;
case MotionEvent.ACTION_CANCEL:
mTranslator.parse_ACTION_UP(event, touchState);
invalidate();
break;
}
return super.onTouchEvent(event);
}
我是文猫君,这里是“一千个类”系列,欢迎你来围观,指正或者赞赏。