19 | * Each time a pointer gets pressed or released, the current gesture (if any) will end, and a new 20 | * one will be started (if there are still pressed pointers left). It is guaranteed that the number 21 | * of pointers within the single gesture will remain the same during the whole gesture. 22 | */ 23 | public class MultiPointerGestureDetector { 24 | 25 | /** The listener for receiving notifications when gestures occur. */ 26 | public interface Listener { 27 | /** Responds to the beginning of a gesture. */ 28 | public void onGestureBegin(MultiPointerGestureDetector detector); 29 | 30 | /** Responds to the update of a gesture in progress. */ 31 | public void onGestureUpdate(MultiPointerGestureDetector detector); 32 | 33 | /** Responds to the end of a gesture. */ 34 | public void onGestureEnd(MultiPointerGestureDetector detector); 35 | } 36 | 37 | private static final int MAX_POINTERS = 2; 38 | 39 | private boolean mGestureInProgress; 40 | private int mCount; 41 | private final int mId[] = new int[MAX_POINTERS]; 42 | private final float mStartX[] = new float[MAX_POINTERS]; 43 | private final float mStartY[] = new float[MAX_POINTERS]; 44 | private final float mCurrentX[] = new float[MAX_POINTERS]; 45 | private final float mCurrentY[] = new float[MAX_POINTERS]; 46 | 47 | private Listener mListener = null; 48 | 49 | public MultiPointerGestureDetector() { 50 | reset(); 51 | } 52 | 53 | /** Factory method that creates a new instance of MultiPointerGestureDetector */ 54 | public static MultiPointerGestureDetector newInstance() { 55 | return new MultiPointerGestureDetector(); 56 | } 57 | 58 | public void setListener(Listener listener) { 59 | mListener = listener; 60 | } 61 | 62 | /** 63 | * Resets the component to the initial state. 64 | */ 65 | public void reset() { 66 | mGestureInProgress = false; 67 | mCount = 0; 68 | for (int i = 0; i < MAX_POINTERS; i++) { 69 | mId[i] = MotionEvent.INVALID_POINTER_ID; 70 | } 71 | } 72 | 73 | protected boolean shouldStartGesture() { 74 | return true; 75 | } 76 | 77 | private void startGesture() { 78 | if (!mGestureInProgress) { 79 | mGestureInProgress = true; 80 | if (mListener != null) { 81 | mListener.onGestureBegin(this); 82 | } 83 | } 84 | } 85 | 86 | private void stopGesture() { 87 | if (mGestureInProgress) { 88 | mGestureInProgress = false; 89 | if (mListener != null) { 90 | mListener.onGestureEnd(this); 91 | } 92 | } 93 | } 94 | 95 | private int getPressedPointerIndex(MotionEvent event, int i) { 96 | final int count = event.getPointerCount(); 97 | final int action = event.getActionMasked(); 98 | final int index = event.getActionIndex(); 99 | if (action == MotionEvent.ACTION_UP || 100 | action == MotionEvent.ACTION_POINTER_UP) { 101 | if (i >= index) { 102 | i++; 103 | } 104 | } 105 | return (i < count) ? i : -1; 106 | } 107 | 108 | public boolean onTouchEvent(final MotionEvent event) { 109 | switch (event.getActionMasked()) { 110 | case MotionEvent.ACTION_MOVE: { 111 | // update pointers 112 | for (int i = 0; i < MAX_POINTERS; i++) { 113 | int index = event.findPointerIndex(mId[i]); 114 | if (index != -1) { 115 | mCurrentX[i] = event.getX(index); 116 | mCurrentY[i] = event.getY(index); 117 | } 118 | } 119 | // start a new gesture if not already started 120 | if (!mGestureInProgress && shouldStartGesture()) { 121 | startGesture(); 122 | } 123 | // notify listener 124 | if (mGestureInProgress && mListener != null) { 125 | mListener.onGestureUpdate(this); 126 | } 127 | break; 128 | } 129 | 130 | case MotionEvent.ACTION_DOWN: 131 | case MotionEvent.ACTION_POINTER_DOWN: 132 | case MotionEvent.ACTION_POINTER_UP: 133 | case MotionEvent.ACTION_UP: { 134 | // we'll restart the current gesture (if any) whenever the number of pointers changes 135 | // NOTE: we only restart existing gestures here, new gestures are started in ACTION_MOVE 136 | boolean wasGestureInProgress = mGestureInProgress; 137 | stopGesture(); 138 | reset(); 139 | // update pointers 140 | for (int i = 0; i < MAX_POINTERS; i++) { 141 | int index = getPressedPointerIndex(event, i); 142 | if (index == -1) { 143 | break; 144 | } 145 | mId[i] = event.getPointerId(index); 146 | mCurrentX[i] = mStartX[i] = event.getX(index); 147 | mCurrentY[i] = mStartY[i] = event.getY(index); 148 | mCount++; 149 | } 150 | // restart the gesture (if any) if there are still pointers left 151 | if (wasGestureInProgress && mCount > 0) { 152 | startGesture(); 153 | } 154 | break; 155 | } 156 | 157 | case MotionEvent.ACTION_CANCEL: { 158 | stopGesture(); 159 | reset(); 160 | break; 161 | } 162 | } 163 | return true; 164 | } 165 | 166 | /** Restarts the current gesture */ 167 | public void restartGesture() { 168 | if (!mGestureInProgress) { 169 | return; 170 | } 171 | stopGesture(); 172 | for (int i = 0; i < MAX_POINTERS; i++) { 173 | mStartX[i] = mCurrentX[i]; 174 | mStartY[i] = mCurrentY[i]; 175 | } 176 | startGesture(); 177 | } 178 | 179 | /** Gets whether gesture is in progress or not */ 180 | public boolean isGestureInProgress() { 181 | return mGestureInProgress; 182 | } 183 | 184 | /** Gets the number of pointers in the current gesture */ 185 | public int getCount() { 186 | return mCount; 187 | } 188 | 189 | /** 190 | * Gets the start X coordinates for the all pointers 191 | * Mutable array is exposed for performance reasons and is not to be modified by the callers. 192 | */ 193 | public float[] getStartX() { 194 | return mStartX; 195 | } 196 | 197 | /** 198 | * Gets the start Y coordinates for the all pointers 199 | * Mutable array is exposed for performance reasons and is not to be modified by the callers. 200 | */ 201 | public float[] getStartY() { 202 | return mStartY; 203 | } 204 | 205 | /** 206 | * Gets the current X coordinates for the all pointers 207 | * Mutable array is exposed for performance reasons and is not to be modified by the callers. 208 | */ 209 | public float[] getCurrentX() { 210 | return mCurrentX; 211 | } 212 | 213 | /** 214 | * Gets the current Y coordinates for the all pointers 215 | * Mutable array is exposed for performance reasons and is not to be modified by the callers. 216 | */ 217 | public float[] getCurrentY() { 218 | return mCurrentY; 219 | } 220 | } -------------------------------------------------------------------------------- /lib.lhh.fiv.library/src/lib.lhh.fiv/library/gestures/TransformGestureDetector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | package lib.lhh.fiv.library.gestures; 14 | 15 | import android.view.MotionEvent; 16 | 17 | /** 18 | * Component that detects translation, scale and rotation based on touch events. 19 | *
20 | * This class notifies its listeners whenever a gesture begins, updates or ends. 21 | * The instance of this detector is passed to the listeners, so it can be queried 22 | * for pivot, translation, scale or rotation. 23 | */ 24 | public class TransformGestureDetector implements MultiPointerGestureDetector.Listener { 25 | 26 | /** The listener for receiving notifications when gestures occur. */ 27 | public interface Listener { 28 | /** Responds to the beginning of a gesture. */ 29 | public void onGestureBegin(TransformGestureDetector detector); 30 | 31 | /** Responds to the update of a gesture in progress. */ 32 | public void onGestureUpdate(TransformGestureDetector detector); 33 | 34 | /** Responds to the end of a gesture. */ 35 | public void onGestureEnd(TransformGestureDetector detector); 36 | } 37 | 38 | private final MultiPointerGestureDetector mDetector; 39 | 40 | private Listener mListener = null; 41 | 42 | public TransformGestureDetector(MultiPointerGestureDetector multiPointerGestureDetector) { 43 | mDetector = multiPointerGestureDetector; 44 | mDetector.setListener(this); 45 | } 46 | 47 | /** Factory method that creates a new instance of TransformGestureDetector */ 48 | public static TransformGestureDetector newInstance() { 49 | return new TransformGestureDetector(MultiPointerGestureDetector.newInstance()); 50 | } 51 | 52 | public void setListener(Listener listener) { 53 | mListener = listener; 54 | } 55 | 56 | /** 57 | * Resets the component to the initial state. 58 | */ 59 | public void reset() { 60 | mDetector.reset(); 61 | } 62 | 63 | public boolean onTouchEvent(final MotionEvent event) { 64 | return mDetector.onTouchEvent(event); 65 | } 66 | 67 | @Override 68 | public void onGestureBegin(MultiPointerGestureDetector detector) { 69 | if (mListener != null) { 70 | mListener.onGestureBegin(this); 71 | } 72 | } 73 | 74 | @Override 75 | public void onGestureUpdate(MultiPointerGestureDetector detector) { 76 | if (mListener != null) { 77 | mListener.onGestureUpdate(this); 78 | } 79 | } 80 | 81 | @Override 82 | public void onGestureEnd(MultiPointerGestureDetector detector) { 83 | if (mListener != null) { 84 | mListener.onGestureEnd(this); 85 | } 86 | } 87 | 88 | private float calcAverage(float[] arr, int len) { 89 | float sum = 0; 90 | for (int i = 0; i < len; i++) { 91 | sum += arr[i]; 92 | } 93 | return (len > 0) ? sum / len : 0; 94 | } 95 | 96 | /** Restarts the current gesture */ 97 | public void restartGesture() { 98 | mDetector.restartGesture(); 99 | } 100 | 101 | /** Gets whether gesture is in progress or not */ 102 | public boolean isGestureInProgress() { 103 | return mDetector.isGestureInProgress(); 104 | } 105 | 106 | /** Gets the X coordinate of the pivot point */ 107 | public float getPivotX() { 108 | return calcAverage(mDetector.getStartX(), mDetector.getCount()); 109 | } 110 | 111 | /** Gets the Y coordinate of the pivot point */ 112 | public float getPivotY() { 113 | return calcAverage(mDetector.getStartY(), mDetector.getCount()); 114 | } 115 | 116 | /** Gets the X component of the translation */ 117 | public float getTranslationX() { 118 | return calcAverage(mDetector.getCurrentX(), mDetector.getCount()) - 119 | calcAverage(mDetector.getStartX(), mDetector.getCount()); 120 | } 121 | 122 | /** Gets the Y component of the translation */ 123 | public float getTranslationY() { 124 | return calcAverage(mDetector.getCurrentY(), mDetector.getCount()) - 125 | calcAverage(mDetector.getStartY(), mDetector.getCount()); 126 | } 127 | 128 | /** Gets the scale */ 129 | public float getScale() { 130 | if (mDetector.getCount() < 2) { 131 | return 1; 132 | } else { 133 | float startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0]; 134 | float startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0]; 135 | float currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0]; 136 | float currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0]; 137 | float startDist = (float) Math.hypot(startDeltaX, startDeltaY); 138 | float currentDist = (float) Math.hypot(currentDeltaX, currentDeltaY); 139 | return currentDist / startDist; 140 | } 141 | } 142 | 143 | /** Gets the rotation in radians */ 144 | public float getRotation() { 145 | if (mDetector.getCount() < 2) { 146 | return 0; 147 | } else { 148 | float startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0]; 149 | float startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0]; 150 | float currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0]; 151 | float currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0]; 152 | float startAngle = (float) Math.atan2(startDeltaY, startDeltaX); 153 | float currentAngle = (float) Math.atan2(currentDeltaY, currentDeltaX); 154 | return currentAngle - startAngle; 155 | } 156 | } 157 | } -------------------------------------------------------------------------------- /lib.lhh.fiv.library/src/lib.lhh.fiv/library/zoomable/DefaultZoomableController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | package lib.lhh.fiv.library.zoomable; 14 | 15 | import android.graphics.Matrix; 16 | import android.graphics.PointF; 17 | import android.graphics.RectF; 18 | import android.view.MotionEvent; 19 | 20 | import lib.lhh.fiv.library.gestures.TransformGestureDetector; 21 | 22 | /** 23 | * Zoomable controller that calculates transformation based on touch events. 24 | */ 25 | public class DefaultZoomableController 26 | implements ZoomableController, TransformGestureDetector.Listener { 27 | 28 | private TransformGestureDetector mGestureDetector; 29 | 30 | private Listener mListener = null; 31 | 32 | private boolean mIsEnabled = false; 33 | private boolean mIsRotationEnabled = false; 34 | private boolean mIsScaleEnabled = true; 35 | private boolean mIsTranslationEnabled = true; 36 | 37 | private float mMinScaleFactor = 1.0f; 38 | private float mMaxScaleFactor = Float.POSITIVE_INFINITY; 39 | 40 | private final RectF mViewBounds = new RectF(); 41 | private final RectF mImageBounds = new RectF(); 42 | private final RectF mTransformedImageBounds = new RectF(); 43 | private final Matrix mPreviousTransform = new Matrix(); 44 | private final Matrix mActiveTransform = new Matrix(); 45 | private final Matrix mActiveTransformInverse = new Matrix(); 46 | private final float[] mTempValues = new float[9]; 47 | 48 | public DefaultZoomableController(TransformGestureDetector gestureDetector) { 49 | mGestureDetector = gestureDetector; 50 | mGestureDetector.setListener(this); 51 | } 52 | 53 | public static DefaultZoomableController newInstance() { 54 | return new DefaultZoomableController(TransformGestureDetector.newInstance()); 55 | } 56 | 57 | @Override 58 | public void setListener(Listener listener) { 59 | mListener = listener; 60 | } 61 | 62 | /** Rests the controller. */ 63 | public void reset() { 64 | mGestureDetector.reset(); 65 | mPreviousTransform.reset(); 66 | mActiveTransform.reset(); 67 | } 68 | 69 | /** Sets whether the controller is enabled or not. */ 70 | @Override 71 | public void setEnabled(boolean enabled) { 72 | mIsEnabled = enabled; 73 | if (!enabled) { 74 | reset(); 75 | } 76 | } 77 | 78 | /** Returns whether the controller is enabled or not. */ 79 | @Override 80 | public boolean isEnabled() { 81 | return mIsEnabled; 82 | } 83 | 84 | /** Sets whether the rotation gesture is enabled or not. */ 85 | public void setRotationEnabled(boolean enabled) { 86 | mIsRotationEnabled = enabled; 87 | } 88 | 89 | /** Gets whether the rotation gesture is enabled or not. */ 90 | public boolean isRotationEnabled() { 91 | return mIsRotationEnabled; 92 | } 93 | 94 | /** Sets whether the scale gesture is enabled or not. */ 95 | public void setScaleEnabled(boolean enabled) { 96 | mIsScaleEnabled = enabled; 97 | } 98 | 99 | /** Gets whether the scale gesture is enabled or not. */ 100 | public boolean isScaleEnabled() { 101 | return mIsScaleEnabled; 102 | } 103 | 104 | /** Sets whether the translation gesture is enabled or not. */ 105 | public void setTranslationEnabled(boolean enabled) { 106 | mIsTranslationEnabled = enabled; 107 | } 108 | 109 | /** Gets whether the translations gesture is enabled or not. */ 110 | public boolean isTranslationEnabled() { 111 | return mIsTranslationEnabled; 112 | } 113 | 114 | /** Gets the image bounds before zoomable transformation is applied. */ 115 | public RectF getImageBounds() { 116 | return mImageBounds; 117 | } 118 | 119 | /** Sets the image bounds before zoomable transformation is applied. */ 120 | @Override 121 | public void setImageBounds(RectF imageBounds) { 122 | mImageBounds.set(imageBounds); 123 | } 124 | 125 | /** Gets the view bounds. */ 126 | public RectF getViewBounds() { 127 | return mViewBounds; 128 | } 129 | 130 | /** Sets the view bounds. */ 131 | @Override 132 | public void setViewBounds(RectF viewBounds) { 133 | mViewBounds.set(viewBounds); 134 | } 135 | 136 | /** Gets the minimum scale factor allowed. */ 137 | public float getMinScaleFactor() { 138 | return mMinScaleFactor; 139 | } 140 | 141 | /** 142 | * Sets the minimum scale factor allowed. 143 | *
144 | * Note that the hierarchy performs scaling as well, which 145 | * is not accounted here, so the actual scale factor may differ. 146 | */ 147 | public void setMinScaleFactor(float minScaleFactor) { 148 | mMinScaleFactor = minScaleFactor; 149 | } 150 | 151 | /** Gets the maximum scale factor allowed. */ 152 | public float getMaxScaleFactor() { 153 | return mMaxScaleFactor; 154 | } 155 | 156 | /** 157 | * Sets the maximum scale factor allowed. 158 | *
159 | * Note that the hierarchy performs scaling as well, which 160 | * is not accounted here, so the actual scale factor may differ. 161 | */ 162 | public void setMaxScaleFactor(float maxScaleFactor) { 163 | mMaxScaleFactor = maxScaleFactor; 164 | } 165 | 166 | /** 167 | * Maps point from the view's to the image's relative coordinate system. 168 | * This takes into account the zoomable transformation. 169 | */ 170 | public PointF mapViewToImage(PointF viewPoint) { 171 | float[] points = mTempValues; 172 | points[0] = viewPoint.x; 173 | points[1] = viewPoint.y; 174 | mActiveTransform.invert(mActiveTransformInverse); 175 | mActiveTransformInverse.mapPoints(points, 0, points, 0, 1); 176 | mapAbsoluteToRelative(points, points, 1); 177 | return new PointF(points[0], points[1]); 178 | } 179 | 180 | /** 181 | * Maps point from the image's relative to the view's coordinate system. 182 | * This takes into account the zoomable transformation. 183 | */ 184 | public PointF mapImageToView(PointF imagePoint) { 185 | float[] points = mTempValues; 186 | points[0] = imagePoint.x; 187 | points[1] = imagePoint.y; 188 | mapRelativeToAbsolute(points, points, 1); 189 | mActiveTransform.mapPoints(points, 0, points, 0, 1); 190 | return new PointF(points[0], points[1]); 191 | } 192 | 193 | private void mapAbsoluteToRelative(float[] destPoints, float[] srcPoints, int numPoints) { 194 | for (int i = 0; i < numPoints; i++) { 195 | destPoints[i * 2 + 0] = (srcPoints[i * 2 + 0] - mImageBounds.left) / mImageBounds.width(); 196 | destPoints[i * 2 + 1] = (srcPoints[i * 2 + 1] - mImageBounds.top) / mImageBounds.height(); 197 | } 198 | } 199 | 200 | private void mapRelativeToAbsolute(float[] destPoints, float[] srcPoints, int numPoints) { 201 | for (int i = 0; i < numPoints; i++) { 202 | destPoints[i * 2 + 0] = srcPoints[i * 2 + 0] * mImageBounds.width() + mImageBounds.left; 203 | destPoints[i * 2 + 1] = srcPoints[i * 2 + 1] * mImageBounds.height() + mImageBounds.top; 204 | } 205 | } 206 | 207 | /** 208 | * Gets the zoomable transformation 209 | * Internal matrix is exposed for performance reasons and is not to be modified by the callers. 210 | */ 211 | @Override 212 | public Matrix getTransform() { 213 | return mActiveTransform; 214 | } 215 | 216 | /** 217 | * Sets the zoomable transformation. Cancels the current gesture if one is happening. 218 | */ 219 | public void setTransform(Matrix activeTransform) { 220 | if (mGestureDetector.isGestureInProgress()) { 221 | mGestureDetector.reset(); 222 | } 223 | mActiveTransform.set(activeTransform); 224 | } 225 | 226 | /** Notifies controller of the received touch event. */ 227 | @Override 228 | public boolean onTouchEvent(MotionEvent event) { 229 | if (mIsEnabled) { 230 | return mGestureDetector.onTouchEvent(event); 231 | } 232 | return false; 233 | } 234 | 235 | public void zoomToImagePoint(float scale, PointF imagePoint) { 236 | if (mGestureDetector.isGestureInProgress()) { 237 | mGestureDetector.reset(); 238 | } 239 | scale = limit(scale, mMinScaleFactor, mMaxScaleFactor); 240 | float[] points = mTempValues; 241 | points[0] = imagePoint.x; 242 | points[1] = imagePoint.y; 243 | mapRelativeToAbsolute(points, points, 1); 244 | mActiveTransform.setScale(scale, scale, points[0], points[1]); 245 | mActiveTransform.postTranslate( 246 | mViewBounds.centerX() - points[0], 247 | mViewBounds.centerY() - points[1]); 248 | limitTranslation(); 249 | } 250 | 251 | /* TransformGestureDetector.Listener methods */ 252 | 253 | @Override 254 | public void onGestureBegin(TransformGestureDetector detector) { 255 | mPreviousTransform.set(mActiveTransform); 256 | } 257 | 258 | @Override 259 | public void onGestureUpdate(TransformGestureDetector detector) { 260 | mActiveTransform.set(mPreviousTransform); 261 | if (mIsRotationEnabled) { 262 | float angle = detector.getRotation() * (float) (180 / Math.PI); 263 | mActiveTransform.postRotate(angle, detector.getPivotX(), detector.getPivotY()); 264 | } 265 | if (mIsScaleEnabled) { 266 | float scale = detector.getScale(); 267 | mActiveTransform.postScale(scale, scale, detector.getPivotX(), detector.getPivotY()); 268 | } 269 | limitScale(detector.getPivotX(), detector.getPivotY()); 270 | if (mIsTranslationEnabled) { 271 | mActiveTransform.postTranslate(detector.getTranslationX(), detector.getTranslationY()); 272 | } 273 | if (limitTranslation()) { 274 | mGestureDetector.restartGesture(); 275 | } 276 | if (mListener != null) { 277 | mListener.onTransformChanged(mActiveTransform); 278 | } 279 | } 280 | 281 | @Override 282 | public void onGestureEnd(TransformGestureDetector detector) { 283 | mPreviousTransform.set(mActiveTransform); 284 | } 285 | 286 | /** Gets the current scale factor. */ 287 | @Override 288 | public float getScaleFactor() { 289 | mActiveTransform.getValues(mTempValues); 290 | return mTempValues[Matrix.MSCALE_X]; 291 | } 292 | 293 | private void limitScale(float pivotX, float pivotY) { 294 | float currentScale = getScaleFactor(); 295 | float targetScale = limit(currentScale, mMinScaleFactor, mMaxScaleFactor); 296 | if (targetScale != currentScale) { 297 | float scale = targetScale / currentScale; 298 | mActiveTransform.postScale(scale, scale, pivotX, pivotY); 299 | } 300 | } 301 | 302 | private boolean limitTranslation() { 303 | RectF bounds = mTransformedImageBounds; 304 | bounds.set(mImageBounds); 305 | mActiveTransform.mapRect(bounds); 306 | 307 | float offsetLeft = getOffset(bounds.left, bounds.width(), mViewBounds.width()); 308 | float offsetTop = getOffset(bounds.top, bounds.height(), mViewBounds.height()); 309 | if (offsetLeft != bounds.left || offsetTop != bounds.top) { 310 | mActiveTransform.postTranslate(offsetLeft - bounds.left, offsetTop - bounds.top); 311 | return true; 312 | } 313 | return false; 314 | } 315 | 316 | private float getOffset(float offset, float imageDimension, float viewDimension) { 317 | float diff = viewDimension - imageDimension; 318 | return (diff > 0) ? diff / 2 : limit(offset, diff, 0); 319 | } 320 | 321 | private float limit(float value, float min, float max) { 322 | return Math.min(Math.max(min, value), max); 323 | } 324 | 325 | } -------------------------------------------------------------------------------- /lib.lhh.fiv.library/src/lib.lhh.fiv/library/zoomable/ZoomableController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | package lib.lhh.fiv.library.zoomable; 14 | 15 | import android.graphics.Matrix; 16 | import android.graphics.RectF; 17 | import android.view.MotionEvent; 18 | 19 | /** 20 | * Interface for implementing a controller that works with {@link ZoomableDraweeView} 21 | * to control the zoom. 22 | */ 23 | public interface ZoomableController { 24 | 25 | /** 26 | * Listener interface. 27 | */ 28 | public interface Listener { 29 | 30 | void onTransformChanged(Matrix transform); 31 | } 32 | 33 | 34 | void setEnabled(boolean enabled); 35 | 36 | boolean isEnabled(); 37 | 38 | void setListener(Listener listener); 39 | 40 | float getScaleFactor(); 41 | 42 | Matrix getTransform(); 43 | 44 | void setImageBounds(RectF imageBounds); 45 | 46 | void setViewBounds(RectF viewBounds); 47 | 48 | boolean onTouchEvent(MotionEvent event); 49 | } 50 | -------------------------------------------------------------------------------- /lib.lhh.fiv.library/src/lib.lhh.fiv/library/zoomable/ZoomableDraweeView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | package lib.lhh.fiv.library.zoomable; 14 | 15 | /* 16 | * This file provided by Facebook is for non-commercial testing and evaluation 17 | * purposes only. Facebook reserves all rights not expressly granted. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 22 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 23 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 24 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | 28 | import android.content.Context; 29 | import android.graphics.Canvas; 30 | import android.graphics.Matrix; 31 | import android.graphics.PointF; 32 | import android.graphics.RectF; 33 | import android.graphics.drawable.Animatable; 34 | import android.util.AttributeSet; 35 | import android.view.MotionEvent; 36 | 37 | import com.facebook.common.internal.Preconditions; 38 | import com.facebook.drawee.controller.AbstractDraweeController; 39 | import com.facebook.drawee.controller.BaseControllerListener; 40 | import com.facebook.drawee.controller.ControllerListener; 41 | import com.facebook.drawee.generic.GenericDraweeHierarchy; 42 | import com.facebook.drawee.interfaces.DraweeController; 43 | import com.facebook.drawee.view.DraweeView; 44 | import com.facebook.drawee.view.GenericDraweeView; 45 | 46 | /** 47 | * DraweeView that has zoomable capabilities. 48 | *
49 | * Once the image loads, pinch-to-zoom and translation gestures are enabled. 50 | * 51 | */ 52 | public class ZoomableDraweeView extends GenericDraweeView 53 | implements ZoomableController.Listener { 54 | 55 | // private static final Class> TAG = ZoomableDraweeView.class; 56 | 57 | private static final float HUGE_IMAGE_SCALE_FACTOR_THRESHOLD = 1.1f; 58 | 59 | private static final int TOUCH_TIME = 250;//触摸间隔时间 60 | 61 | private final RectF mImageBounds = new RectF(); 62 | private final RectF mViewBounds = new RectF(); 63 | 64 | private final ControllerListener mControllerListener = new BaseControllerListener