【问题标题】:How to implement ripple effect in Android KeyboardView?如何在 Android KeyboardView 中实现波纹效果?
【发布时间】:2015-03-15 23:58:43
【问题描述】:

我一直在尝试找到一种方法来在 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>

然后是 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 上开发应用程序,但我想这不会有任何区别。

【问题讨论】:

  • 涟漪只有在设置为视图背景时才能正常工作(以及其他一些实例,如 FrameLayout 的前景和 AbsListView 的选择器)。由于它处理渲染和触摸交互的方式,它们在 KeyboardView 中不起作用。
  • 啊我明白了,所以确实不可能在 KeyboardView 上使用波纹。我怀疑是这样,但希望它仍然可以通过某些方式完成。无论如何,感谢您的确认,@alanv!

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


【解决方案1】:

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

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

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

希望这可以避免其他人浪费时间试图弄清楚如何将波纹添加到 KeyboardView :)

【讨论】:

    【解决方案2】:

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

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

    “如果你不能解决问题,就改变问题”(亨利福特)。

    【讨论】:

      【解决方案3】:

      也许这个,顺便说一句,如果有人需要它,对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>
      

      【讨论】:

      • 可绘制对象在哪里
      • R.drawable.sym_keyboard_delete
      • 您将例如 pngs 放在您的可绘制文件夹中。确保你有正确的名字。例如 sym_keyboard_delete.png
      猜你喜欢
      • 1970-01-01
      • 2023-03-31
      • 2015-02-08
      • 2017-05-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多