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

时间:2015-01-17 13:24:35

标签: android android-5.0-lollipop material-design rippledrawable

我一直试图找到一种方法来在按下时对KeyboardView键实现涟漪效果。听起来很简单,但我已经尝试了所有添加涟漪的方法,这些方法适用于其他类型的视图(列表视图,按钮等),但根本没有成功。

我的目标是创建一个数字小键盘,看起来像股票计算器应用程序中的键盘,默认情况下在Lollipop OS中出现:

Stock Lollipop calculator

在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上开发应用程序,但我认为这不应该有任何区别。

感谢任何帮助,谢谢!

3 个答案:

答案 0 :(得分:5)

回答我自己的问题,这样可以帮助别人。

正如@alanv在评论中提到的,Ripples因为处理渲染和触摸交互的方式不同而无法在KeyboardView中工作。

答案是否定的,在Android KeyboardView中无法使用涟漪效应。

希望这可以节省其他人浪费时间试图弄清楚如何向KeyboardView添加涟漪:)

答案 1 :(得分:1)

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

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

“如果你无法解决问题,请改变问题”(亨利福特)。

答案 2 :(得分:0)

也许这就是,如果有人需要它,可以通过对drawables的色调效果:

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>