diff options
Diffstat (limited to 'core/java/android/gesture/GestureOverlayView.java')
-rwxr-xr-x | core/java/android/gesture/GestureOverlayView.java | 793 |
1 files changed, 793 insertions, 0 deletions
diff --git a/core/java/android/gesture/GestureOverlayView.java b/core/java/android/gesture/GestureOverlayView.java new file mode 100755 index 0000000..5bfdcc4 --- /dev/null +++ b/core/java/android/gesture/GestureOverlayView.java @@ -0,0 +1,793 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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 android.gesture; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.animation.AnimationUtils; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.widget.FrameLayout; +import android.os.SystemClock; +import com.android.internal.R; + +import java.util.ArrayList; + +/** + * A transparent overlay for gesture input that can be placed on top of other + * widgets or contain other widgets. + * + * @attr ref android.R.styleable#GestureOverlayView_eventsInterceptionEnabled + * @attr ref android.R.styleable#GestureOverlayView_fadeDuration + * @attr ref android.R.styleable#GestureOverlayView_fadeOffset + * @attr ref android.R.styleable#GestureOverlayView_fadeEnabled + * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeWidth + * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeAngleThreshold + * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeLengthThreshold + * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeSquarenessThreshold + * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeType + * @attr ref android.R.styleable#GestureOverlayView_gestureColor + * @attr ref android.R.styleable#GestureOverlayView_orientation + * @attr ref android.R.styleable#GestureOverlayView_uncertainGestureColor + */ +public class GestureOverlayView extends FrameLayout { + public static final int GESTURE_STROKE_TYPE_SINGLE = 0; + public static final int GESTURE_STROKE_TYPE_MULTIPLE = 1; + + public static final int ORIENTATION_HORIZONTAL = 0; + public static final int ORIENTATION_VERTICAL = 1; + + private static final int FADE_ANIMATION_RATE = 16; + private static final boolean GESTURE_RENDERING_ANTIALIAS = true; + private static final boolean DITHER_FLAG = true; + + private final Paint mGesturePaint = new Paint(); + + private long mFadeDuration = 150; + private long mFadeOffset = 420; + private long mFadingStart; + private boolean mFadingHasStarted; + private boolean mFadeEnabled = true; + + private int mCurrentColor; + private int mCertainGestureColor = 0xFFFFFF00; + private int mUncertainGestureColor = 0x48FFFF00; + private float mGestureStrokeWidth = 12.0f; + private int mInvalidateExtraBorder = 10; + + private int mGestureStrokeType = GESTURE_STROKE_TYPE_SINGLE; + private float mGestureStrokeLengthThreshold = 50.0f; + private float mGestureStrokeSquarenessTreshold = 0.275f; + private float mGestureStrokeAngleThreshold = 40.0f; + + private int mOrientation = ORIENTATION_VERTICAL; + + private final Rect mInvalidRect = new Rect(); + private final Path mPath = new Path(); + private boolean mGestureVisible = true; + + private float mX; + private float mY; + + private float mCurveEndX; + private float mCurveEndY; + + private float mTotalLength; + private boolean mIsGesturing = false; + private boolean mPreviousWasGesturing = false; + private boolean mInterceptEvents = true; + private boolean mIsListeningForGestures; + private boolean mResetGesture; + + // current gesture + private Gesture mCurrentGesture; + private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100); + + // TODO: Make this a list of WeakReferences + private final ArrayList<OnGestureListener> mOnGestureListeners = + new ArrayList<OnGestureListener>(); + // TODO: Make this a list of WeakReferences + private final ArrayList<OnGesturePerformedListener> mOnGesturePerformedListeners = + new ArrayList<OnGesturePerformedListener>(); + // TODO: Make this a list of WeakReferences + private final ArrayList<OnGesturingListener> mOnGesturingListeners = + new ArrayList<OnGesturingListener>(); + + private boolean mHandleGestureActions; + + // fading out effect + private boolean mIsFadingOut = false; + private float mFadingAlpha = 1.0f; + private final AccelerateDecelerateInterpolator mInterpolator = + new AccelerateDecelerateInterpolator(); + + private final FadeOutRunnable mFadingOut = new FadeOutRunnable(); + + public GestureOverlayView(Context context) { + super(context); + init(); + } + + public GestureOverlayView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.gestureOverlayViewStyle); + } + + public GestureOverlayView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.GestureOverlayView, defStyle, 0); + + mGestureStrokeWidth = a.getFloat(R.styleable.GestureOverlayView_gestureStrokeWidth, + mGestureStrokeWidth); + mInvalidateExtraBorder = Math.max(1, ((int) mGestureStrokeWidth) - 1); + mCertainGestureColor = a.getColor(R.styleable.GestureOverlayView_gestureColor, + mCertainGestureColor); + mUncertainGestureColor = a.getColor(R.styleable.GestureOverlayView_uncertainGestureColor, + mUncertainGestureColor); + mFadeDuration = a.getInt(R.styleable.GestureOverlayView_fadeDuration, (int) mFadeDuration); + mFadeOffset = a.getInt(R.styleable.GestureOverlayView_fadeOffset, (int) mFadeOffset); + mGestureStrokeType = a.getInt(R.styleable.GestureOverlayView_gestureStrokeType, + mGestureStrokeType); + mGestureStrokeLengthThreshold = a.getFloat( + R.styleable.GestureOverlayView_gestureStrokeLengthThreshold, + mGestureStrokeLengthThreshold); + mGestureStrokeAngleThreshold = a.getFloat( + R.styleable.GestureOverlayView_gestureStrokeAngleThreshold, + mGestureStrokeAngleThreshold); + mGestureStrokeSquarenessTreshold = a.getFloat( + R.styleable.GestureOverlayView_gestureStrokeSquarenessThreshold, + mGestureStrokeSquarenessTreshold); + mInterceptEvents = a.getBoolean(R.styleable.GestureOverlayView_eventsInterceptionEnabled, + mInterceptEvents); + mFadeEnabled = a.getBoolean(R.styleable.GestureOverlayView_fadeEnabled, + mFadeEnabled); + mOrientation = a.getInt(R.styleable.GestureOverlayView_orientation, mOrientation); + + a.recycle(); + + init(); + } + + private void init() { + setWillNotDraw(false); + + final Paint gesturePaint = mGesturePaint; + gesturePaint.setAntiAlias(GESTURE_RENDERING_ANTIALIAS); + gesturePaint.setColor(mCertainGestureColor); + gesturePaint.setStyle(Paint.Style.STROKE); + gesturePaint.setStrokeJoin(Paint.Join.ROUND); + gesturePaint.setStrokeCap(Paint.Cap.ROUND); + gesturePaint.setStrokeWidth(mGestureStrokeWidth); + gesturePaint.setDither(DITHER_FLAG); + + mCurrentColor = mCertainGestureColor; + setPaintAlpha(255); + } + + public ArrayList<GesturePoint> getCurrentStroke() { + return mStrokeBuffer; + } + + public int getOrientation() { + return mOrientation; + } + + public void setOrientation(int orientation) { + mOrientation = orientation; + } + + public void setGestureColor(int color) { + mCertainGestureColor = color; + } + + public void setUncertainGestureColor(int color) { + mUncertainGestureColor = color; + } + + public int getUncertainGestureColor() { + return mUncertainGestureColor; + } + + public int getGestureColor() { + return mCertainGestureColor; + } + + public float getGestureStrokeWidth() { + return mGestureStrokeWidth; + } + + public void setGestureStrokeWidth(float gestureStrokeWidth) { + mGestureStrokeWidth = gestureStrokeWidth; + mInvalidateExtraBorder = Math.max(1, ((int) gestureStrokeWidth) - 1); + mGesturePaint.setStrokeWidth(gestureStrokeWidth); + } + + public int getGestureStrokeType() { + return mGestureStrokeType; + } + + public void setGestureStrokeType(int gestureStrokeType) { + mGestureStrokeType = gestureStrokeType; + } + + public float getGestureStrokeLengthThreshold() { + return mGestureStrokeLengthThreshold; + } + + public void setGestureStrokeLengthThreshold(float gestureStrokeLengthThreshold) { + mGestureStrokeLengthThreshold = gestureStrokeLengthThreshold; + } + + public float getGestureStrokeSquarenessTreshold() { + return mGestureStrokeSquarenessTreshold; + } + + public void setGestureStrokeSquarenessTreshold(float gestureStrokeSquarenessTreshold) { + mGestureStrokeSquarenessTreshold = gestureStrokeSquarenessTreshold; + } + + public float getGestureStrokeAngleThreshold() { + return mGestureStrokeAngleThreshold; + } + + public void setGestureStrokeAngleThreshold(float gestureStrokeAngleThreshold) { + mGestureStrokeAngleThreshold = gestureStrokeAngleThreshold; + } + + public boolean isEventsInterceptionEnabled() { + return mInterceptEvents; + } + + public void setEventsInterceptionEnabled(boolean enabled) { + mInterceptEvents = enabled; + } + + public boolean isFadeEnabled() { + return mFadeEnabled; + } + + public void setFadeEnabled(boolean fadeEnabled) { + mFadeEnabled = fadeEnabled; + } + + public Gesture getGesture() { + return mCurrentGesture; + } + + public void setGesture(Gesture gesture) { + if (mCurrentGesture != null) { + clear(false); + } + + setCurrentColor(mCertainGestureColor); + mCurrentGesture = gesture; + + final Path path = mCurrentGesture.toPath(); + final RectF bounds = new RectF(); + path.computeBounds(bounds, true); + + // TODO: The path should also be scaled to fit inside this view + mPath.rewind(); + mPath.addPath(path, -bounds.left + (getWidth() - bounds.width()) / 2.0f, + -bounds.top + (getHeight() - bounds.height()) / 2.0f); + + mResetGesture = true; + + invalidate(); + } + + public Path getGesturePath() { + return mPath; + } + + public Path getGesturePath(Path path) { + path.set(mPath); + return path; + } + + public boolean isGestureVisible() { + return mGestureVisible; + } + + public void setGestureVisible(boolean visible) { + mGestureVisible = visible; + } + + public long getFadeOffset() { + return mFadeOffset; + } + + public void setFadeOffset(long fadeOffset) { + mFadeOffset = fadeOffset; + } + + public void addOnGestureListener(OnGestureListener listener) { + mOnGestureListeners.add(listener); + } + + public void removeOnGestureListener(OnGestureListener listener) { + mOnGestureListeners.remove(listener); + } + + public void removeAllOnGestureListeners() { + mOnGestureListeners.clear(); + } + + public void addOnGesturePerformedListener(OnGesturePerformedListener listener) { + mOnGesturePerformedListeners.add(listener); + if (mOnGesturePerformedListeners.size() > 0) { + mHandleGestureActions = true; + } + } + + public void removeOnGesturePerformedListener(OnGesturePerformedListener listener) { + mOnGesturePerformedListeners.remove(listener); + if (mOnGesturePerformedListeners.size() <= 0) { + mHandleGestureActions = false; + } + } + + public void removeAllOnGesturePerformedListeners() { + mOnGesturePerformedListeners.clear(); + mHandleGestureActions = false; + } + + public void addOnGesturingListener(OnGesturingListener listener) { + mOnGesturingListeners.add(listener); + } + + public void removeOnGesturingListener(OnGesturingListener listener) { + mOnGesturingListeners.remove(listener); + } + + public void removeAllOnGesturingListeners() { + mOnGesturingListeners.clear(); + } + + public boolean isGesturing() { + return mIsGesturing; + } + + private void setCurrentColor(int color) { + mCurrentColor = color; + if (mFadingHasStarted) { + setPaintAlpha((int) (255 * mFadingAlpha)); + } else { + setPaintAlpha(255); + } + invalidate(); + } + + /** + * @hide + */ + public Paint getGesturePaint() { + return mGesturePaint; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + if (mCurrentGesture != null && mGestureVisible) { + canvas.drawPath(mPath, mGesturePaint); + } + } + + private void setPaintAlpha(int alpha) { + alpha += alpha >> 7; + final int baseAlpha = mCurrentColor >>> 24; + final int useAlpha = baseAlpha * alpha >> 8; + mGesturePaint.setColor((mCurrentColor << 8 >>> 8) | (useAlpha << 24)); + } + + public void clear(boolean animated) { + clear(animated, false, true); + } + + private void clear(boolean animated, boolean fireActionPerformed, boolean immediate) { + setPaintAlpha(255); + removeCallbacks(mFadingOut); + mResetGesture = false; + mFadingOut.fireActionPerformed = fireActionPerformed; + mFadingOut.resetMultipleStrokes = false; + + if (animated && mCurrentGesture != null) { + mFadingAlpha = 1.0f; + mIsFadingOut = true; + mFadingHasStarted = false; + mFadingStart = AnimationUtils.currentAnimationTimeMillis() + mFadeOffset; + + postDelayed(mFadingOut, mFadeOffset); + } else { + mFadingAlpha = 1.0f; + mIsFadingOut = false; + mFadingHasStarted = false; + + if (immediate) { + mCurrentGesture = null; + mPath.rewind(); + invalidate(); + } else if (fireActionPerformed) { + postDelayed(mFadingOut, mFadeOffset); + } else if (mGestureStrokeType == GESTURE_STROKE_TYPE_MULTIPLE) { + mFadingOut.resetMultipleStrokes = true; + postDelayed(mFadingOut, mFadeOffset); + } else { + mCurrentGesture = null; + mPath.rewind(); + invalidate(); + } + } + } + + public void cancelClearAnimation() { + setPaintAlpha(255); + mIsFadingOut = false; + mFadingHasStarted = false; + removeCallbacks(mFadingOut); + mPath.rewind(); + mCurrentGesture = null; + } + + public void cancelGesture() { + mIsListeningForGestures = false; + + // add the stroke to the current gesture + mCurrentGesture.addStroke(new GestureStroke(mStrokeBuffer)); + + // pass the event to handlers + final long now = SystemClock.uptimeMillis(); + final MotionEvent event = MotionEvent.obtain(now, now, + MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); + + final ArrayList<OnGestureListener> listeners = mOnGestureListeners; + int count = listeners.size(); + for (int i = 0; i < count; i++) { + listeners.get(i).onGestureCancelled(this, event); + } + + event.recycle(); + + clear(false); + mIsGesturing = false; + mPreviousWasGesturing = false; + mStrokeBuffer.clear(); + + final ArrayList<OnGesturingListener> otherListeners = mOnGesturingListeners; + count = otherListeners.size(); + for (int i = 0; i < count; i++) { + otherListeners.get(i).onGesturingEnded(this); + } + } + + @Override + protected void onDetachedFromWindow() { + cancelClearAnimation(); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + if (isEnabled()) { + final boolean cancelDispatch = (mIsGesturing || (mCurrentGesture != null && + mCurrentGesture.getStrokesCount() > 0 && mPreviousWasGesturing)) && + mInterceptEvents; + + processEvent(event); + + if (cancelDispatch) { + event.setAction(MotionEvent.ACTION_CANCEL); + } + + super.dispatchTouchEvent(event); + + return true; + } + + return super.dispatchTouchEvent(event); + } + + private boolean processEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + touchDown(event); + invalidate(); + return true; + case MotionEvent.ACTION_MOVE: + if (mIsListeningForGestures) { + Rect rect = touchMove(event); + if (rect != null) { + invalidate(rect); + } + return true; + } + break; + case MotionEvent.ACTION_UP: + if (mIsListeningForGestures) { + touchUp(event, false); + invalidate(); + return true; + } + break; + case MotionEvent.ACTION_CANCEL: + if (mIsListeningForGestures) { + touchUp(event, true); + invalidate(); + return true; + } + } + + return false; + } + + private void touchDown(MotionEvent event) { + mIsListeningForGestures = true; + + float x = event.getX(); + float y = event.getY(); + + mX = x; + mY = y; + + mTotalLength = 0; + mIsGesturing = false; + + if (mGestureStrokeType == GESTURE_STROKE_TYPE_SINGLE || mResetGesture) { + if (mHandleGestureActions) setCurrentColor(mUncertainGestureColor); + mResetGesture = false; + mCurrentGesture = null; + mPath.rewind(); + } else if (mCurrentGesture == null || mCurrentGesture.getStrokesCount() == 0) { + if (mHandleGestureActions) setCurrentColor(mUncertainGestureColor); + } + + // if there is fading out going on, stop it. + if (mFadingHasStarted) { + cancelClearAnimation(); + } else if (mIsFadingOut) { + setPaintAlpha(255); + mIsFadingOut = false; + mFadingHasStarted = false; + removeCallbacks(mFadingOut); + } + + if (mCurrentGesture == null) { + mCurrentGesture = new Gesture(); + } + + mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); + mPath.moveTo(x, y); + + final int border = mInvalidateExtraBorder; + mInvalidRect.set((int) x - border, (int) y - border, (int) x + border, (int) y + border); + + mCurveEndX = x; + mCurveEndY = y; + + // pass the event to handlers + final ArrayList<OnGestureListener> listeners = mOnGestureListeners; + final int count = listeners.size(); + for (int i = 0; i < count; i++) { + listeners.get(i).onGestureStarted(this, event); + } + } + + private Rect touchMove(MotionEvent event) { + Rect areaToRefresh = null; + + final float x = event.getX(); + final float y = event.getY(); + + final float previousX = mX; + final float previousY = mY; + + final float dx = Math.abs(x - previousX); + final float dy = Math.abs(y - previousY); + + if (dx >= GestureStroke.TOUCH_TOLERANCE || dy >= GestureStroke.TOUCH_TOLERANCE) { + areaToRefresh = mInvalidRect; + + // start with the curve end + final int border = mInvalidateExtraBorder; + areaToRefresh.set((int) mCurveEndX - border, (int) mCurveEndY - border, + (int) mCurveEndX + border, (int) mCurveEndY + border); + + float cX = mCurveEndX = (x + previousX) / 2; + float cY = mCurveEndY = (y + previousY) / 2; + + mPath.quadTo(previousX, previousY, cX, cY); + + // union with the control point of the new curve + areaToRefresh.union((int) previousX - border, (int) previousY - border, + (int) previousX + border, (int) previousY + border); + + // union with the end point of the new curve + areaToRefresh.union((int) cX - border, (int) cY - border, + (int) cX + border, (int) cY + border); + + mX = x; + mY = y; + + mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); + + if (mHandleGestureActions && !mIsGesturing) { + mTotalLength += (float) Math.sqrt(dx * dx + dy * dy); + + if (mTotalLength > mGestureStrokeLengthThreshold) { + final OrientedBoundingBox box = + GestureUtilities.computeOrientedBoundingBox(mStrokeBuffer); + + float angle = Math.abs(box.orientation); + if (angle > 90) { + angle = 180 - angle; + } + + if (box.squareness > mGestureStrokeSquarenessTreshold || + (mOrientation == ORIENTATION_VERTICAL ? + angle < mGestureStrokeAngleThreshold : + angle > mGestureStrokeAngleThreshold)) { + + mIsGesturing = true; + setCurrentColor(mCertainGestureColor); + + final ArrayList<OnGesturingListener> listeners = mOnGesturingListeners; + int count = listeners.size(); + for (int i = 0; i < count; i++) { + listeners.get(i).onGesturingStarted(this); + } + } + } + } + + // pass the event to handlers + final ArrayList<OnGestureListener> listeners = mOnGestureListeners; + final int count = listeners.size(); + for (int i = 0; i < count; i++) { + listeners.get(i).onGesture(this, event); + } + } + + return areaToRefresh; + } + + private void touchUp(MotionEvent event, boolean cancel) { + mIsListeningForGestures = false; + + // A gesture wasn't started or was cancelled + if (mCurrentGesture != null) { + // add the stroke to the current gesture + mCurrentGesture.addStroke(new GestureStroke(mStrokeBuffer)); + + if (!cancel) { + // pass the event to handlers + final ArrayList<OnGestureListener> listeners = mOnGestureListeners; + int count = listeners.size(); + for (int i = 0; i < count; i++) { + listeners.get(i).onGestureEnded(this, event); + } + + clear(mHandleGestureActions && mFadeEnabled, mHandleGestureActions && mIsGesturing, + false); + } else { + cancelGesture(event); + + } + } else { + cancelGesture(event); + } + + mStrokeBuffer.clear(); + mPreviousWasGesturing = mIsGesturing; + mIsGesturing = false; + + final ArrayList<OnGesturingListener> listeners = mOnGesturingListeners; + int count = listeners.size(); + for (int i = 0; i < count; i++) { + listeners.get(i).onGesturingEnded(this); + } + } + + private void cancelGesture(MotionEvent event) { + // pass the event to handlers + final ArrayList<OnGestureListener> listeners = mOnGestureListeners; + final int count = listeners.size(); + for (int i = 0; i < count; i++) { + listeners.get(i).onGestureCancelled(this, event); + } + + clear(false); + } + + private void fireOnGesturePerformed() { + final ArrayList<OnGesturePerformedListener> actionListeners = mOnGesturePerformedListeners; + final int count = actionListeners.size(); + for (int i = 0; i < count; i++) { + actionListeners.get(i).onGesturePerformed(GestureOverlayView.this, mCurrentGesture); + } + } + + private class FadeOutRunnable implements Runnable { + boolean fireActionPerformed; + boolean resetMultipleStrokes; + + public void run() { + if (mIsFadingOut) { + final long now = AnimationUtils.currentAnimationTimeMillis(); + final long duration = now - mFadingStart; + + if (duration > mFadeDuration) { + if (fireActionPerformed) { + fireOnGesturePerformed(); + } + + mPreviousWasGesturing = false; + mIsFadingOut = false; + mFadingHasStarted = false; + mPath.rewind(); + mCurrentGesture = null; + setPaintAlpha(255); + } else { + mFadingHasStarted = true; + float interpolatedTime = Math.max(0.0f, + Math.min(1.0f, duration / (float) mFadeDuration)); + mFadingAlpha = 1.0f - mInterpolator.getInterpolation(interpolatedTime); + setPaintAlpha((int) (255 * mFadingAlpha)); + postDelayed(this, FADE_ANIMATION_RATE); + } + } else if (resetMultipleStrokes) { + mResetGesture = true; + } else { + fireOnGesturePerformed(); + + mFadingHasStarted = false; + mPath.rewind(); + mCurrentGesture = null; + mPreviousWasGesturing = false; + setPaintAlpha(255); + } + + invalidate(); + } + } + + public static interface OnGesturingListener { + void onGesturingStarted(GestureOverlayView overlay); + + void onGesturingEnded(GestureOverlayView overlay); + } + + public static interface OnGestureListener { + void onGestureStarted(GestureOverlayView overlay, MotionEvent event); + + void onGesture(GestureOverlayView overlay, MotionEvent event); + + void onGestureEnded(GestureOverlayView overlay, MotionEvent event); + + void onGestureCancelled(GestureOverlayView overlay, MotionEvent event); + } + + public static interface OnGesturePerformedListener { + void onGesturePerformed(GestureOverlayView overlay, Gesture gesture); + } +} |