/* * $Id$ * This file is a part of the Arakhne Foundation Classes, http://www.arakhne.org/afc * * Copyright (c) 2000-2012 Stephane GALLAND. * Copyright (c) 2005-10, Multiagent Team, Laboratoire Systemes et Transports, * Universite de Technologie de Belfort-Montbeliard. * Copyright (c) 2013-2016 The original authors, and other authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.arakhne.afc.ui.android.zoom; import org.arakhne.afc.math.continous.object2d.Circle2f; import org.arakhne.afc.math.continous.object2d.Ellipse2f; import org.arakhne.afc.math.continous.object2d.PathIterator2f; import org.arakhne.afc.math.continous.object2d.Point2f; import org.arakhne.afc.math.continous.object2d.Rectangle2f; import org.arakhne.afc.math.continous.object2d.RoundRectangle2f; import org.arakhne.afc.math.continous.object2d.Segment2f; import org.arakhne.afc.math.continous.object2d.Shape2f; import org.arakhne.afc.ui.CenteringTransform; import org.arakhne.afc.ui.ZoomableContext; import org.arakhne.afc.ui.ZoomableContextUtil; import org.arakhne.afc.ui.android.R; import org.arakhne.afc.ui.event.PointerEvent; import android.app.Activity; import android.content.Context; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.RectF; import android.os.Looper; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.widget.Toast; /** This abstract view provides the tools to move and scale * the view. It is abstract because it does not draw anything. * <p> * The implementation of the ZoomableView handles the pointer and key events as following: * <table border="1" width="100%" summary=""> * <thead> * <tr><td>Event</td><td>Status</td><td>Callback</td><td>Note</td></tr> * </thead> * <tbody> * <tr><td>POINTER_PRESSED</td><td>supported</td><td>{@link #onPointerPressed(PointerEvent)}</td><td>Allways called</td></tr> * <tr><td>POINTER_DRAGGED</td><td>supported</td><td>{@link #onPointerDragged(PointerEvent)}</td><td>Called only when the scale and move gestures are not in progress</td></tr> * <tr><td>POINTER_RELEASED</td><td>supported</td><td>{@link #onPointerReleased(PointerEvent)}</td><td>Called only when the scale and move gestures are not in progress</td></tr> * <tr><td>POINTER_MOVED</td><td>not supported</td><td></td><td>Pointer move on a touch screen cannot be detected?</td></tr> * <tr><td>POINTER_CLICK</td><td>not supported</td><td></td><td>See {@link #setOnClickListener(OnClickListener)}</td></tr> * <tr><td>POINTER_LONG_CLICK</td><td>not supported</td><td></td><td>See {@link #setOnLongClickListener(OnLongClickListener)}</td></tr> * <tr><td>KEY_PRESSED</td><td>not supported</td><td></td><td>See {@link #onKeyDown(int, android.view.KeyEvent)}</td></tr> * <tr><td>KEY_RELEASED</td><td>not supported</td><td></td><td>See {@link #onKeyUp(int, android.view.KeyEvent)}</td></tr> * <tr><td>KEY_TYPED</td><td>not supported</td><td></td><td>See {@link #onKeyUp(int, android.view.KeyEvent)}</td></tr> * </tbody> * </table> * <p> * The function {@link #onDrawView(Canvas, float, CenteringTransform)}} may * use an instance of the graphical context {@link DroidZoomableGraphics2D} to draw * the elements according to the zooming attributes. * * @author $Author: sgalland$ * @version $Name$ $Revision$ $Date$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ * @see DroidZoomableGraphics2D * @deprecated see JavaFX API */ @Deprecated public abstract class ZoomableView extends View implements ZoomableContext { /** Current position of the workspace. * This position permits to scroll the view. */ private float focusX = 0f; /** Current position of the workspace. * This position permits to scroll the view. */ private float focusY = 0f; /** Current scaling factor. */ private float scaleFactor = 1.f; /** Minimal scaling factor. */ private float minScaleFactor = 0.001f; /** Maximal scaling factor. */ private float maxScaleFactor = 100.f; /** Zooming sensibility. */ private float zoomingSensitivity = 1.5f; /** Invert the scrolling direction. */ private boolean isInvertScrollingDirection = false; /** Indicates if the repaint requests are ignored. */ private boolean isIgnoreRepaint = false; /** Transformation to center the view used when rendering the view. */ private final CenteringTransform centeringTransform = new CenteringTransform(); /** Indicates if the X axis is inverted or not. */ private boolean isXAxisInverted = false; /** Indicates if the Y axis is inverted or not. */ private boolean isYAxisInverted = false; /** Manager of touch events. */ private final TouchManager touchManager; /** Wrapper to the viewed document. */ private DocumentWrapper documentWrapper = null; /** * @param context is the droid context of the view. */ public ZoomableView(Context context) { this(context, null, 0); } /** * @param context is the droid context of the view. * @param attrs are the attributes of the view. */ public ZoomableView(Context context, AttributeSet attrs) { this(context, attrs, 0); } /** * @param context is the droid context of the view. * @param attrs are the attributes of the view. * @param defStyle is the style of the view. */ public ZoomableView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); this.documentWrapper = createDocumentWrapper(); this.touchManager = new TouchManager(this); this.touchManager.init(); } /** Invoked by the constructor to create a wrapper to the * data of the viewed document. * If there is no wrapper, several functions of ZoomableView * will not work. * * @return the wrapper or <code>null</code>. */ protected abstract DocumentWrapper createDocumentWrapper(); @Override public final boolean isXAxisInverted() { return this.isXAxisInverted; } @Override public final boolean isYAxisInverted() { return this.isYAxisInverted; } /** Invert or not the X axis. * <p> * If the X axis is inverted, the positives are to the left; * otherwise they are to the right (default UI standard). * * @param invert */ public final void setXAxisInverted(boolean invert) { if (invert!=this.isXAxisInverted) { this.isXAxisInverted = invert; onUpdateViewParameters(); repaint(); } } /** Invert or not the Y axis. * <p> * If the Y axis is inverted, the positives are to the left; * otherwise they are to the right (default UI standard). * * @param invert */ public final void setYAxisInverted(boolean invert) { if (invert!=this.isYAxisInverted) { this.isYAxisInverted = invert; onUpdateViewParameters(); repaint(); } } @Override public final void setOnLongClickListener(OnLongClickListener l) { super.setOnLongClickListener(this.touchManager.setOnLongClickListener(l)); } @Override public final void setOnClickListener(OnClickListener l) { super.setOnClickListener(this.touchManager.setOnClickListener(l)); } /** Set if the repaint requests are ignored or not. * * @param ignore */ public final void setIgnoreRepaint(boolean ignore) { this.isIgnoreRepaint = ignore; } /** Replies if the repaint requests are ignored or not. * * @return <code>true</code> if the repaint requests are ignored; * <code>false</code> if not. */ public final boolean isIgnoreRepaint() { return this.isIgnoreRepaint; } /** Invalidate this view wherever this function is invoked. * If the current thread is the UI-thread, {@link #invalidate()} * is invoked. If not, {@link #postInvalidate()} is invoked. * <p> * If {@link #isIgnoreRepaint()} replies <code>false</code>, this * function does nothing. * * @see #isIgnoreRepaint() */ public final void repaint() { if (!this.isIgnoreRepaint) { if (Looper.getMainLooper() == Looper.myLooper()) { invalidate(); } else { postInvalidate(); } } } /** Invalidate this view wherever this function is invoked. * If the current thread is the UI-thread, {@link #invalidate()} * is invoked. If not, {@link #postInvalidate()} is invoked. * <p> * If {@link #isIgnoreRepaint()} replies <code>false</code>, this * function does nothing. * * @param x * @param y * @param width * @param height * @see #isIgnoreRepaint() */ public final void repaint(float x, float y, float width, float height) { if (!this.isIgnoreRepaint) { int l = (int)logical2pixel_x(x)-5; int t = (int)logical2pixel_y(y)-5; int r = l + (int)logical2pixel_size(width)+10; int b = t + (int)logical2pixel_size(height)+10; if (Looper.getMainLooper() == Looper.myLooper()) { invalidate(l, t, r, b); } else { postInvalidate(l, t, r, b); } } } /** Invalidate this view wherever this function is invoked. * If the current thread is the UI-thread, {@link #invalidate()} * is invoked. If not, {@link #postInvalidate()} is invoked. * <p> * If {@link #isIgnoreRepaint()} replies <code>false</code>, this * function does nothing. * * @param r * @see #isIgnoreRepaint() */ public final void repaint(Rectangle2f r) { if (r!=null) repaint(r.getMinX(), r.getMinY(), r.getWidth(), r.getHeight()); } /** Invalidate this view wherever this function is invoked. * If the current thread is the UI-thread, {@link #invalidate()} * is invoked. If not, {@link #postInvalidate()} is invoked. * <p> * If {@link #isIgnoreRepaint()} replies <code>false</code>, this * function does nothing. * * @param r * @see #isIgnoreRepaint() */ public final void repaint(Rect r) { if (r!=null && !this.isIgnoreRepaint) { if (Looper.getMainLooper() == Looper.myLooper()) { invalidate(r.left, r.top, r.right, r.bottom); } else { postInvalidate(r.left, r.top, r.right, r.bottom); } } } /** Invalidate this view wherever this function is invoked. * If the current thread is the UI-thread, {@link #invalidate()} * is invoked. If not, {@link #postInvalidate()} is invoked. * <p> * If {@link #isIgnoreRepaint()} replies <code>false</code>, this * function does nothing. * * @param r * @see #isIgnoreRepaint() */ public final void repaint(RectF r) { if (r!=null) { repaint(r.left, r.top, r.right, r.bottom); } } /** Replies if the direction of moving is inverted. * * @return <code>true</code> if the direction of moving * is inverted; otherwise <code>false</code>. */ public final boolean isMoveDirectionInverted() { return this.isInvertScrollingDirection; } /** Set if the direction of moving is inverted. * * @param invert is <code>true</code> if the direction of moving * is inverted; otherwise <code>false</code>. */ public final void setMoveDirectionInverted(boolean invert) { this.isInvertScrollingDirection = invert; } /** {@inheritDoc} */ @Override public final float logical2pixel_size(float l) { /*float s = l * this.scaleFactor; if ((l!=0)&&(s==0f)) s = 1f; return s;*/ return ZoomableContextUtil.logical2pixel_size(l, this.scaleFactor); } /** {@inheritDoc} */ @Override public final float logical2pixel_x(float l) { /*float dx = l - this.focusX; dx *= getScalingFactor(); return getViewportCenterX() + dx;*/ return ZoomableContextUtil.logical2pixel_x(l, this.centeringTransform, this.scaleFactor); } /** {@inheritDoc} */ @Override public final float logical2pixel_y(float l) { /*float dy = l - this.focusY; dy *= getScalingFactor(); return getViewportCenterY() + dy;*/ return ZoomableContextUtil.logical2pixel_y(l, this.centeringTransform, this.scaleFactor); } /** {@inheritDoc} */ @Override public final float pixel2logical_size(float l) { //return l / this.scaleFactor; return ZoomableContextUtil.pixel2logical_size(l, this.scaleFactor); } /** {@inheritDoc} */ @Override public final float pixel2logical_x(float l) { /*float dx = l - getViewportCenterX(); dx /= getScalingFactor(); return this.focusX + dx;*/ return ZoomableContextUtil.pixel2logical_x(l, this.centeringTransform, this.scaleFactor); } /** {@inheritDoc} */ @Override public final float pixel2logical_y(float l) { /*float dy = l - getViewportCenterY(); dy /= getScalingFactor(); return this.focusY + dy;*/ return ZoomableContextUtil.pixel2logical_y(l, this.centeringTransform, this.scaleFactor); } @Override public PathIterator2f logical2pixel(PathIterator2f p) { return ZoomableContextUtil.logical2pixel(p, this.centeringTransform, this.scaleFactor); } @Override public PathIterator2f pixel2logical(PathIterator2f p) { return ZoomableContextUtil.pixel2logical(p, this.centeringTransform, this.scaleFactor); } @Override public void logical2pixel(Segment2f s) { ZoomableContextUtil.logical2pixel(s, this.centeringTransform, this.scaleFactor); } @Override public void pixel2logical(Segment2f s) { ZoomableContextUtil.pixel2logical(s, this.centeringTransform, this.scaleFactor); } @Override public void logical2pixel(RoundRectangle2f r) { ZoomableContextUtil.logical2pixel(r, this.centeringTransform, this.scaleFactor); } @Override public void pixel2logical(RoundRectangle2f r) { ZoomableContextUtil.pixel2logical(r, this.centeringTransform, this.scaleFactor); } @Override public void logical2pixel(Point2f p) { ZoomableContextUtil.logical2pixel(p, this.centeringTransform, this.scaleFactor); } @Override public void pixel2logical(Point2f p) { ZoomableContextUtil.pixel2logical(p, this.centeringTransform, this.scaleFactor); } @Override public void logical2pixel(Ellipse2f e) { ZoomableContextUtil.logical2pixel(e, this.centeringTransform, this.scaleFactor); } @Override public void pixel2logical(Ellipse2f e) { ZoomableContextUtil.pixel2logical(e, this.centeringTransform, this.scaleFactor); } @Override public void logical2pixel(Circle2f r) { ZoomableContextUtil.logical2pixel(r, this.centeringTransform, this.scaleFactor); } @Override public void pixel2logical(Circle2f r) { ZoomableContextUtil.pixel2logical(r, this.centeringTransform, this.scaleFactor); } @Override public void logical2pixel(Rectangle2f r) { ZoomableContextUtil.logical2pixel(r, this.centeringTransform, this.scaleFactor); } @Override public void pixel2logical(Rectangle2f r) { ZoomableContextUtil.pixel2logical(r, this.centeringTransform, this.scaleFactor); } @Override public Shape2f logical2pixel(Shape2f s) { return ZoomableContextUtil.logical2pixel(s, this.centeringTransform, this.scaleFactor); } @Override public Shape2f pixel2logical(Shape2f s) { return ZoomableContextUtil.pixel2logical(s, this.centeringTransform, this.scaleFactor); } /** {@inheritDoc} */ @Override public final float getScalingSensitivity() { return this.zoomingSensitivity; } /** Replies the sensivility of the {@code zoomIn()} * and {@code zoomOut()} actions. * * @param sensivility */ public final void setScalingSensitivity(float sensivility) { this.zoomingSensitivity = Math.max(sensivility, Float.MIN_NORMAL); } /** Replies the X coordinate of the center of the viewport (in screen coordinate). * * @return the center of the viewport. */ public final float getViewportCenterX() { return getMeasuredWidth()/2f; } /** Replies the Y coordinate of the center of the viewport (in screen coordinate). * * @return the center of the viewport. */ public final float getViewportCenterY() { return getMeasuredHeight()/2f; } /** {@inheritDoc} */ @Override public final float getFocusX() { return pixel2logical_x(getViewportCenterX()); } /** {@inheritDoc} */ @Override public final float getFocusY() { return pixel2logical_y(getViewportCenterY()); } /** {@inheritDoc} */ @Override public final float getScalingFactor() { return this.scaleFactor; } /** Set the scaling factor. * * @param factor is the scaling factor. * @return <code>true</code> if the scaling factor has changed; * otherwise <code>false</code>. */ public final boolean setScalingFactor(float factor) { if (setScalingFactorAndFocus(Float.NaN, Float.NaN, factor)) { repaint(); return true; } return false; } /** Change the scaling factor to have the specified * ratio between 1 pixel and 1 unit in the document. * <p> * Each unit from the displayed document will * have the same graphical size as the amount of * pixels specified by the <var>ratio</var>. * * @param ratio */ public void setScalingFactorForPixelRatio(float ratio) { float onePixel = pixel2logical_size(1); float factor = onePixel * ratio; setScalingFactor(factor); } /** Set the scaling factor. This function does not repaint. * * @param scalingX is the coordinate of the point (on the screen) where the focus occurs. * @param scalingY is the coordinate of the point (on the screen) where the focus occurs. * @param factor is the scaling factor. * @return <code>true</code> if the scaling factor or the focus point has changed; * otherwise <code>false</code>. */ protected final boolean setScalingFactorAndFocus(float scalingX, float scalingY, float factor) { // Normalize the scaling factor float normalizedFactor = factor; if (normalizedFactor<this.minScaleFactor) normalizedFactor = this.minScaleFactor; if (normalizedFactor>this.maxScaleFactor) normalizedFactor = this.maxScaleFactor; // Determine the new position of the focus. // The new position of the focus depends on the current position, // the new scaling factor and where the scaling occured. if (!Float.isNaN(scalingX) && !Float.isNaN(scalingY)) { // Get screen coordinates float screenCenterX = getViewportCenterX(); float screenCenterY = getViewportCenterY(); float vectorToScreenCenterX = screenCenterX - scalingX; float vectorToScreenCenterY = screenCenterY - scalingY; // Get logical coordinates float sX = pixel2logical_x(scalingX); float sY = pixel2logical_y(scalingY); float newX = sX + vectorToScreenCenterX / normalizedFactor; float newY = sY + vectorToScreenCenterY / normalizedFactor; if (normalizedFactor!=this.scaleFactor || newX!=this.focusX || newY!=this.focusY) { this.scaleFactor = normalizedFactor; this.focusX = newX; this.focusY = newY; onUpdateViewParameters(); return true; } } else if (normalizedFactor!=this.scaleFactor) { this.scaleFactor = normalizedFactor; onUpdateViewParameters(); return true; } return false; } /** Zoom the view in. */ public final void zoomIn() { if (onScale( getViewportCenterX(), getViewportCenterY(), getScalingSensitivity())) { repaint(); } } /** Zoom the view out. */ public final void zoomOut() { if (onScale( getViewportCenterX(), getViewportCenterY(), 1f/getScalingSensitivity())) { repaint(); } } /** {@inheritDoc} */ @Override public final float getMaxScalingFactor() { return this.maxScaleFactor; } /** Set the maximal scaling factor allowing in the view * * @param factor is the maximal scaling factor. */ public final void setMaxScalingFactor(float factor) { if (factor>0f && factor!=this.maxScaleFactor) { this.maxScaleFactor = factor; if (this.minScaleFactor>this.maxScaleFactor) this.minScaleFactor = this.maxScaleFactor; if (this.scaleFactor>this.maxScaleFactor) this.scaleFactor = this.maxScaleFactor; repaint(); } } /** {@inheritDoc} */ @Override public final float getMinScalingFactor() { return this.minScaleFactor; } /** Set the minimal scaling factor allowing in the view * * @param factor is the minimal scaling factor. */ public final void setMinScalingFactor(float factor) { if (factor>0f && factor!=this.minScaleFactor) { this.minScaleFactor = factor; if (this.maxScaleFactor<this.minScaleFactor) this.maxScaleFactor = this.minScaleFactor; if (this.scaleFactor<this.minScaleFactor) this.scaleFactor = this.minScaleFactor; repaint(); } } /** Set the position of the focus point. * * @param x * @param y */ public final void setFocusPoint(float x, float y) { if (this.focusX!=x || this.focusY!=y) { this.focusX = x; this.focusY = y; onUpdateViewParameters(); repaint(); } } /** Translate the position of the focus point. * * @param dx * @param dy */ public final void translateFocusPoint(float dx, float dy) { if (dx!=0f || dy!=0f) { this.focusX += dx; this.focusY += dy; onUpdateViewParameters(); repaint(); } } /** Update any viewing parameter according to the * current value of the focus point and the scaling factor. * <p> * This function is invoked when the coordinates of the * focus point or the scaling factor has been changed to * ensure that all the drawing attributes are properly set. */ protected void onUpdateViewParameters() { /*float sf = getScalingFactor(); float w = getMeasuredWidth() / sf; float h = getMeasuredHeight() / sf; this.translateToCenterX = -(getFocusX() - w/2f); this.translateToCenterY = -(getFocusY() - h/2f);*/ float t; t = pixel2logical_size(getViewportCenterX()); if (isXAxisInverted()) { this.centeringTransform.setCenteringX( true, -1, t + this.focusX); } else { this.centeringTransform.setCenteringX( false, 1, t - this.focusX); } t = pixel2logical_size(getViewportCenterY()); if (isYAxisInverted()) { this.centeringTransform.setCenteringY( true, -1, t + this.focusY); } else { this.centeringTransform.setCenteringY( false, 1, t - this.focusY); } } /** Replies the preferred position of the focus point. * * @return the preferred position of the focus point. */ protected abstract float getPreferredFocusX(); /** Replies the preferred position of the focus point. * * @return the preferred position of the focus point. */ protected abstract float getPreferredFocusY(); /** Reset the view to the default configuration. * * @return <code>true</code> if the view has changed; <code>false</code> otherwise. */ public final boolean resetView() { float px = getPreferredFocusX(); float py = getPreferredFocusY(); if (this.focusX!=px || this.focusY!=py || getScalingFactor()!=1f) { this.focusX = px; this.focusY = py; if (!setScalingFactor(1f)) { onUpdateViewParameters(); repaint(); // Force to refresh the UI } toast(getContext().getString(R.string.reset_view_done), false); return true; } return false; } /** Replies the scaling factor that may be used * to fit the content of the document to the * drawing area. * <p> * If there is no wrapper replied by {@link #createDocumentWrapper()}, * this function replies <code>1</code>. * * @return the scaling factor that permits to fit the * document. */ public float getScalingFactorToFit() { if (this.documentWrapper!=null) { Rectangle2f documentBounds = this.documentWrapper.getDocumentBounds(); if (documentBounds!=null) { float drawingAreaSize, documentSize; // horizontal fitting drawingAreaSize = getWidth(); documentSize = documentBounds.getWidth(); float horizontalFactor = ZoomableContextUtil.determineFactor( documentSize, drawingAreaSize); // vertical fitting drawingAreaSize = getHeight(); documentSize = documentBounds.getHeight(); float verticalFactor = ZoomableContextUtil.determineFactor( documentSize, drawingAreaSize); return Math.min(horizontalFactor, verticalFactor); } } return 1f; } /** Reset the view so that the document is fitting the * drawing area. * <p> * If there is no wrapper replied by {@link #createDocumentWrapper()}, * this function does the same as {@link #resetView()}. * * @return <code>true</code> if the view has changed; <code>false</code> otherwise. */ public final boolean fitView() { float px = getPreferredFocusX(); float py = getPreferredFocusY(); float fitFactor = getScalingFactorToFit(); if (this.focusX!=px || this.focusY!=py || getScalingFactor()!=fitFactor) { this.focusX = px; this.focusY = py; if (!setScalingFactor(fitFactor)) { onUpdateViewParameters(); repaint(); // Force to refresh the UI } toast(getContext().getString(R.string.fit_view_done), false); return true; } return false; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { onUpdateViewParameters(); } /** * {@inheritDoc} */ @Override public final void onDraw(Canvas canvas) { super.onDraw(canvas); if(!isInEditMode()) { onDrawView(canvas, this.scaleFactor, this.centeringTransform); } } /** Invoked to paint the view after it is translated and scaled. * * @param canvas is the canvas in which the view must be painted. * @param scaleFactor is the scaling factor to use for drawing. * @param centeringTransform is the transform to use to put the draws at the center of the view. */ protected abstract void onDrawView(Canvas canvas, float scaleFactor, CenteringTransform centeringTransform); /** * {@inheritDoc} */ @Override public final boolean onTouchEvent(MotionEvent ev) { byte c = this.touchManager.onTouchEvent(ev); if ((c&2)!=0) { if (super.onTouchEvent(ev)) { c |= 1; } } return ((c&1)!=0); } /** Invoked when the a touch-down event is detected. * * @param e */ protected void onPointerPressed(PointerEvent e) { // } /** Invoked when the a touch-up event is detected. * * @param e */ protected void onPointerReleased(PointerEvent e) { // } /** Invoked when the pointer is moved with a button down. * * @param e */ protected void onPointerDragged(PointerEvent e) { // } /** Invoked when a long-click is detected. * * @param e */ protected void onLongClick(PointerEvent e) { // } /** Invoked when a short-click is detected. * * @param e */ protected void onClick(PointerEvent e) { // } /** * Invoked when the view must be scaled. * <p> * One of the border effect if this function replies <code>true</code> * is that the view will be repaint. * <p> * The default implementation of this function invokes * {@link #setScalingFactorAndFocus(float, float, float)}. * * @param focusX is the position of the focal point on the screen. * @param focusY is the position of the focal point on the screen. * @param requestedScaleFactor is the new scale factor. * @return Whether or not the detector should consider this event as handled. * If an event was not handled, the detector will continue to accumulate movement * until an event is handled. This can be useful if an application, for example, * only wants to update scaling factors if the change is greater than 0.01. * @see #setScalingFactorAndFocus(float, float, float) */ public boolean onScale(float focusX, float focusY, float requestedScaleFactor) { setScalingFactorAndFocus(focusX, focusY, this.scaleFactor * requestedScaleFactor); return true; } /** Show a toast message. * * @param message is the message to display * @param isLong indicates if it is a long-time (<code>true</code>) * or a short-time (<code>false</code>) message. */ public final void toast(String message, boolean isLong) { Context context = getContext(); if (context instanceof Activity) { ((Activity)context).runOnUiThread(new AsynchronousToaster(context, message, isLong)); } } /** Show a toast message. * * @param message is the message to display * @param isLong indicates if it is a long-time (<code>true</code>) * or a short-time (<code>false</code>) message. */ public final void toast(int message, boolean isLong) { Context context = getContext(); if (context instanceof Activity) { ((Activity)context).runOnUiThread(new AsynchronousToaster(context, message, isLong)); } } /** This class permits to display a toast message outside * the main UI thread. * <p> * A toast message is a small notification message to put on the UI. * The location of this message depends on the Android API. * * @author $Author: sgalland$ * @version $Name$ $Revision$ $Date$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ * @see Toast */ private static class AsynchronousToaster implements Runnable { private final Context context; private final String message; private final int messageId; private final boolean isLong; /** * @param context * @param message is the message to toast * @param isLong indicates if it is a long-time (<code>true</code>) * or a short-time (<code>false</code>) message. */ public AsynchronousToaster(Context context, String message, boolean isLong) { this.context = context; this.message = message; this.messageId = -1; this.isLong = isLong; } /** * @param context * @param message is the message to toast * @param isLong indicates if it is a long-time (<code>true</code>) * or a short-time (<code>false</code>) message. */ public AsynchronousToaster(Context context, int message, boolean isLong) { this.context = context; this.message = null; this.messageId = message; this.isLong = isLong; } @Override public void run() { if (this.message!=null && !this.message.isEmpty()) { Toast.makeText(this.context, this.message, this.isLong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show(); } else if (this.messageId>=0) { Toast.makeText(this.context, this.messageId, this.isLong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show(); } } } // class AsynchronousToaster }