Как реализовать эффект ряби в Android KeyboardView?
Я пытался найти способ реализовать эффект ряби на клавишах KeyboardView при их нажатии. Звучит достаточно просто, но я попробовал все способы добавления пульсации, которые работают на других типах представления (просмотр списка, кнопка и т. Д.), Но безуспешно.
Моя цель состоит в том, чтобы создать цифровую клавиатуру, которая будет выглядеть как клавиатура в приложении калькулятора, которое по умолчанию поставляется в Lollipop OS:
В GitHub есть похожее приложение для калькулятора ( https://github.com/numixproject/com.numix.calculator), которое имеет волновой эффект на клавиатуре, но когда я читаю код, кажется, что он использует кнопки для цифровых клавиш вместо KeyboardView.
Я надеюсь, что волновой эффект выполним с KeyboardView, так как мое приложение уже имеет встроенную цифровую клавиатуру, использующую KeyboardView, и я не хотел бы менять ее для использования кнопок.
Я пытался добавить пульсацию как keyBackground
атрибут из стиля, как это:
<android.inputmethodservice.KeyboardView
android:id="@+id/numeric_keypad"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:focusable="true"
android:focusableInTouchMode="true"
style="@style/my_numeric_keypad" />
Тогда в themes.xml:
<style name="my_numeric_keypad">
<item name="android:keyTextSize">30dp</item>
<item name="android:fontFamily">roboto</item>
<item name="android:keyBackground">@drawable/numeric_keypad_ripple</item>
<item name="android:keyTextColor">@android:color/white</item>
</style>
а затем numeric_keypad_ripple.xml в папке drawable-v21:
<?xml version="1.0" encoding="UTF-8" ?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:colorControlHighlight">
<item android:drawable="@drawable/numeric_keypad_states"/>
</ripple>
numeric_keypad_states.xml - это старый селектор с нажатым состоянием (раньше он был объявлен прямым как keyBackground
атрибуты):
<?xml version="1.0" encoding="UTF-8" ?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/numeric_keypad_pressed" />
<item android:drawable="@drawable/numeric_keypad_normal" />
</selector>
numeric_keypad_pressed.xml и numeric_keypad_normal.xml - это просто отрисовка с цветом для каждого конкретного состояния, например, так (оба одинаковы, различаются только по атрибуту цвета):
<?xml version="1.0" encoding="UTF-8" ?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item>
<shape android:shape="rectangle">
<solid android:color="@color/numeric_keypad_pressed_color"/>
</shape>
</item>
</layer-list>
Я думал, что мой подход выше будет работать; но это не так. В моем устройстве Lollipop, когда я нажимаю на клавиатуре, он просто показывает обычный нажатый цвет без каких-либо колебаний, без разницы с моей старой реализацией без пульсации. Я пытался удалить слой, потому что думал, что он как-то перекрывается с рябью, но все равно не работает. Добавление маски к пульсации также не работает.
Я также попытался использовать rippledrawable в качестве отрисовки нажатого состояния в селекторе вместо переноса селектора, но он все еще не работает. Также пытался использовать ?android:attr/selectableItemBackground
вместо волнистой, но также не работает.
И, на самом деле, я разрабатываю приложение на Xamarin вместо нативного Android, но это не должно иметь никакого значения, я полагаю.
Любая помощь приветствуется, спасибо заранее!
3 ответа
Просто отвечаю на мой вопрос, чтобы помочь другим.
Как отметил @alanv в своем комментарии, Ripples не будут работать в KeyboardView из-за его другого способа обработки рендеринга и сенсорного взаимодействия.
Так что ответ - нет, невозможно использовать волновой эффект в Android KeyboardView.
Надеюсь, что это может спасти других от потери времени, пытаясь выяснить, как добавить рябь в KeyboardView:)
Мой ответ на это: нет уже реализованного способа (другими словами: легкий способ) сделать это. Но мы говорим о технологиях с открытым исходным кодом, так что...
Если у вас есть немного времени и терпения, вы можете создать собственный KeyboardView из оригинального, чтобы переопределить способ, которым компонент по умолчанию создает свои представления с пульсирующим совместимым макетом.
"Если вы не можете решить проблему, измените проблему" (Генри Форд).
Может это, кстати, с эффектом оттенка на рисовалках, если кому-то это нужно:
import java.util.List;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.ObjectAnimator;
import com.nineoldandroids.animation.ValueAnimator;
import com.nineoldandroids.animation.Animator.AnimatorListener;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.inputmethodservice.Keyboard;
import android.inputmethodservice.Keyboard.Key;
import android.os.Build;
import android.inputmethodservice.KeyboardView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.graphics.Region.Op;
public class RippleKeyboardView extends KeyboardView
{
private Bitmap tintedBitmap,tintedBitmap2;
private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
private Paint mPaint = new Paint();
private int tintcolor = 0xffff0000;
private BitmapDrawable mDrawable,mDrawable2;
private int invalidatekeyindex = -1;
private static final int NOT_A_KEY = -1;
private float animationcircleProgress;
private Paint circlePaint = new Paint();
private ValueAnimator circleAnimator;
private static final int ANIMATION_TIME_ID = android.R.integer.config_shortAnimTime;
private float ripplex,rippley;
private float textoffsety;
private int keytextsize;
private int mLabelTextSize;
private Rect mDirtyRect = new Rect();
private boolean mDrawPending;
private Bitmap mBuffer;
private boolean mKeyboardChanged;
private Canvas mCanvas;
private Keyboard mKeyboard;
private List<Key> mkeys;
private Rect tobeinvalidated=new Rect();
@SuppressWarnings("deprecation")
@TargetApi(21)
public RippleKeyboardView(Context context, AttributeSet attrs)
{
super(context, attrs);
if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
{
tintedBitmap = generateIconBitmaps(getBitmapFromDrawable(getResources().getDrawable(R.drawable.sym_keyboard_delete,null)));
tintedBitmap2 = generateIconBitmaps(getBitmapFromDrawable(getResources().getDrawable(R.drawable.sym_keyboard_feedback_return,null)));
}
else
{
tintedBitmap = generateIconBitmaps(getBitmapFromDrawable(getResources().getDrawable(R.drawable.sym_keyboard_delete)));
tintedBitmap2 = generateIconBitmaps(getBitmapFromDrawable(getResources().getDrawable(R.drawable.sym_keyboard_feedback_return)));
}
mDrawable = new BitmapDrawable(getResources(), tintedBitmap);
mDrawable2 = new BitmapDrawable(getResources(), tintedBitmap2);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.RippleKeyboardView, 0, 0);
keytextsize = a.getDimensionPixelSize(R.styleable.RippleKeyboardView_keytxtsize,60);
mLabelTextSize = a.getDimensionPixelSize(R.styleable.RippleKeyboardView_lblsize, 40);
a.recycle();
animationcircleProgress = 0;
final int pressedAnimationTime = getResources().getInteger(ANIMATION_TIME_ID);
circleAnimator = ObjectAnimator.ofFloat(this, "animationlayerProgress", 100, 0f);
circleAnimator.setDuration(pressedAnimationTime);
circleAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
mPaint.setColor(0xff000000);
mPaint.setAntiAlias(true);
mPaint.setTextAlign(Align.CENTER);
mPaint.setAlpha(255);
circlePaint.setColor(0x77989898);
super.setPreviewEnabled(false);
invalidateAllKeys();
}
private Bitmap getBitmapFromDrawable(Drawable drawable)
{
if (drawable == null)
return null;
if (drawable instanceof BitmapDrawable)
return ((BitmapDrawable) drawable).getBitmap();
try
{
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
catch (OutOfMemoryError e)
{
return null;
}
}
private Bitmap generateIconBitmaps(Bitmap origin)
{
if (origin == null)
return null;
Bitmap bmp = origin.copy(Bitmap.Config.ARGB_8888, true);
Canvas canvas = new Canvas(bmp);
canvas.drawColor(tintcolor & 0x00ffffff | 0xff000000 , PorterDuff.Mode.SRC_IN);
origin.recycle();
return bmp;
}
@Override
public void onSizeChanged(int w, int h, int oldw, int oldh)
{
super.onSizeChanged(w, h, oldw, oldh);
mBuffer = null;
}
@Override
public void setKeyboard(Keyboard keyboard)
{
super.setKeyboard(keyboard);
mKeyboardChanged = true;
mKeyboard = keyboard;
invalidateAllKeys();
}
@Override
public void onDraw(Canvas canvas)
{
if (mDrawPending || mBuffer == null || mKeyboardChanged)
onBufferDraw();
canvas.drawBitmap(mBuffer, 0, 0, null);
}
public void onBufferDraw()
{
if (mBuffer == null || mKeyboardChanged)
{
if (mBuffer == null || mKeyboardChanged && (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight()))
{
final int width = Math.max(1, getWidth());
final int height = Math.max(1, getHeight());
mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mBuffer);
mkeys = getKeyboard().getKeys();
}
invalidateAllKeys();
mKeyboardChanged = false;
}
final Canvas canvas = mCanvas;
canvas.clipRect(mDirtyRect, Op.REPLACE);
canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
if (mKeyboard == null)
return;
List<android.inputmethodservice.Keyboard.Key> keys = mkeys;
for (android.inputmethodservice.Keyboard.Key key : keys)
{
canvas.translate(key.x , key.y );
if (key.codes[0] == 67)
{
final int drawableX = (key.width - 0 - key.icon.getIntrinsicWidth()) / 2 + 0;
final int drawableY = (key.height - 0 - key.icon.getIntrinsicHeight()) / 2 + 0;
canvas.translate(drawableX, drawableY);
mDrawable.setBounds(0, 0, key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight());
mDrawable.draw(canvas);
canvas.translate(-drawableX, -drawableY);
}
else if (key.codes[0] == 66)
{
final int drawableX = (key.width - 0 - key.icon.getIntrinsicWidth()) / 2 + 0;
final int drawableY = (key.height - 0 - key.icon.getIntrinsicHeight()) / 2 + 0;
canvas.translate(drawableX, drawableY);
mDrawable2.setBounds(0, 0, key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight());
mDrawable2.draw(canvas);
canvas.translate(-drawableX, -drawableY);
}
else
{
String label = key.label.toString();
if (label.length()>1 && key.codes.length < 2)
{
mPaint.setTextSize(mLabelTextSize);
mPaint.setTypeface(Typeface.DEFAULT_BOLD);
}
else
{
mPaint.setTextSize(keytextsize);
mPaint.setTypeface(Typeface.DEFAULT);
}
textoffsety= (mPaint.getTextSize() - mPaint.descent()) / 2;
canvas.drawText(label,(key.width ) / 2,(key.height ) / 2+ textoffsety, mPaint);
}
canvas.translate(-key.x , -key.y );
}
if (invalidatekeyindex!=-1)
canvas.drawCircle(ripplex , rippley, getAnimationlayerProgress(), circlePaint);
mDrawPending = false;
mDirtyRect.setEmpty();
}
@Override
public void invalidateAllKeys()
{
mDirtyRect.union(0, 0, getWidth(), getHeight());
mDrawPending = true;
}
@Override
public void invalidateKey(int keyIndex)
{
List<android.inputmethodservice.Keyboard.Key> mKeys = mkeys;
if (mKeys == null) return;
if (keyIndex < 0 || keyIndex >= mKeys.size())
return;
final Key key = mKeys.get(keyIndex);
mDirtyRect.union(key.x , key.y , key.x + key.width , key.y + key.height );
invalidate(key.x , key.y , key.x + key.width , key.y + key.height );
}
@Override
public boolean performClick()
{
super.performClick();
return true;
}
@Override
public boolean onTouchEvent(MotionEvent me)
{
boolean ret = super.onTouchEvent(me);
final int action = me.getAction();
int primaryIndex = NOT_A_KEY;
if (action == MotionEvent.ACTION_UP)
{
int [] nearestKeyIndices=getKeyboard().getNearestKeys((int)me.getX(),(int) me.getY());
final int keyCount = nearestKeyIndices.length;
List<android.inputmethodservice.Keyboard.Key> keys = mkeys;
for (int i = 0; i < keyCount; i++)
{
final android.inputmethodservice.Keyboard.Key key = keys.get(nearestKeyIndices[i]);
boolean isInside = key.isInside((int)me.getX(),(int)me.getY());
if (isInside)
primaryIndex = nearestKeyIndices[i];
}
}
if (primaryIndex!=NOT_A_KEY)
{
DrawCustomRipple(primaryIndex);
}
performClick();
return ret;
}
private void DrawCustomRipple(int keyindex)
{
if (circleAnimator.isRunning())
{
tobeinvalidated.set((int)(ripplex-animationcircleProgress-4),
(int)(rippley-animationcircleProgress-4),
(int)(ripplex+animationcircleProgress+4),
(int)(rippley+animationcircleProgress+4));
circleAnimator.removeAllListeners();
invalidatekeyindex = -1;
mDrawPending = true;
invalidatekeyindex = -1;
mDirtyRect.union(tobeinvalidated);
invalidate(tobeinvalidated);
}
invalidatekeyindex=keyindex;
List<android.inputmethodservice.Keyboard.Key> keys = mkeys;
final android.inputmethodservice.Keyboard.Key cKey=keys.get(invalidatekeyindex);
if (cKey.label == null && cKey.codes[0] != 67 && cKey.codes[0] != 66)
return;
circleAnimator.setFloatValues(30.0f,100.0f);
circleAnimator.addListener(new AnimatorListener()
{
@Override
public void onAnimationStart(Animator animation)
{
final Rect bounds=new Rect();
if (cKey.label!=null)
{
String label = cKey.label.toString();
mPaint.getTextBounds(label, 0, 1, bounds);
}
ripplex =cKey.x+(cKey.width ) / 2+bounds.width()/2;
rippley = cKey.y + (cKey.height ) / 2;
}
@Override
public void onAnimationEnd(Animator animation)
{
invalidatekeyindex = -1;
mDrawPending = true;
tobeinvalidated.set((int)(ripplex-animationcircleProgress-4),
(int)(rippley-animationcircleProgress-4),
(int)(ripplex+animationcircleProgress+4),
(int)(rippley+animationcircleProgress+4));
mDirtyRect.union(tobeinvalidated);
onBufferDraw();
invalidate(tobeinvalidated);
}
@Override
public void onAnimationCancel(Animator animation)
{
}
@Override
public void onAnimationRepeat(Animator animation)
{
}
});
circleAnimator.start();
}
public float getAnimationlayerProgress()
{
return animationcircleProgress;
}
public void setAnimationlayerProgress(float animationlayerProgress)
{
this.animationcircleProgress = animationlayerProgress;
tobeinvalidated.set((int)(ripplex-animationcircleProgress-4),
(int)(rippley-animationcircleProgress-4),
(int)(ripplex+animationcircleProgress+4),
(int)(rippley+animationcircleProgress+4));
mDirtyRect.union((int)(ripplex-animationcircleProgress-4),
(int)(rippley-animationcircleProgress-4),
(int)(ripplex+animationcircleProgress+4),
(int)(rippley+animationcircleProgress+4) );
mDrawPending = true;
invalidate( (int)(ripplex-animationcircleProgress-4),
(int)(rippley-animationcircleProgress-4),
(int)(ripplex+animationcircleProgress+4),
(int)(rippley+animationcircleProgress+4));
}
}
и добавьте этот файл attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE resources >
<resources>
<declare-styleable name="RippleKeyboardView">
<attr name="keytxtsize" format="dimension" />
<attr name="lblsize" format="dimension" />
</declare-styleable>
</resources>