/*
* Copyright (c) 2013 Menny Even-Danan
*
* 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 com.anysoftkeyboard.keyboards.views;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Point;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.widget.TextView;
import com.anysoftkeyboard.Configuration;
import com.anysoftkeyboard.Configuration.AnimationsLevel;
import com.anysoftkeyboard.api.KeyCodes;
import com.anysoftkeyboard.keyboardextensions.KeyboardExtension;
import com.anysoftkeyboard.keyboards.*;
import com.anysoftkeyboard.keyboards.AnyKeyboard.AnyKey;
import com.anysoftkeyboard.keyboards.Keyboard.Key;
import com.anysoftkeyboard.keyboards.Keyboard.Row;
import com.anysoftkeyboard.quicktextkeys.QuickTextKey;
import com.anysoftkeyboard.theme.KeyboardTheme;
import com.anysoftkeyboard.utils.Log;
import com.menny.android.anysoftkeyboard.AnyApplication;
import com.menny.android.anysoftkeyboard.FeaturesSet;
import com.menny.android.anysoftkeyboard.R;
public class AnyKeyboardView extends AnyKeyboardBaseView {
private static final int DELAY_BEFORE_POPING_UP_EXTENSION_KBD = 35;// milliseconds
private final static String TAG = "AnyKeyboardView";
private boolean mExtensionVisible = false;
private final int mExtensionKeyboardYActivationPoint;
private final int mExtensionKeyboardPopupOffset;
private final int mExtensionKeyboardYDismissPoint;
private Key mExtensionKey;
private Key mUtilityKey;
private Key mSpaceBarKey = null;
private Point mFirstTouchPont = new Point(0, 0);
private boolean mIsFirstDownEventInsideSpaceBar = false;
private Animation mInAnimation;
private TextView mPreviewText;
private float mGesturePreviewTextSize;
private int mGesturePreviewTextColor, mGesturePreviewTextColorRed,
mGesturePreviewTextColorGreen, mGesturePreviewTextColorBlue;
/** Whether we've started dropping move events because we found a big jump */
// private boolean mDroppingEvents;
/**
* Whether multi-touch disambiguation needs to be disabled if a real
* multi-touch event has occured
*/
// private boolean mDisableDisambiguation;
/**
* The distance threshold at which we start treating the touch session as a
* multi-touch
*/
// private int mJumpThresholdSquare = Integer.MAX_VALUE;
/**
* The y coordinate of the last row
*/
// private int mLastRowY;
public AnyKeyboardView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AnyKeyboardView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mExtensionKeyboardPopupOffset = 0;
mExtensionKeyboardYActivationPoint = -5;
mExtensionKeyboardYDismissPoint = getThemedKeyboardDimens()
.getNormalKeyHeight();
mInAnimation = null;
mGesturePreviewTextColorRed = (mGesturePreviewTextColor & 0x00FF0000) >> 16;
mGesturePreviewTextColorGreen = (mGesturePreviewTextColor & 0x0000FF00) >> 8;
mGesturePreviewTextColorBlue = mGesturePreviewTextColor & 0x000000FF;
}
/*
* protected void createGestureSlideAnimation() {
* mGestureSlideReachedAnimation =
* AnimationUtils.loadAnimation(getContext().getApplicationContext(),
* R.anim.gesture_slide_threshold_reached); }
*/
protected String getKeyboardViewNameForLogging() {
return "AnyKeyboardView";
}
@Override
protected ViewGroup inflatePreviewWindowLayout(LayoutInflater inflate) {
ViewGroup v = super.inflatePreviewWindowLayout(inflate);
mPreviewText = (TextView) v.findViewById(R.id.key_preview_text);
return v;
}
public void setKeyboardSwitcher(KeyboardSwitcher switcher) {
mSwitcher = switcher;
}
@Override
public void setKeyboard(AnyKeyboard newKeyboard) {
mExtensionKey = null;
mExtensionVisible = false;
mUtilityKey = null;
super.setKeyboard(newKeyboard);
if (newKeyboard != null && newKeyboard instanceof GenericKeyboard
&& ((GenericKeyboard) newKeyboard).disableKeyPreviews()) {
// Phone keyboard never shows popup preview (except language
// switch).
setPreviewEnabled(false);
} else {
setPreviewEnabled(AnyApplication.getConfig().getShowKeyPreview());
}
// TODO: For now! should be a calculated value
// lots of key : true
// some keys: false
setProximityCorrectionEnabled(true);
// One-seventh of the keyboard width seems like a reasonable threshold
// mJumpThresholdSquare = newKeyboard.getMinWidth() / 7;
// mJumpThresholdSquare *= mJumpThresholdSquare;
// Assuming there are 4 rows, this is the coordinate of the last row
// mLastRowY = (newKeyboard.getHeight() * 3) / 4;
// setKeyboardLocal(newKeyboard);
// looking for the spacebar, so I'll be able to detect swipes starting
// at it
mSpaceBarKey = null;
for (Key aKey : newKeyboard.getKeys()) {
if (aKey.codes[0] == (int) ' ') {
mSpaceBarKey = aKey;
break;
}
}
}
@Override
public boolean setValueFromTheme(TypedArray remoteTypedArray, int[] padding, int localAttrId, int remoteTypedArrayIndex) {
switch (localAttrId) {
case R.attr.previewGestureTextSize:
mGesturePreviewTextSize = remoteTypedArray.getDimensionPixelSize(remoteTypedArrayIndex, 0);
Log.d(TAG, "AnySoftKeyboardTheme_previewGestureTextSize "
+ mGesturePreviewTextSize);
break;
case R.attr.previewGestureTextColor:
mGesturePreviewTextColor = remoteTypedArray.getColor(remoteTypedArrayIndex, 0xFFF);
Log.d(TAG, "AnySoftKeyboardTheme_previewGestureTextColor "
+ mGesturePreviewTextColor);
default:
return super.setValueFromTheme(remoteTypedArray, padding, localAttrId, remoteTypedArrayIndex);
}
return true;
}
@Override
protected int getKeyboardStyleResId(KeyboardTheme theme) {
return theme.getThemeResId();
}
@Override
final protected boolean isFirstDownEventInsideSpaceBar() {
return mIsFirstDownEventInsideSpaceBar;
}
/*
public void simulateLongPress(int keyCode) {
Key key = findKeyByKeyCode(keyCode);
if (key != null)
super.onLongPress(getKeyboard().getKeyboardContext(), key, false, true);
}
*/
private boolean invokeOnKey(int primaryCode, Key key, int multiTapIndex) {
getOnKeyboardActionListener().onKey(primaryCode, key, multiTapIndex,
null, false);
return true;
}
public boolean isShiftLocked() {
AnyKeyboard keyboard = getKeyboard();
if (keyboard != null) {
return keyboard.isShiftLocked();
}
return false;
}
public boolean setShiftLocked(boolean shiftLocked) {
AnyKeyboard keyboard = getKeyboard();
if (keyboard != null) {
if (keyboard.setShiftLocked(shiftLocked)) {
invalidateAllKeys();
return true;
}
}
return false;
}
@Override
protected boolean onLongPress(Context packageContext, Key key,
boolean isSticky, boolean requireSlideInto) {
if (key != null && key instanceof AnyKey) {
AnyKey anyKey = (AnyKey) key;
if (anyKey.longPressCode != 0) {
invokeOnKey(anyKey.longPressCode, null, 0);
return true;
} else if (anyKey.codes[0] == KeyCodes.QUICK_TEXT) {
invokeOnKey(KeyCodes.QUICK_TEXT_POPUP, null, 0);
return true;
}
}
if (mAnimationLevel == AnimationsLevel.None) {
mMiniKeyboardPopup.setAnimationStyle(0);
} else if (mExtensionVisible
&& mMiniKeyboardPopup.getAnimationStyle() != R.style.ExtensionKeyboardAnimation) {
Log.d(TAG,
"Switching mini-keyboard animation to ExtensionKeyboardAnimation");
mMiniKeyboardPopup
.setAnimationStyle(R.style.ExtensionKeyboardAnimation);
} else if (!mExtensionVisible
&& mMiniKeyboardPopup.getAnimationStyle() != R.style.MiniKeyboardAnimation) {
Log.d(TAG,
"Switching mini-keyboard animation to MiniKeyboardAnimation");
mMiniKeyboardPopup.setAnimationStyle(R.style.MiniKeyboardAnimation);
}
return super.onLongPress(packageContext, key, isSticky,
requireSlideInto);
}
private long mExtensionKeyboardAreaEntranceTime = -1;
@Override
public boolean onTouchEvent(MotionEvent me) {
Log.d(TAG, "onTouchEvent with "+me.getPointerCount()+" points");
if (getKeyboard() == null)//I mean, if there isn't any keyboard I'm handling, what's the point?
return false;
if (areTouchesDisabled())
return super.onTouchEvent(me);
if (me.getAction() == MotionEvent.ACTION_DOWN) {
mFirstTouchPont.x = (int) me.getX();
mFirstTouchPont.y = (int) me.getY();
mIsFirstDownEventInsideSpaceBar =
mSpaceBarKey != null && mSpaceBarKey.isInside(mFirstTouchPont.x, mFirstTouchPont.y);
} else if (mIsFirstDownEventInsideSpaceBar) {
if (me.getAction() == MotionEvent.ACTION_MOVE) {
setGesturePreviewText(mSwitcher, me);
return true;
} else if (me.getAction() == MotionEvent.ACTION_UP) {
final int slide = getSlideDistance(me);
final int distance = slide & 0x00FF;// removing direction
if (distance > SLIDE_RATIO_FOR_GESTURE) {
// gesture!!
switch (slide & 0xFF00) {
case DIRECTION_DOWN:
mKeyboardActionListener.onSwipeDown(true);
break;
case DIRECTION_UP:
mKeyboardActionListener.onSwipeUp(true);
break;
case DIRECTION_LEFT:
mKeyboardActionListener.onSwipeLeft(true, isAtTwoFingersState());
break;
case DIRECTION_RIGHT:
mKeyboardActionListener.onSwipeRight(true, isAtTwoFingersState());
break;
}
} else {
// just a key press
super.onTouchEvent(me);
}
return true;
}
}
// If the motion event is above the keyboard and it's a MOVE event
// coming even before the first MOVE event into the extension area
if (!mIsFirstDownEventInsideSpaceBar
&& me.getY() < mExtensionKeyboardYActivationPoint
&& !isPopupShowing()
&& !mExtensionVisible
&& me.getAction() == MotionEvent.ACTION_MOVE) {
if (mExtensionKeyboardAreaEntranceTime <= 0)
mExtensionKeyboardAreaEntranceTime = System.currentTimeMillis();
if (System.currentTimeMillis() - mExtensionKeyboardAreaEntranceTime > DELAY_BEFORE_POPING_UP_EXTENSION_KBD) {
KeyboardExtension extKbd = ((ExternalAnyKeyboard) getKeyboard()).getExtensionLayout();
if (extKbd == null || extKbd.getKeyboardResId() == -1) {
return super.onTouchEvent(me);
} else {
// telling the main keyboard that the last touch was
// canceled
MotionEvent cancel = MotionEvent.obtain(me.getDownTime(),
me.getEventTime(), MotionEvent.ACTION_CANCEL,
me.getX(), me.getY(), 0);
super.onTouchEvent(cancel);
cancel.recycle();
mExtensionVisible = true;
dismissKeyPreview();
if (mExtensionKey == null) {
mExtensionKey = new AnyKey(new Row(getKeyboard()),
getThemedKeyboardDimens());
mExtensionKey.codes = new int[]{0};
mExtensionKey.edgeFlags = 0;
mExtensionKey.height = 1;
mExtensionKey.width = 1;
mExtensionKey.popupResId = extKbd.getKeyboardResId();
mExtensionKey.externalResourcePopupLayout = mExtensionKey.popupResId != 0;
mExtensionKey.x = getWidth() / 2;
mExtensionKey.y = mExtensionKeyboardPopupOffset;
}
// so the popup will be right above your finger.
mExtensionKey.x = (int) me.getX();
onLongPress(extKbd.getPackageContext(), mExtensionKey,
AnyApplication.getConfig().isStickyExtensionKeyboard(),
!AnyApplication.getConfig().isStickyExtensionKeyboard());
// it is an extension..
mMiniKeyboard.setPreviewEnabled(true);
return true;
}
} else {
return super.onTouchEvent(me);
}
} else if (mExtensionVisible
&& me.getY() > mExtensionKeyboardYDismissPoint) {
// closing the popup
dismissPopupKeyboard();
return true;
} else {
return super.onTouchEvent(me);
}
}
private static final int SLIDE_RATIO_FOR_GESTURE = 250;
private void setGesturePreviewText(KeyboardSwitcher switcher, MotionEvent me) {
if (mPreviewText == null)
return;
// started at SPACE, so I stick with the position. This is used
// for showing gesture info on the spacebar.
// we'll also add the current gesture, with alpha [0...200,255].
// if any
final int slide = getSlideDistance(me);
final int slideDisatance = slide & 0x00FF;// removing direction
if (slideDisatance >= 20) {
final boolean isGesture = slideDisatance > SLIDE_RATIO_FOR_GESTURE;
final int alpha = isGesture ? 255 : slideDisatance / 2;
mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX,
mGesturePreviewTextSize);
int color = Color
.argb(alpha, mGesturePreviewTextColorRed,
mGesturePreviewTextColorGreen,
mGesturePreviewTextColorBlue);
mPreviewText.setTextColor(color);
final int swipeKeyTarget;
final Configuration cfg = AnyApplication.getConfig();
switch (slide & 0xFF00) {// removing distance
case DIRECTION_UP:
swipeKeyTarget = cfg.getGestureSwipeUpKeyCode(true);
break;
case DIRECTION_DOWN:
swipeKeyTarget = cfg.getGestureSwipeDownKeyCode();
break;
case DIRECTION_LEFT:
swipeKeyTarget = cfg.getGestureSwipeLeftKeyCode(true, false);
break;
case DIRECTION_RIGHT:
swipeKeyTarget = cfg.getGestureSwipeRightKeyCode(true, false);
break;
default:
swipeKeyTarget = KeyCodes.SPACE;
break;
}
String tooltip;
switch (swipeKeyTarget) {
case KeyCodes.MODE_ALPHABET:
// printing the next alpha keyboard name
tooltip = switcher!=null? switcher.peekNextAlphabetKeyboard() : "";
break;
case KeyCodes.MODE_SYMOBLS:
// printing the next alpha keyboard name
tooltip = switcher!=null? switcher.peekNextSymbolsKeyboard() : "";
break;
default:
tooltip = "";
break;
}
mPreviewText.setText(tooltip);
} else {
mPreviewText.setText("");
}
}
private final static int DIRECTION_UP = 0x0100;
private final static int DIRECTION_DOWN = 0x0200;
private final static int DIRECTION_LEFT = 0x0400;
private final static int DIRECTION_RIGHT = 0x0800;
private int getSlideDistance(MotionEvent me) {
final int horizontalSlide = ((int) me.getX()) - mFirstTouchPont.x;
final int horizontalSlideAbs = Math.abs(horizontalSlide);
final int verticalSlide = ((int) me.getY()) - mFirstTouchPont.y;
final int verticalSlideAbs = Math.abs(verticalSlide);
final int direction;
final int slide;
final int maxSlide;
if (horizontalSlideAbs > verticalSlideAbs) {
if (horizontalSlide > 0) {
direction = DIRECTION_RIGHT;
} else {
direction = DIRECTION_LEFT;
}
maxSlide = mSwipeSpaceXDistanceThreshold;
slide = Math.min(horizontalSlideAbs, maxSlide);
} else {
if (verticalSlide > 0) {
direction = DIRECTION_DOWN;
} else {
direction = DIRECTION_UP;
}
maxSlide = mSwipeYDistanceThreshold;
slide = Math.min(verticalSlideAbs, maxSlide);
}
final int slideRatio = (255 * slide) / maxSlide;
return direction + slideRatio;
}
@Override
protected void onUpEvent(PointerTracker tracker, int x, int y,
long eventTime) {
super.onUpEvent(tracker, x, y, eventTime);
mIsFirstDownEventInsideSpaceBar = false;
}
protected void onCancelEvent(PointerTracker tracker, int x, int y,
long eventTime) {
super.onCancelEvent(tracker, x, y, eventTime);
mIsFirstDownEventInsideSpaceBar = false;
}
@Override
protected boolean dismissPopupKeyboard() {
mExtensionKeyboardAreaEntranceTime = -1;
mExtensionVisible = false;
return super.dismissPopupKeyboard();
}
public void showQuickTextPopupKeyboard(QuickTextKey key) {
Key popupKey = findKeyByKeyCode(KeyCodes.QUICK_TEXT);
popupKey.popupResId = key.getPopupKeyboardResId();
popupKey.externalResourcePopupLayout = popupKey.popupResId != 0;
super.onLongPress(key.getPackageContext(), popupKey, false, true);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
super.onSharedPreferenceChanged(sharedPreferences, key);
/*
* if (mAnimationLevel == AnimationsLevel.None &&
* mGestureSlideReachedAnimation != null) {
* mGestureSlideReachedAnimation = null; } else if (mAnimationLevel !=
* AnimationsLevel.None && mGestureSlideReachedAnimation == null) {
* createGestureSlideAnimation(); }
*/
}
public void openUtilityKeyboard() {
dismissKeyPreview();
if (mUtilityKey == null) {
mUtilityKey = new AnyKey(new Row(getKeyboard()),
getThemedKeyboardDimens());
mUtilityKey.codes = new int[]{0};
mUtilityKey.edgeFlags = Keyboard.EDGE_BOTTOM;
mUtilityKey.height = 0;
mUtilityKey.width = 0;
mUtilityKey.popupResId = R.xml.ext_kbd_utility_utility;
mUtilityKey.externalResourcePopupLayout = false;
mUtilityKey.x = getWidth() / 2;
mUtilityKey.y = getHeight()
- getThemedKeyboardDimens().getSmallKeyHeight();
}
super.onLongPress(getKeyboard().getKeyboardContext(), mUtilityKey, true, false);
mMiniKeyboard.setPreviewEnabled(true);
}
public void requestInAnimation(Animation animation) {
if (mAnimationLevel != AnimationsLevel.None)
mInAnimation = animation;
else
mInAnimation = null;
}
@Override
public void onDraw(Canvas canvas) {
final boolean keyboardChanged = mKeyboardChanged;
super.onDraw(canvas);
// switching animation
if (mAnimationLevel != AnimationsLevel.None && keyboardChanged
&& (mInAnimation != null)) {
startAnimation(mInAnimation);
mInAnimation = null;
}
// text pop out animation
if (mPopOutText != null && mAnimationLevel != AnimationsLevel.None) {
final int maxVerticalTravel = getHeight() / 2;
final long animationDuration = 1200;
final long currentAnimationTime = SystemClock.elapsedRealtime()
- mPopOutTime;
if (currentAnimationTime > animationDuration) {
mPopOutText = null;
} else {
final float animationProgress = ((float) currentAnimationTime)
/ ((float) animationDuration);
final float animationFactoredProgress = getPopOutAnimationInterpolator(animationProgress);
final int y = mPopOutStartPoint.y
- (int) (maxVerticalTravel * animationFactoredProgress);
final int x = mPopOutStartPoint.x;
final int alpha = 255 - (int) (255 * animationProgress);
if (FeaturesSet.DEBUG_LOG)
Log.d(TAG, "Drawing text popout '" + mPopOutText + "' at "
+ x + "," + y + " with alpha " + alpha
+ ". Animation progress is " + animationProgress
+ ", and factor progress is "
+ animationFactoredProgress);
// drawing
setPaintToKeyText(mPaint);
// will disappear over time
mPaint.setAlpha(alpha);
mPaint.setShadowLayer(5, 0, 0, Color.BLACK);
// will grow over time
mPaint.setTextSize(mPaint.getTextSize()
* (1.0f + animationFactoredProgress));
canvas.translate(x, y);
canvas.drawText(mPopOutText, 0, mPopOutText.length(), 0, 0,
mPaint);
canvas.translate(-x, -y);
// next frame
postInvalidateDelayed(1000 / 50);// doing 50 frames per second;
}
}
}
/*
* Taken from
* https://android.googlesource.com/platform/frameworks/base/+/refs
* /heads/master
* /core/java/android/view/animation/DecelerateInterpolator.java
*/
private float getPopOutAnimationInterpolator(float input) {
float result;
if (mPopOutAnimationFactor == 1.0f) {
result = (float) (1.0f - (1.0f - input) * (1.0f - input));
} else {
result = (float) (1.0f - Math.pow((1.0f - input),
2 * mPopOutAnimationFactor));
}
return result;
}
private CharSequence mPopOutText = null;
private long mPopOutTime = 0;
private final Point mPopOutStartPoint = new Point();
private float mPopOutAnimationFactor = 1.0f;
public void popTextOutOfKey(CharSequence text) {
if (TextUtils.isEmpty(text)) {
Log.w(TAG, "Call for popTextOutOfKey with missing text argument!");
return;
}
if (!AnyApplication.getConfig().workaround_alwaysUseDrawText())
return;// not doing it with StaticLayout
//performing "toString" so we'll have a separate copy of the CharSequence,
// and not the original object which I fear is a reference copy (hence may be changed).
mPopOutText = text.toString();
mPopOutTime = SystemClock.elapsedRealtime();
mPopOutStartPoint.x = mFirstTouchPont.x;
mPopOutStartPoint.y = mFirstTouchPont.y;
// it is ok to wait for the next loop.
postInvalidate();
}
}