RippleDrawable для Pre-L пристроїв

image

Доброго часу доби!

Ті, хто стежив за Google IO/2014, знають про новий Material Design і нові можливості. Однією з них є пульсуючий ефект при натисканні. Вчора я вирішив його портувати для старих пристроїв.

В Android L перейшли на новий ефект — пульсування, він використовується за замовчуванням у відповідь реакції на дотик. Тобто при торканні на екран з'являється великий зникаючий (fades) овал з розміром батьківського шару і разом з ним зростає коло в точці дотику. Ця анімація мене вдохвновила використовувати в своєму проекті і я вирішив спробувати його зробити.



Приклади анімації на Google Design.

Створимо клас RippleDrawable з допоміжним класом Circle, який буде допомагати нам малювати кола:

class RippleDrawable extends Drawable{

static final class Circle{
float cx; // x координата центра кола
float cy; // y координата центра кола
float radius; // радіус кола

/**
* Малюємо коло
* 
* @param canvas Canvas для малювання
* @param paint Paint з описом як стилізувати наш коло
*/
public void draw(Canvas canvas, Paint paint){
canvas.drawCircle(cx, cy, radius, paint);
}
}
}


Допоміжний елемент Circle нам знадобиться для збереження точки дотику. Тепер нам знадобиться два кола: фонової коло, який покриє всього батька та коло поменше, для відображення точки дотику. Ах, так, і ще оголосимо константи, значення анімації за замовчуванням буде 250мс, радіус кола за замовчуванням в 150px. У скільки разів збільшувати фонової коло, примітки, усі цифри взяті на око.

class RippleDrawable extends Drawable{

static final int DEFAULT_ANIM_DURATION = 250;
static final float END_RIPPLE_TOUCH_RADIUS = 150f;
static final float END_SCALE = 1.3f;

// Коло для дотику
Circle mTouchRipple;
// Фонової коло
Circle mBackgroundRipple;

// Стилі для промальовування "кола для дотику"
Paint mRipplePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// Стилі для фонового кола
Paint mRippleBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);


Прапор Paint.ANTI_ALIAS_FLAG призначений для згладжування, щоб кола були колами, а не фіг зрозумій мазаниною якийсь, тепер ініціалізуємо наші змінні в окремому методі, вкажемо, що стиль забарвлення «заливка» і створимо кола, далі викличемо його в конструкторі:


void initRippleElements(){
mTouchRipple = new Circle();
mBackgroundRipple = new Circle();

mRipplePaint.setStyle(Paint.Style.FILL);
mRippleBackgroundPaint.setStyle(Paint.Style.FILL);
}


Готово, напевно перейдемо до найцікавішого обробці торкань, додамо в наш клас інтерфейс OnTouchListener:

class RippleDrawable extends Drawable implements OnTouchListener{

...

@Override
public boolean onTouch(View v, MotionEvent event) {
// Зберігаємо досконале дію
final int action = event.getAction();
// і в залежності від дії виконуємо методи
switch (action){
// Користувач торкнувся екрану
case MotionEvent.ACTION_DOWN:
onFingerDown(v, event.getX(), event.getY());
// Для того що б події View спрацьовували нам треба його викликати
return v.onTouchEvent(event);
// Користувач рухає пальцем по екрану (це продовження торкання)
case MotionEvent.ACTION_MOVE:
onFingerMove(event.getX(), event.getY());
break;
// Користувач убал свій пальчик
case MotionEvent.ACTION_UP:
onFingerUp();
break;
}
return false;
}

...


При торканні по екрану спочатку ми зберігаємо координати дотику по колам і розмір View (для фонового кола), потім стартуємо анимашку, якщо вона раніше не стартувала. До речі кажучи, в обох кіл є opacity (прозорість), я їх визначив як 100 для фонового кола і від 160 до 40 для маленького кружальця. Всі цифри знову ж були взяті зі стелі (пильне око) (якщо хто не зрозумів, цифри від 0 до 255 argb).

int mViewSize = 0;

void onFingerDown(View v, float x, float y){
mTouchRipple.cx = mBackgroundRipple.cx = x;
mTouchRipple.cy = mBackgroundRipple.cy = y;
mTouchRipple.radius = mBackgroundRipple.radius = 0f;
mViewSize = Math.max(v.getWidth(), v.getHeight());

// Якщо минула анімація закінчилася створимо нову
if(mCurrentAnimator == null){
// Вкажемо стан за замовчуванням для нашого фонового кола
// тобто відновимо його прозорість на дефолтний
mRippleBackgroundPaint.setAlpha(RIPPLE_BACKGROUND_ALPHA);

// Створимо анимашку, тут константа CREATE_TOUCH_RIPPLE це геттери і сетери
// для відправки стану анімації
mCurrentAnimator = ObjectAnimator.ofFloat(this, CREATE_TOUCH_RIPPLE, 0f, 1f);
mCurrentAnimator.setDuration(DEFAULT_ANIM_DURATION);
}

// Якщо анімація відіграє нічого не робимо чекаємо поки закінчиться
if(!mCurrentAnimator.isRunning()){
mCurrentAnimator.start();
}
}

// Збереження стану, необхідно для ObjectAnimator
float mAnimationValue;

/**
* ObjectAnimator викликає цю функцію
* 
* @param value стан анімації від 0 до 1
*/
void createTouchRipple(float value){
mAnimationValue = value;

// step by step збільшуємо кола, мінімальний радіус 40px 
mTouchRipple.radius = 40f + (mAnimationValue * (END_RIPPLE_TOUCH_RADIUS - 40f));
mBackgroundRipple.radius = mAnimationValue * (mViewSize * END_SCALE);

// і плавне исчезновние ще не появивщихся кіл,
// тобто при старті анімації їх opacity максимальна, 
// і в кінці вона падає до мінімального значення
int min = RIPPLE_TOUCH_MIN_ALPHA;
int max = RIPPLE_TOUCH_MAX_ALPHA;
int alpha = min + (int) (mAnimationValue * (max - min));
mRipplePaint.setAlpha((max min) - alpha);

// Перемальовуємо
invalidateSelf();
}



Тепер, якщо користувач торкнувся, у нас з'являються 2 кола, інтерфейс і фонової, але не йдуть, і навіть не рухаються при русі пальця, пора виправляти:

void onFingerMove(float x, float y){
mTouchRipple.cx = x;
mTouchRipple.cy = y;

invalidateSelf();
}


Перевірте, рухається тепер кружечок-то, а?

Логіка при знятті з пальця курка, тобто з екрану. Якщо анімація була запущена, ми повинні її закінчити і привести до кінцевого стану, далі запустити анімацію, зникнення кіл, де користувальницький коло буде збільшуватися і зникати одночасно, приступимо:


void onFingerUp(){
// Закінчуємо анімацію
if(mCurrentAnimator != null) {
mCurrentAnimator.end();
mCurrentAnimator = null;
createTouchRipple(1f);
}

// Створюємо нову, і при завершенні очищаємо її
mCurrentAnimator = ObjectAnimator.ofFloat(this, DESTROY_TOUCH_RIPPLE, 0f, 1f);
mCurrentAnimator.setDuration(DEFAULT_ANIM_DURATION);
mCurrentAnimator.addListener(new SimpleAnimationListener(){
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mCurrentAnimator = null;
}
});
mCurrentAnimator.start();
}

void destroyTouchRipple(float value){
// Зберігаємо стан анімації
mAnimationValue = value;

// Збільшуємо радіус кола до фонового радіуса
mTouchRipple.radius = END_RIPPLE_TOUCH_RADIUS + (mAnimationValue * (mViewSize * END_SCALE));

// та одночасно в обох кіл створюємо ефект згасання
mRipplePaint.setAlpha((int) (RIPPLE_TOUCH_MIN_ALPHA - (mAnimationValue * RIPPLE_TOUCH_MIN_ALPHA)));
mRippleBackgroundPaint.setAlpha
((int) (RIPPLE_BACKGROUND_ALPHA - (mAnimationValue * RIPPLE_BACKGROUND_ALPHA)));

// ну і як же без перемальовування?
invalidateSelf();
}


Анімація готова, можемо сміливо перевіряти.

Вихідний код

import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.Property;
import android.view.MotionEvent;
import android.view.View;

public class RippleDrawable extends Drawable implements View.OnTouchListener{

static final Property<RippleDrawable, Float> CREATE_TOUCH_RIPPLE =
new FloatProperty<RippleDrawable>("createTouchRipple") {
@Override
public void setValue(RippleDrawable object, float value) {
object.createTouchRipple(value);
}

@Override
public Float get(RippleDrawable object) {
return object.getAnimationState();
}
};

static final Property<RippleDrawable, Float> DESTROY_TOUCH_RIPPLE =
new FloatProperty<RippleDrawable>("destroyTouchRipple") {
@Override
public void setValue(RippleDrawable object, float value) {
object.destroyTouchRipple(value);
}

@Override
public Float get(RippleDrawable object) {
return object.getAnimationState();
}
};

static final int DEFAULT_ANIM_DURATION = 250;
static final float END_RIPPLE_TOUCH_RADIUS = 150f;
static final float END_SCALE = 1.3f;

static final int RIPPLE_TOUCH_MIN_ALPHA = 40;
static final int RIPPLE_TOUCH_MAX_ALPHA = 120;
static final int RIPPLE_BACKGROUND_ALPHA = 100;

Paint mRipplePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Paint mRippleBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

Circle mTouchRipple;
Circle mBackgroundRipple;

ObjectAnimator mCurrentAnimator;

Drawable mOriginalBackground;

public RippleDrawable() {
initRippleElements();
}

public static void createRipple(View v, int primaryColor){
RippleDrawable rippleDrawable = new RippleDrawable();
rippleDrawable.setDrawable(v.getBackground());
rippleDrawable.setColor(primaryColor);
rippleDrawable.setBounds(v.getPaddingLeft(), v.getPaddingTop(),
v.getPaddingRight(), v.getPaddingBottom());

v.setOnTouchListener(rippleDrawable);
if(Build.VERSION.SDK_INT >= 16) {
v.setBackground(rippleDrawable);
}else{
v.setBackgroundDrawable(rippleDrawable);
}
}

public static void createRipple(int x, int y, View v, int primaryColor){
if(!(v.getBackground() instanceof RippleDrawable)) {
createRipple(v, primaryColor);
}
RippleDrawable drawable = (RippleDrawable) v.getBackground();
drawable.setColor(primaryColor);
drawable.onFingerDown(v, x, y);
}

/**
* Set colors of ripples
*
* @param primaryColor color of ripples
*/
public void setColor(int primaryColor){
mRippleBackgroundPaint.setColor(primaryColor);
mRippleBackgroundPaint.setAlpha(RIPPLE_BACKGROUND_ALPHA);
mRipplePaint.setColor(primaryColor);

invalidateSelf();
}

/**
* set first layer you background drawable
*
* @param drawable original background
*/
public void setDrawable(Drawable drawable){
mOriginalBackground = drawable;

invalidateSelf();
}

void initRippleElements(){
mTouchRipple = new Circle();
mBackgroundRipple = new Circle();

mRipplePaint.setStyle(Paint.Style.FILL);
mRippleBackgroundPaint.setStyle(Paint.Style.FILL);
}

@Override
public void draw(Canvas canvas) {
if(mOriginalBackground != null){
mOriginalBackground.setBounds(getBounds());
mOriginalBackground.draw(canvas);
}

mBackgroundRipple.draw(canvas, mRippleBackgroundPaint);
mTouchRipple.draw(canvas, mRipplePaint);
}

@Override public void setAlpha(int alpha) {}

@Override public void setColorFilter(ColorFilter cf) {}

@Override public int getOpacity() {
return 0;
}

@Override
public boolean onTouch(View v, MotionEvent event) {
// Зберігаємо досконале дію
final int action = event.getAction();
// і в залежності від дії виконуємо методи
switch (action){
// Користувач торкнувся екрану
case MotionEvent.ACTION_DOWN:
onFingerDown(v, event.getX(), event.getY());
// Для того що б події View спрацьовували нам треба його викликати
return v.onTouchEvent(event);
// Користувач рухає пальцем по екрану (це продовження торкання)
case MotionEvent.ACTION_MOVE:
onFingerMove(event.getX(), event.getY());
break;
// Користувач убал свій пальчик
case MotionEvent.ACTION_UP:
onFingerUp();
break;
}
return false;
}

int mViewSize = 0;

void onFingerDown(View v, float x, float y){
mTouchRipple.cx = mBackgroundRipple.cx = x;
mTouchRipple.cy = mBackgroundRipple.cy = y;
mTouchRipple.radius = mBackgroundRipple.radius = 0f;
mViewSize = Math.max(v.getWidth(), v.getHeight());

// Якщо минула анімація закінчилася створимо нову
if(mCurrentAnimator == null){
// Вкажемо стан за замовчуванням для нашого фонового кола
// тобто відновимо його прозорість на дефолтний
mRippleBackgroundPaint.setAlpha(RIPPLE_BACKGROUND_ALPHA);

// Створимо анимашку, тут константа CREATE_TOUCH_RIPPLE це геттери і сетери
// для відправки стану анімації
mCurrentAnimator = ObjectAnimator.ofFloat(this, CREATE_TOUCH_RIPPLE, 0f, 1f);
mCurrentAnimator.setDuration(DEFAULT_ANIM_DURATION);
}

// Якщо анімація відіграє нічого не робимо чекаємо поки закінчиться
if(!mCurrentAnimator.isRunning()){
mCurrentAnimator.start();
}
}

float mAnimationValue;

/**
* ObjectAnimator викликає цю функцію
*
* @param value стан анімації від 0 до 1
*/
void createTouchRipple(float value){
mAnimationValue = value;

// step by step збільшуємо кола, мінімальний радіус 40px
mTouchRipple.radius = 40f + (mAnimationValue * (END_RIPPLE_TOUCH_RADIUS - 40f));
mBackgroundRipple.radius = mAnimationValue * (mViewSize * END_SCALE);

// і плавне исчезновние ще не появивщихся кіл,
// тобто при старті анімації їх opacity максимальна,
// і в кінці вона падає до мінімального значення
int min = RIPPLE_TOUCH_MIN_ALPHA;
int max = RIPPLE_TOUCH_MAX_ALPHA;
int alpha = min + (int) (mAnimationValue * (max - min));
mRipplePaint.setAlpha((max min) - alpha);

// Перемальовуємо
invalidateSelf();
}


void destroyTouchRipple(float value){
// Зберігаємо стан анімації
mAnimationValue = value;

// Збільшуємо радіус кола до фонового радіуса
mTouchRipple.radius = END_RIPPLE_TOUCH_RADIUS + (mAnimationValue * (mViewSize * END_SCALE));

// та одночасно в обох кіл створюємо ефект згасання
mRipplePaint.setAlpha((int) (RIPPLE_TOUCH_MIN_ALPHA - (mAnimationValue * RIPPLE_TOUCH_MIN_ALPHA)));
mRippleBackgroundPaint.setAlpha
((int) (RIPPLE_BACKGROUND_ALPHA - (mAnimationValue * RIPPLE_BACKGROUND_ALPHA)));

// ну і як же без перемальовування?
invalidateSelf();
}

float getAnimationState(){
return mAnimationValue;
}

void onFingerUp(){
// Закінчуємо анімацію
if(mCurrentAnimator != null) {
mCurrentAnimator.end();
mCurrentAnimator = null;
createTouchRipple(1f);
}

// Створюємо нову, і при завершенні очищаємо її
mCurrentAnimator = ObjectAnimator.ofFloat(this, DESTROY_TOUCH_RIPPLE, 0f, 1f);
mCurrentAnimator.setDuration(DEFAULT_ANIM_DURATION);
mCurrentAnimator.addListener(new SimpleAnimationListener(){
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mCurrentAnimator = null;
}
});
mCurrentAnimator.start();
}

void onFingerMove(float x, float y){
mTouchRipple.cx = x;
mTouchRipple.cy = y;

invalidateSelf();
}

@Override
public boolean setState(int[] stateSet) {
if(mOriginalBackground != null){
return mOriginalBackground.setState(stateSet);
}
return super.setState(stateSet);
}

@Override
public int[] getState() {
if(mOriginalBackground != null){
return mOriginalBackground.getState();
}
return super.getState();
}

static final class Circle{
float cx;
float cy;
float radius;

public void draw(Canvas canvas, Paint paint){
canvas.drawCircle(cx, cy, radius, paint);
}
}

}





У результаті:



Проект на Github.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.