我一直试图找到一种方法来在按下时对KeyboardView键实现涟漪效果。听起来很简单,但我已经尝试了所有添加涟漪的方法,这些方法适用于其他类型的视图(列表视图,按钮等),但根本没有成功。
我的目标是创建一个数字小键盘,看起来像股票计算器应用程序中的键盘,默认情况下在Lollipop OS中出现:
在GitHub(https://github.com/numixproject/com.numix.calculator)中有一个类似的计算器应用程序,它的键盘有一个连锁反应,但是当我读到代码时,它似乎是使用数字键的按钮而不是键盘视图。
我希望使用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>
然后在drawable-v21文件夹中的numeric_keypad_ripple.xml:
<?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只是每个特定状态的颜色的drawable,就像这样(两者完全相同,只是颜色属性不同):
<?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作为选择器中的按下状态drawable而不是包装选择器,但它仍然无法正常工作。还尝试使用?android:attr/selectableItemBackground
而不是rippledrawable但也无效。
哦,我实际上是在Xamarin而不是原生Android上开发应用程序,但我认为这不应该有任何区别。
感谢任何帮助,谢谢!
答案 0 :(得分:5)
回答我自己的问题,这样可以帮助别人。
正如@alanv在评论中提到的,Ripples因为处理渲染和触摸交互的方式不同而无法在KeyboardView中工作。
答案是否定的,在Android KeyboardView中无法使用涟漪效应。
希望这可以节省其他人浪费时间试图弄清楚如何向KeyboardView添加涟漪:)
答案 1 :(得分:1)
我对此的回答是:没有一种已经实现的方式(换句话说:一种简单的方法)来做到这一点。但我们正在谈论开源技术,所以......
如果您有时间和耐心,可以从original创建自定义KeyboardView,以重新定义默认组件使用纹波兼容布局创建视图的方式。
“如果你无法解决问题,请改变问题”(亨利福特)。
答案 2 :(得分:0)
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>