SolidlSnake
@SolidlSnake
Ваш дружелюбный сосед

Ripple эффект с произвольными формами?

Ищу помощи у более продвинутых в деле людей.

Необходимо реализовать ripple-эффект на форме круга:
<shape  xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval" android:dither="true">
        <solid android:color="#cccccc"/>
</shape>

Для SDK версии 21+ проблем нет, мы используем простой тег ripple, но эффект так же необходимо реализовать и на старых устройствах (SDK 15+).

В поисках решения наткнулся на две библиотеки: RippleEffect и RippleDrawable, причем вторая сразу заявляет, что может реализовать эффект на произвольных формах, однако сделав все как описано в проекте на github'е приложение попросту вылетает с ошибкой на SDK ниже 21 версии, как я понял дело все так же в ripple теге, который попросту не поддерживается, но используется для библиотеки. Возможно проблема в том, что я использую фрагмент, а в примере все сделано сходу на активити, что вроде как не есть хорошая практика.

У первой библиотеки из кастомных форм есть только небольшой пример самого drawable файла, даже без реализации, однако подобный вариант не сработал, эффект ряби все так же выезжает за пределы круга создавая квадрат.

Нашлась еще парочка библиотек, но у них тоже не нашлось ничего про кастомные формы.

Возможно уже кто-то реализовывал подобное для старых устройств и может помочь?
  • Вопрос задан
  • 638 просмотров
Решения вопроса 1
enq3
@enq3
Android engineer at #ITX5
Я делал контролы с полной отрисовкой всей графики на Canvas. И есть ограничение, придется пожертвовать аппаратным ускорением, но я визуально разницу не особо заметил.
Полный код класса:
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.ValueAnimator;

public class RippleCircle extends View {
    // ===========================================================
    // Constants
    // ===========================================================
    private static final String TAG = RippleCircle.class.getSimpleName();
    private static final int RIPPLE_COLOR = 0x66000000;
    private static final int RIPPLE_FADE_COLOR = 0x22000000;
    private static final int FAB_COLOR = 0xffcccccc;
    private static final long ANIM_DURATION = 250;
    protected static int FLAGS = Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG;
    // ===========================================================
    // Fields
    // ===========================================================
    private Paint mButtonBg;
    private Paint mRippleButtonBg;
    private Path main;
    private ValueAnimator animatorRipple;
    private float selRadius;
    private float radius;
    private float centerY;
    private float centerX;
    private float rippleX;
    private float rippleY;
    private boolean isButtonTouchDown;
    private boolean isProgress;     
    // ===========================================================
    // Constructors
    // ===========================================================
    public RippleCircle(Context context) {
        super(context);
        init();
    }

    public RippleCircle(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawCircle(centerX, centerY, radius, mButtonBg);
        canvas.clipPath(main);
        canvas.drawCircle(rippleX, rippleY, selRadius, mRippleButtonBg);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        float size = Math.min(w, h);
        centerY = size / 2;
        centerX = size / 2;
        radius = size / 2;
        main.addCircle(centerX, centerY, radius, Path.Direction.CCW);
        animatorRipple.setFloatValues(0, radius);
    }
    // ===========================================================
    // Inner Methods
    // ===========================================================
    private void init() {
        main = new Path();

        animatorRipple = ValueAnimator.ofFloat(0, 0);
        animatorRipple.setDuration(ANIM_DURATION);
        animatorRipple.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                selRadius = (Float) animation.getAnimatedValue();
                if (selRadius / radius <= 1 && selRadius / radius >= 0)
                    mRippleButtonBg.setColor(blendColors(RIPPLE_COLOR, RIPPLE_FADE_COLOR, 1 - selRadius / radius));
                invalidate();
            }
        });

        animatorRipple.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                if (!isButtonTouchDown) {
                    selRadius = 0;
                    invalidate();
                    isProgress = false;
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
            setLayerType(LAYER_TYPE_SOFTWARE, null);
        }
        mButtonBg = new Paint(FLAGS);
        mButtonBg.setColor(FAB_COLOR);

        mRippleButtonBg = new Paint(FLAGS);
        mRippleButtonBg.setColor(RIPPLE_COLOR);

        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {

                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        rippleX = event.getX();
                        rippleY = event.getY();
                        if (isPointInCircle(centerX, centerY, event.getX(), event.getY(), radius)) {
                            isButtonTouchDown = true;
                            if (!isProgress) {
                                rippleX = event.getX();
                                rippleY = event.getY();
                                isProgress = true;
                                float max = distance(centerX, centerY, rippleX, rippleY) + radius;
                                animatorRipple.setFloatValues(0, max);
                                animatorRipple.start();
                            }
                        }
                        break;
                    case MotionEvent.ACTION_UP:
                        isButtonTouchDown = false;
                        if (!animatorRipple.isRunning()) {
                            selRadius = 0;
                            invalidate();
                            isProgress = false;
                        }
                        break;
                }
                return true;
            }
        });
    }

    private boolean isPointInCircle(float centerX, float centerY, float x, float y, float radius) {
        return distance(centerX, centerY, x, y) <= radius;
    }

    private float distance(final float pX1, final float pY1, final float pX2, final float pY2) {
        final float dX = pX2 - pX1;
        final float dY = pY2 - pY1;
        return (float) Math.sqrt((dX * dX) + (dY * dY));
    }

    private int blendColors(int color1, int color2, float ratio) {
        final float inverseRation = 1f - ratio;
        float a = (Color.alpha(color1) * ratio) + (Color.alpha(color2) * inverseRation);
        float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation);
        float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation);
        float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation);
        return Color.argb((int) a, (int) r, (int) g, (int) b);
    }
    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================
}

Используем в разметке:
<yourPackage.RippleCircle
        android:layout_width="56dp"
        android:layout_height="56dp"
        android:layout_centerInParent="true"/>

По аналогии можно добавить какую угодно форму, я делал прямоугольник со скругленными углами и окружность.
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы