您的位置:首页 > 移动开发 > Android开发

Android MaterialDesign之水波点击效果的几种实现方法

2017-05-11 13:50 435 查看
转载:http://www.jianshu.com/p/74bfa3338f11

什么是水波点击的效果? 下面是几种不同的实现方法的效果图以及实现方法



Video_2016-08-31_003846

如何实现?


方法一 使用官方提供的RippleDrawable类

优点:使用方便,非常漂亮。

缺点:Android5.0以下版本无法使用

步骤:

添加一个普通的 ripple_bg_drawable.xml 背景文件
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#8cc476" />
<corners android:radius="0dp" />
</shape>


添加带波纹效果的背景文件 ripple_bg.xml
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#FF21272B">
<item android:drawable="@drawable/ripple_bg_drawable" />
</ripple>


这里使用了上面的xml文件作为背景,然后给组件设置背景时,选 ripple_bg.xml 就可以了。如
<Button
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@drawable/ripple_bg"
android:padding="10dp"
android:text="使用RippleDrawable类实现" />


效果:



Video_2016-08-30_231558

很简单的录制了下gif ,质量不好。实际效果很好看。

注意事项:如果你的api最低版本低于21,则ripple这里其实是有错误提醒的,低于这个版本会报错。


方法二 使用代码实现

优点:低版本兼容、使用简单

缺点:效果不如官方的好

步骤:

在values下添加 attrs.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MaterialLayout">
<attr name="alpha" format="integer" />
<attr name="alpha_step" format="integer" />
<attr name="framerate" format="integer" />
<attr name="duration" format="integer" />
<attr name="mycolor" format="color" />
<attr name="scale" format="float" />
</declare-styleable>
</resources>


添加一个自定义的布局类 MaterialLayout.class
public class MaterialLayout extends RelativeLayout {

private static final int DEFAULT_RADIUS = 10;
private static final int DEFAULT_FRAME_RATE = 10;
private static final int DEFAULT_DURATION = 200;
private static final int DEFAULT_ALPHA = 255;
private static final float DEFAULT_SCALE = 0.8f;
private static final int DEFAULT_ALPHA_STEP = 5;

/**
* 动画帧率
*/
private int mFrameRate = DEFAULT_FRAME_RATE;
/**
* 渐变动画持续时间
*/
private int mDuration = DEFAULT_DURATION;
/**
*
*/
private Paint mPaint = new Paint();
/**
* 被点击的视图的中心点
*/
private Point mCenterPoint = null;
/**
* 视图的Rect
*/
private RectF mTargetRectf;
/**
* 起始的圆形背景半径
*/
private int mRadius = DEFAULT_RADIUS;
/**
* 最大的半径
*/
private int mMaxRadius = DEFAULT_RADIUS;

/**
* 渐变的背景色
*/
private int mCirclelColor = Color.LTGRAY;
/**
* 每次重绘时半径的增幅
*/
private int mRadiusStep = 1;
/**
* 保存用户设置的alpha值
*/
private int mBackupAlpha;

/**
* 圆形半径针对于被点击视图的缩放比例,默认为0.8
*/
private float mCircleScale = DEFAULT_SCALE;
/**
* 颜色的alpha值, (0, 255)
*/
private int mColorAlpha = DEFAULT_ALPHA;
/**
* 每次动画Alpha的渐变递减值
*/
private int mAlphaStep = DEFAULT_ALPHA_STEP;

private View mTargetView;

/**
* @param context
*/
public MaterialLayout(Context context) {
this(context, null);
}

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

public MaterialLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}

private void init(Context context, AttributeSet attrs) {
if (isInEditMode()) {
return;
}

if (attrs != null) {
initTypedArray(context, attrs);
}

initPaint();

this.setWillNotDraw(false);
this.setDrawingCacheEnabled(true);
}

private void initTypedArray(Context context, AttributeSet attrs) {
final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MaterialLayout);
mCirclelColor = typedArray.getColor(R.styleable.MaterialLayout_mycolor, Color.LTGRAY);
mDuration = typedArray.getInteger(R.styleable.MaterialLayout_duration, DEFAULT_DURATION);
mFrameRate = typedArray.getInteger(R.styleable.MaterialLayout_framerate, DEFAULT_FRAME_RATE);
mColorAlpha = typedArray.getInteger(R.styleable.MaterialLayout_alpha, DEFAULT_ALPHA);
mCircleScale = typedArray.getFloat(R.styleable.MaterialLayout_scale, DEFAULT_SCALE);

typedArray.recycle();

}

private void initPaint() {
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mCirclelColor);
mPaint.setAlpha(mColorAlpha);

// 备份alpha属性用于动画完成时重置
mBackupAlpha = mColorAlpha;
}

/**
* 点击的某个坐标点是否在View的内部
*
* @param touchView
* @param x 被点击的x坐标
* @param y 被点击的y坐标
* @return 如果点击的坐标在该view内则返回true,否则返回false
*/
private boolean isInFrame(View touchView, float x, float y) {
initViewRect(touchView);
return mTargetRectf.contains(x, y);
}

/**
* 获取点中的区域,屏幕绝对坐标值,这个高度值也包含了状态栏和标题栏高度
*
* @param touchView
*/
private void initViewRect(View touchView) {
int[] location = new int[2];
touchView.getLocationOnScreen(location);
// 视图的区域
mTargetRectf = new RectF(location[0], location[1], location[0] + touchView.getWidth(),
location[1] + touchView.getHeight());

}

/**
* 减去状态栏和标题栏的高度
*/
private void removeExtraHeight() {
int[] location = new int[2];
this.getLocationOnScreen(location);
// 减去两个该布局的top,这个top值就是状态栏的高度
mTargetRectf.top -= location[1];
mTargetRectf.bottom -= location[1];
// 计算中心点坐标
int centerHorizontal = (int) (mTargetRectf.left + mTargetRectf.right) / 2;
int centerVertical = (int) ((mTargetRectf.top + mTargetRectf.bottom) / 2);
// 获取中心点
mCenterPoint = new Point(centerHorizontal, centerVertical);

}

private View findTargetView(ViewGroup viewGroup, float x, float y) {
int childCount = viewGroup.getChildCount();
// 迭代查找被点击的目标视图
for (int i = 0; i < childCount; i++) {
View childView = viewGroup.getChildAt(i);
if (childView instanceof ViewGroup) {
return findTargetView((ViewGroup) childView, x, y);
} else if (isInFrame(childView, x, y)) { // 否则判断该点是否在该View的frame内
return childView;
}
}

return null;
}

private boolean isAnimEnd() {
return mRadius >= mMaxRadius;
}

private void calculateMaxRadius(View view) {
// 取视图的最长边
int maxLength = Math.max(view.getWidth(), view.getHeight());
// 计算Ripple圆形的半径
mMaxRadius = (int) ((maxLength / 2) * mCircleScale);

int redrawCount = mDuration / mFrameRate;
// 计算每次动画半径的增值
mRadiusStep = (mMaxRadius - DEFAULT_RADIUS) / redrawCount;
// 计算每次alpha递减的值
mAlphaStep = (mColorAlpha - 100) / redrawCount;
}

/**
* 处理ACTION_DOWN触摸事件, 注意这里获取的是Raw x, y,
* 即屏幕的绝对坐标,但是这个当屏幕中有状态栏和标题栏时就需要去掉这些高度,因此得到mTargetRectf后其高度需要减去该布局的top起点
* ,也就是标题栏和状态栏的总高度.
*
* @param event
*/
private void deliveryTouchDownEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
mTargetView = findTargetView(this, event.getRawX(), event.getRawY());
if (mTargetView != null) {
removeExtraHeight();
// 计算相关数据
calculateMaxRadius(mTargetView);
// 重绘视图
invalidate();
}
}
}

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
deliveryTouchDownEvent(event);
return super.onInterceptTouchEvent(event);
}

@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
// 绘制Circle
drawRippleIfNecessary(canvas);
}

private void drawRippleIfNecessary(Canvas canvas) {
if (isFoundTouchedSubView()) {
// 计算新的半径和alpha值
mRadius += mRadiusStep;
mColorAlpha -= mAlphaStep;

// 裁剪一块区域,这块区域就是被点击的View的区域.通过clipRect来获取这块区域,使得绘制操作只能在这个区域范围内的进行,
// 即使绘制的内容大于这块区域,那么大于这块区域的绘制内容将不可见. 这样保证了背景层只能绘制在被点击的视图的区域
canvas.clipRect(mTargetRectf);
mPaint.setAlpha(mColorAlpha);
// 绘制背景圆形,也就是
canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mRadius, mPaint);
}

if (isAnimEnd()) {
reset();
} else {
invalidateDelayed();
}
}

/**
* 发送重绘消息
*/
private void invalidateDelayed() {
this.postDelayed(new Runnable() {

@Override
public void run() {
invalidate();
}
}, mFrameRate);
}

/**
* 判断是否找到被点击的子视图
*
* @return
*/
private boolean isFoundTouchedSubView() {
return mCenterPoint != null && mTargetView != null;
}

private void reset() {
mCenterPoint = null;
mTargetRectf = null;
mRadius = DEFAULT_RADIUS;
mColorAlpha = mBackupAlpha;
mTargetView = null;
invalidate();
}
}


在布局文件中引用
<com.liangddyy.rippledemo.MaterialLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="100dp"
android:padding="10dp"
android:text="自定义布局实现1" />
<Button
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="100dp"
android:padding="10dp"
android:text="自定义布局实现2" />
</LinearLayout>
</com.liangddyy.rippledemo.MaterialLayout>


效果:



Video_2016-08-30_235950

其实可以看到,只要在 MaterialLayout 布局中的控件都有这个效果,所以使用其他是很方便的。

这部分代码见原作者博客 http://blog.csdn.net/bboyfeiyu/article/details/42587799


方法三 第三方库实现

github上的一个叫 RippleEffec 项目

优点:美观、使用简单、最低兼容API 9

缺点:不如官方的好看啦

步骤:

导入库
dependencies {
compile 'com.github.traex.rippleeffect:library:1.3'
}


使用类似于方法二,比较灵活和方便
<com.andexert.library.RippleView
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="100dp"
rv_centered="true">
<Button
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#4ce65e"
android:text="第三方库实现"/>
</com.andexert.library.RippleView>


如果直接导入使用报错,那可以复制如下源码到工程使用。

定义个颜色

<color name="rippelColor">#FFFFFF</color>


添加attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RippleView">
<attr name="rv_alpha" format="integer" />
<attr name="rv_framerate" format="integer" />
<attr name="rv_rippleDuration" format="integer" />
<attr name="rv_zoomDuration" format="integer" />
<attr name="rv_color" format="color" />
<attr name="rv_centered" format="boolean" />
<attr name="rv_type" format="enum">
<enum name="simpleRipple" value="0" />
<enum name="doubleRipple" value="1" />
<enum name="rectangle" value="2" />
</attr>
<attr name="rv_ripplePadding" format="dimension" />
<attr name="rv_zoom" format="boolean" />
<attr name="rv_zoomScale" format="float" />
</declare-styleable>
</resources>


添加RippleView.java文件

代码太多,不复制了。参见末尾处的项目源码吧。


效果:



Video_2016-08-31_003808

最后附上整个示例工程代码:

https://github.com/liangddyy/RippleDemo
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: