如何在 Android KeyboardView 中实现涟漪效果?

How to implement ripple effect in Android KeyboardView?

我一直在尝试寻找一种方法来在按下 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 只是每个特定状态的颜色的可绘制对象,像这样(两者完全相同,只是颜色属性不同):

<?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 而不是 rippledrawable 但也不起作用。

哦,我实际上是在 Xamarin 而不是本机上开发应用程序 Android,但我认为这应该没有任何区别。

只是回答我自己的问题,希望对其他人有所帮助。

正如@alanv 在他的评论中提到的,Ripples 在 KeyboardView 中不起作用,因为它处理渲染和触摸交互的方式不同。

所以答案是否定的,无法在 Android KeyboardView 中使用波纹效果。

希望这可以避免其他人浪费时间试图找出如何向 KeyboardView 添加波纹:)

我对此的回应是:没有一种已经实现的方法(换句话说:一种简单的方法)来做到这一点。但是我们说的是开源技术,所以...

如果您有时间和耐心,可以从 original 创建一个自定义的 KeyboardView,以重新定义默认组件创建其具有波纹兼容布局的视图的方式。

"If you can't solve a problem, change the problem"(亨利·福特)。

也许这个,顺便说一句,如果有人需要的话,对可绘制对象有色调效果:

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>