How to implement ripple effect in Android KeyboardView?
Asked Answered
R

3

5

I've been trying to find a way to implement ripple effect on KeyboardView keys when they are pressed. Sounds simple enough, but I've tried all the ways of adding ripple that work on other types of view (listview, button, etc) with no success at all.

My aim is to create a numeric keypad that looks like the keypad in the stock Calculator app which comes by default in Lollipop OS:

Stock Lollipop calculator

There's a similar calculator app in GitHub (https://github.com/numixproject/com.numix.calculator) which has a ripple effect on its keypad, but as I read the code seems like it's using buttons for the numeric keys instead of a KeyboardView.

I hope ripple effect is doable with KeyboardView, as my app already has the implementation of a custom numeric keyboard using KeyboardView, and I'd hate to have to change it to use buttons.

I've tried adding the ripple as the keyBackground attribute from the style like this:

<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" />

Then in 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>

and then numeric_keypad_ripple.xml in drawable-v21 folder:

<?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 is the old selector with the pressed state (it used to be declared straight as the keyBackground attribute):

<?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 and numeric_keypad_normal.xml are just the drawable with the color for each specific state, like this (both are exactly the same, just differ in the color attribute):

<?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>

I thought my approach above would work; but it doesn't. In my Lollipop device, when I press the keypad it just shows the normal pressed color without any ripple at all, no difference to my old implementation without the ripple. I've tried removing the layer because I thought it's somehow overlapping with the ripple, but still doesn't work. Adding mask to the ripple also doesn't work.

I've also tried using the rippledrawable as the pressed state drawable in the selector instead of wrapping the selector, but it's still not working. Also tried using ?android:attr/selectableItemBackground instead of a rippledrawable but also doesn't work.

And oh, I'm actually developing the app on Xamarin instead of native Android, but that shouldn't make any difference I suppose.

Radicalism answered 17/1, 2015 at 13:24 Comment(2)
Ripples only work correctly when set as View backgrounds (and a few other instances, like FrameLayout's foreground and AbsListView's selector). They won't work in KeyboardView because of the way it handles rendering and touch interaction.Bedlamite
Ah I see, so it is not possible indeed to use ripple on KeyboardView. I suspected so but was hoping it still could be done in some ways. Thanks for the confirmation anyway, @alanv!Radicalism
R
7

Just answering my own question so it may help others.

As @alanv mentioned in his comment, Ripples won't work in KeyboardView because of its different way of handling rendering and touch interaction.

So the answer is no, it's not possible to use ripple effect in Android KeyboardView.

Hope this can save others from wasting time trying to figure out how to add ripple to KeyboardView :)

Radicalism answered 26/3, 2015 at 12:36 Comment(0)
S
3

My response to that is: there is not an already implemented way (in another words: an easy way) to do that. But we are talking about open source technology, so...

If you have some time and patience, you can create a custom KeyboardView from original to redefines the way that default component create its views with a ripple compatible layout.

"If you can't solve a problem, change the problem" (Henry Ford).

Sikata answered 18/6, 2015 at 8:57 Comment(0)
S
-1

maybe this , by the way with tint effect on drawables, if someone needs it:

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));
}

}

and add this 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>
Seibel answered 3/2, 2016 at 21:52 Comment(3)
where are drawablesOperculum
R.drawable.sym_keyboard_deleteOperculum
you put for example pngs in your drawable folder. make sure you have the right name. for example sym_keyboard_delete.pngSeibel

© 2022 - 2024 — McMap. All rights reserved.