android水波紋漣漪效果的實現 ---- 入門+初步提高
android水波紋漣漪效果的實現<入門+初步提高>
作為一個android開發著,水波紋效果是常見的效果,可以優化ui提高使用者的互動,在android5.0之前是不會自帶水波紋的,隨著material design的提出水波紋不僅僅被用於btn的點選還有部分ui的跳轉,讓anroid介面變得比較炫酷起來;
首先今天下午沒事幹實現了一個水波紋的demo下面先展示一下:
ok下面開始進入正題,剖析實現的程式碼:
1
.MainActivity中 沒有進行任何操作,只是button所在xml的一個載體<可以跳過>,看一下 activity_main.layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="15dp"
android:paddingLeft="30dp"
android:paddingRight="30dp">
<com.example.houruixiang.touchripple.widget.RippleButton
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:background="@drawable/bg_1"
/>
<LinearLayout
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center">
<com.example.houruixiang.touchripple.widget.RippleButton
android:layout_width="150dp"
android:layout_height="match_parent"
android:gravity="center"
android:text="click1"
android:background="@color/colorAccent"
/>
<com.example.houruixiang.touchripple.widget.RippleButton
android:layout_marginLeft="10dp"
android:layout_width="150dp"
android:layout_height="match_parent"
android:gravity="center"
android:text="click2"
android:background="@color/colorPrimary"
/>
</LinearLayout>
<com.example.houruixiang.touchripple.widget.RippleButton
android:layout_marginTop="15dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
/>
</LinearLayout>
2
可以看到RippleButton就是 我們要實現的自定義button,但是需要注意水波紋漣漪效果的實現是繼承於drawable實現的,而rippleButton只是呼叫了其中的drwa()方法:
為了好理解先來看drawable的子類RippleDrawable的程式碼:
首先來看需要實現的方法:
public class RippleDrawable extends Drawable {
@Override
public void draw(Canvas canvas) {}
@Override
public void setAlpha(int alpha) {
mAlpha = alpha;
onColorOrAlphaChange();
}
@Override
public int getAlpha() {
return mAlpha;
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
//濾鏡效果
}
@Override
public int getOpacity() {
//返回透明度
if (mAlpha == 255){
return PixelFormat.OPAQUE;
}else if (mAlpha == 0){
return PixelFormat.TRANSPARENT;
}else{
return PixelFormat.TRANSLUCENT;
}
}
介紹一下:
draw():用來完成控制元件的繪製
setColorFilter():實現濾鏡效果
getAlpha()/setAlpha():set/get方法 設定和返回透明度
getOpacity():同樣是返回透明度,就是把相關透明度轉化;eg:mAlpha = 255 —->PixelFormat.OPAQUE
if (mAlpha == 255){
return PixelFormat.OPAQUE;
}else if (mAlpha == 0){
return PixelFormat.TRANSPARENT;
}else{
return PixelFormat.TRANSLUCENT;
}
首先在RippleDrawable的構造方法中完成初始化<畫筆的初始化,顏色透明度的配置>,通過 使用者設定的透明度*畫筆的透明度 得到準確透明度;即:
if (mAlpha != 255){
int pAlpha = mPaint.getAlpha();
int realAlpha = (int) (pAlpha * (mAlpha/255f));
//設定透明度
mPaint.setAlpha(realAlpha);
}
public RippleDrawable() {
//抗鋸齒的畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//設定抗鋸齒
mPaint.setAntiAlias(true);
//設定放防抖動
mPaint.setDither(true);
setRippleColor(0x60000000);
}
public void setRippleColor(int rippleColor) {
this.rippleColor = rippleColor;
onColorOrAlphaChange();
}
private void onColorOrAlphaChange() {
//設定畫筆顏色
mPaint.setColor(rippleColor);
if (mAlpha != 255){
int pAlpha = mPaint.getAlpha();
int realAlpha = (int) (pAlpha * (mAlpha/255f));
//設定透明度
mPaint.setAlpha(realAlpha);
}
invalidateSelf();
}
ok畫筆顏色和透明度設定完後,來看下draw方法,顧名思義draw()就是完成繪製的過程;在這個demo中有3層bg
最下面是一個美女的bg 點選時候可以看出來 上層是一個灰色的bg ,在上面是一個灰色的圓形背景,需要注意的是上面兩層bg實在自定義drawable中實現的,需要注意透明度的計算不能再互動的過程中遮擋button原有的bg
下面來看一個方法<程式碼中進行標註 看起來會易懂一些>:
public int getCircleAlpha(int preAlpha,int bgAlpha){
int dAlpha = preAlpha<使用者設定的透明度> - bgAlpha<預設bg上層的bg>;
//根據以上的透明度得到最上層的透明度應該是多少時不會遮擋
return (int) ((dAlpha*255f)/(255f - bgAlpha));
}
根據不同透明度繪製bg<詳細看程式碼>:
//獲取使用者設定的透明度 就是Z
int preAlpha = mPaint.getAlpha();
//當前背景
int bgAlpha = (int) (preAlpha * (mBgAlpha/255f));
//bg + prebg運算得到得背景
int maxCircleAlpha = getCircleAlpha(preAlpha,bgAlpha);
int circleAlpha = (int) (maxCircleAlpha * (mCircleAlpha/255f));
mPaint.setAlpha(bgAlpha);
canvas.drawColor(mPaint.getColor());
mPaint.setAlpha(circleAlpha);
canvas.drawCircle(mRipplePointX,mRipplePointY,mRippleRadius,mPaint);
//設定最初的透明度 保證下次進入運算不會出錯
mPaint.setAlpha(preAlpha);
3
下面看進入動畫的漣漪效果的邏輯:
首先互動的過程就是點選的過程 所以需要監聽點選;應為drawable的子類中沒有onTonchEvent的方法 所以需要通過RippleButton的onTouchEvent方法中傳入參給RippleDrawable的自定義ontouch中完成down --- move --- up --- cancel:
先看RippleButton ;其中
//設定重新整理介面,View中已經實現 --->原始碼 button繼承子drawable.callback
rippleDrawable.setCallback(this);
//設定重新整理區域--->原始碼
rippleDrawable.setBounds(0,0,getWidth(),getHeight());
@Override
protected boolean verifyDrawable(Drawable who) {
return who == rippleDrawable || super.verifyDrawable(who);
}
以上程式碼用來解決 RippleDrawable 不能重新整理的問題 詳細請看原始碼,之後博文會有提到這裡不多說,然後繼續看RippleButton的程式碼
/**
* Created by houruixiang on 2017/7/18.
*/
public class RippleButton extends Button {
private RippleDrawable rippleDrawable;
private Paint paint;
public RippleButton(Context context) {
this(context,null);
}
public RippleButton(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public RippleButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paint = new Paint();
rippleDrawable = new RippleDrawable();
//設定重新整理介面,View中已經實現 --->原始碼 button繼承子drawable.callback
rippleDrawable.setCallback(this);
//rippleDrawable
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//設定重新整理區域--->原始碼
rippleDrawable.setBounds(0,0,getWidth(),getHeight());
}
@Override
protected boolean verifyDrawable(Drawable who) {
return who == rippleDrawable || super.verifyDrawable(who);
}
@Override
protected void onDraw(Canvas canvas) {
rippleDrawable.draw(canvas);
super.onDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
rippleDrawable.onTouch(event);
return true;
}
}
在RippleButton的:
@Override
public boolean onTouchEvent(MotionEvent event) {
rippleDrawable.onTouch(event);
return true;
}
4
傳參開始奠定RippleDrawble的點選監聽:
public void onTouch(MotionEvent event){
switch (event.getActionMasked()){
case MotionEvent.ACTION_DOWN:
//按下
mClickPointX = event.getX();
mClickPointY = event.getY();
onTouchDown(mClickPointX, mClickPointY);
break;
case MotionEvent.ACTION_MOVE:
//移動
//onTouchMove(moveX,moveY);
break;
case MotionEvent.ACTION_UP:
//擡起
onTouchUp();
break;
case MotionEvent.ACTION_CANCEL:
//退出
//onTouchCancel();
break;
}
}
public void onTouchDown(float x,float y){
//Log.i("onTouchDown====",x + "" + y );
//unscheduleSelf(runnable);
mUpDone = false;
mRipplePointX = x;
mRipplePointY = y;
mRippleRadius = 0;
startEnterRunnable();
}
public void onTouchMove(float x,float y){
}
public void onTouchUp(){
mUpDone = true;
if (mEnterDone){
startExitRunnable();
}
}
public void onTouchCancel(){
mUpDone = true;
if (mEnterDone){
startExitRunnable();
}
}
可以看到在ontouchDown中提到startEnterRunnable(),在onTouchUp和onTouchCancel中有startExitRunnable();那麼接著看著兩個方法:
/**
* 開啟進入動畫
* */
public void startEnterRunnable(){
mProgress = 0;
//mEnterDone = false;
unscheduleSelf(exitRunnable);
unscheduleSelf(runnable);
scheduleSelf(runnable,SystemClock.uptimeMillis());
}
/**
* 開啟退出動畫
* */
public void startExitRunnable(){
mExitProgress = 0;
unscheduleSelf(runnable);
unscheduleSelf(exitRunnable);
scheduleSelf(exitRunnable,SystemClock.uptimeMillis());
}
其中drawable中有handler機制來進出佇列完成繪製:而一下方法就是分別開始排程和終止排程:
unscheduleSelf(runnable);
unscheduleSelf(exitRunnable);
5
那麼下面看關鍵的程式碼,進入動畫的排程 和退出動畫的排程
首先來看進入動畫:
/**進入動畫*/
//進入動畫的進度值
private float mProgress;
//每次遞增的時間
private float mEnterIncrement = 16f/360;
//進入動畫新增插值器
private DecelerateInterpolator mEnterInterpolator = new DecelerateInterpolator(2);
private Runnable runnable = new Runnable() {
@Override
public void run() {
mEnterDone = false;
mCircleAlpha = 255;
mProgress = mProgress + mEnterIncrement;
if (mProgress > 1){
onEnterPrograss(1);
enterDone();
return;
}
float interpolation = mEnterInterpolator.getInterpolation(mProgress);
onEnterPrograss(interpolation);
scheduleSelf(this, SystemClock.uptimeMillis() + 16);
}
};
/**進入動畫重新整理的方法
* @parms realProgress */
public void onEnterPrograss(float realPrograss){
mRippleRadius = getCenter(startRadius,endRadius,realPrograss);
mRipplePointX = getCenter(mClickPointX,mCenterPointX,realPrograss);
mRipplePointY = getCenter(mClickPointY,mCenterPointY,realPrograss);
mBgAlpha = (int) getCenter(0,182,realPrograss);
invalidateSelf();
}
private void enterDone() {
mEnterDone = true;
if(mUpDone)
startExitRunnable();
}
然後在看退出動畫的排程:
/**退出動畫*/
//退出動畫的進度值
private float mExitProgress;
//每次遞增的時間
private float mExitIncrement = 16f/280;
//退出動畫新增插值器
private AccelerateInterpolator mExitInterpolator = new AccelerateInterpolator(2);
private Runnable exitRunnable = new Runnable() {
@Override
public void run() {
if (!mEnterDone){
return;
}
mExitProgress = mExitProgress + mExitIncrement;
if (mExitProgress > 1){
onExitPrograss(1);
exitDone();
return;
}
float interpolation = mExitInterpolator.getInterpolation(mExitProgress);
onExitPrograss(interpolation);
scheduleSelf(this, SystemClock.uptimeMillis() + 16);
}
};
/**退出動畫重新整理的方法
* @parms realProgress */
public void onExitPrograss(float realPrograss){
//設定背景
mBgAlpha = (int) getCenter(182,0,realPrograss);
//設定圓形區域
mCircleAlpha = (int) getCenter(255,0,realPrograss);
invalidateSelf();
}
private void exitDone() {
mEnterDone = false;
}
ok~基本的講解已經完畢 下面展示完整程式碼:
RippleButton:
package com.example.houruixiang.touchripple.widget;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.example.houruixiang.touchripple.R;
/**
* Created by houruixiang on 2017/7/18.
*/
public class RippleButton extends Button {
private RippleDrawable rippleDrawable;
private Paint paint;
public RippleButton(Context context) {
this(context,null);
}
public RippleButton(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public RippleButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paint = new Paint();
rippleDrawable = new RippleDrawable();
//設定重新整理介面,View中已經實現 --->原始碼 button繼承子drawable.callback
rippleDrawable.setCallback(this);
//rippleDrawable
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//設定重新整理區域--->原始碼
rippleDrawable.setBounds(0,0,getWidth(),getHeight());
}
@Override
protected boolean verifyDrawable(Drawable who) {
return who == rippleDrawable || super.verifyDrawable(who);
}
@Override
protected void onDraw(Canvas canvas) {
rippleDrawable.draw(canvas);
super.onDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
rippleDrawable.onTouch(event);
return true;
}
}
RippleDrawable:
package com.example.houruixiang.touchripple.widget;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Interpolator;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
import android.util.Log;
import android.view.MotionEvent;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import java.security.PrivilegedExceptionAction;
import java.util.Timer;
import java.util.TimerTask;
/**
* Created by houruixiang on 2017/7/18.
*/
public class RippleDrawable extends Drawable {
private Paint mPaint;
private Bitmap bitmap;
private int rippleColor;
private float mRipplePointX = 0;
private float mRipplePointY = 0;
private float mRippleRadius = 0;
private int mAlpha = 200;
private float mCenterPointX,mCenterPointY;
private float mClickPointX;
private float mClickPointY;
//最大半徑
private float MaxRadius;
//開始半徑
private float startRadius;
//結束半徑
private float endRadius;
//記錄是否擡起手--->boolean
private boolean mUpDone;
//記錄進入動畫是否完畢
private boolean mEnterDone;
/**進入動畫*/
//進入動畫的進度值
private float mProgress;
//每次遞增的時間
private float mEnterIncrement = 16f/360;
//進入動畫新增插值器
private DecelerateInterpolator mEnterInterpolator = new DecelerateInterpolator(2);
private Runnable runnable = new Runnable() {
@Override
public void run() {
mEnterDone = false;
mCircleAlpha = 255;
mProgress = mProgress + mEnterIncrement;
if (mProgress > 1){
onEnterPrograss(1);
enterDone();
return;
}
float interpolation = mEnterInterpolator.getInterpolation(mProgress);
onEnterPrograss(interpolation);
scheduleSelf(this, SystemClock.uptimeMillis() + 16);
}
};
/**進入動畫重新整理的方法
* @parms realProgress */
public void onEnterPrograss(float realPrograss){
mRippleRadius = getCenter(startRadius,endRadius,realPrograss);
mRipplePointX = getCenter(mClickPointX,mCenterPointX,realPrograss);
mRipplePointY = getCenter(mClickPointY,mCenterPointY,realPrograss);
mBgAlpha = (int) getCenter(0,182,realPrograss);
invalidateSelf();
}
private void enterDone() {
mEnterDone = true;
if(mUpDone)
startExitRunnable();
}
/**退出動畫*/
//退出動畫的進度值
private float mExitProgress;
//每次遞增的時間
private float mExitIncrement = 16f/280;
//退出動畫新增插值器
private AccelerateInterpolator mExitInterpolator = new AccelerateInterpolator(2);
private Runnable exitRunnable = new Runnable() {
@Override
public void run() {
if (!mEnterDone){
return;
}
mExitProgress = mExitProgress + mExitIncrement;
if (mExitProgress > 1){
onExitPrograss(1);
exitDone();
return;
}
float interpolation = mExitInterpolator.getInterpolation(mExitProgress);
onExitPrograss(interpolation);
scheduleSelf(this, SystemClock.uptimeMillis() + 16);
}
};
/**退出動畫重新整理的方法
* @parms realProgress */
public void onExitPrograss(float realPrograss){
//設定背景
mBgAlpha = (int) getCenter(182,0,realPrograss);
//設定圓形區域
mCircleAlpha = (int) getCenter(255,0,realPrograss);
invalidateSelf();
}
private void exitDone() {
mEnterDone = false;
}
//設定漸變效果 包括半徑/bg color/圓心位置等
public float getCenter(float start,float end,float prograss){
return start + (end - start)*prograss;
}
public RippleDrawable() {
//抗鋸齒的畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//設定抗鋸齒
mPaint.setAntiAlias(true);
//設定放防抖動
mPaint.setDither(true);
setRippleColor(0x60000000);
}
//private int mPaintAlpha = 255;
//背景的透明度
private int mBgAlpha;
//圓形區域的透明度
private int mCircleAlpha;
@Override
public void draw(Canvas canvas) {
//獲取使用者設定的透明度 就是Z
int preAlpha = mPaint.getAlpha();
//當前背景
int bgAlpha = (int) (preAlpha * (mBgAlpha/255f));
//bg + prebg運算得到得背景
int maxCircleAlpha = getCircleAlpha(preAlpha,bgAlpha);
int circleAlpha = (int) (maxCircleAlpha * (mCircleAlpha/255f));
mPaint.setAlpha(bgAlpha);
canvas.drawColor(mPaint.getColor());
mPaint.setAlpha(circleAlpha);
canvas.drawCircle(mRipplePointX,mRipplePointY,mRippleRadius,mPaint);
//設定最初的透明度 保證下次進入運算不會出錯
mPaint.setAlpha(preAlpha);
}
public int getCircleAlpha(int preAlpha,int bgAlpha){
int dAlpha = preAlpha - bgAlpha;
return (int) ((dAlpha*255f)/(255f - bgAlpha));
}
public void onTouch(MotionEvent event){
switch (event.getActionMasked()){
case MotionEvent.ACTION_DOWN:
//按下
mClickPointX = event.getX();
mClickPointY = event.getY();
onTouchDown(mClickPointX, mClickPointY);
break;
case MotionEvent.ACTION_MOVE:
//移動
//onTouchMove(moveX,moveY);
break;
case MotionEvent.ACTION_UP:
//擡起
onTouchUp();
break;
case MotionEvent.ACTION_CANCEL:
//退出
//onTouchCancel();
break;
}
}
public void onTouchDown(float x,float y){
//Log.i("onTouchDown====",x + "" + y );
//unscheduleSelf(runnable);
mUpDone = false;
mRipplePointX = x;
mRipplePointY = y;
mRippleRadius = 0;
startEnterRunnable();
}
public void onTouchMove(float x,float y){
}
public void onTouchUp(){
mUpDone = true;
if (mEnterDone){
startExitRunnable();
}
}
public void onTouchCancel(){
mUpDone = true;
if (mEnterDone){
startExitRunnable();
}
}
/**
* 開啟進入動畫
* */
public void startEnterRunnable(){
mProgress = 0;
//mEnterDone = false;
unscheduleSelf(exitRunnable);
unscheduleSelf(runnable);
scheduleSelf(runnable,SystemClock.uptimeMillis());
}
/**
* 開啟退出動畫
* */
public void startExitRunnable(){
mExitProgress = 0;
unscheduleSelf(runnable);
unscheduleSelf(exitRunnable);
scheduleSelf(exitRunnable,SystemClock.uptimeMillis());
}
public int changeColorAlpha(int color,int alpha){
//設定透明度
int a = (color >> 24) & 0xFF;
a = a * alpha/255;
int red = Color.red(color);
int green = Color.green(color);
int blue = Color.blue(color);
int argb = Color.argb(a, red, green, blue);
return argb;
}
//取所繪製區域的中心點
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
mCenterPointX = bounds.centerX();
mCenterPointY = bounds.centerY();
MaxRadius = Math.max(mCenterPointX,mCenterPointY);
startRadius = MaxRadius * 0.1f;
endRadius = MaxRadius * 0.8f;
}
@Override
public void setAlpha(int alpha) {
mAlpha = alpha;
onColorOrAlphaChange();
}
@Override
public int getAlpha() {
return mAlpha;
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
//濾鏡效果
if (mPaint.getColorFilter() != colorFilter){
mPaint.setColorFilter(colorFilter);
invalidateSelf();
}
}
@Override
public int getOpacity() {
//返回透明度
if (mAlpha == 255){
return PixelFormat.OPAQUE;
}else if (mAlpha == 0){
return PixelFormat.TRANSPARENT;
}else{
return PixelFormat.TRANSLUCENT;
}
}
public void setRippleColor(int rippleColor) {
this.rippleColor = rippleColor;
onColorOrAlphaChange();
}
private void onColorOrAlphaChange() {
//設定畫筆顏色
mPaint.setColor(rippleColor);
if (mAlpha != 255){
int pAlpha = mPaint.getAlpha();
int realAlpha = (int) (pAlpha * (mAlpha/255f));
//設定透明度
mPaint.setAlpha(realAlpha);
}
invalidateSelf();
}
}