Android RippleEffect波纹效果,重写ViewGroup
一直觉得Material Design很美,一直琢磨着打算给公司项目也换成Material Design风格,这里先介绍一种重写RelativeLayout实现的RippleEffect的波纹效果。
先来看看Demo效果(GIF做得不好,见谅):
先来看实现的思路,可以重写Button,ImageView等View,也可以重写ViewGroup。不同的是,重写ViewGroup时,需要在dispatchTouchEvent拦截点击事件,确定到底是出发了哪一个子view,然后再在onTouchEvent中去触发,当然也可以忽略dispatchTouchEvent,那么整个ViewGroup都会有RippleEffect效果。
实现RippleEffect波纹效果的方式就是drawCircle,点击控件时,记录下点击位置的x,y值,作为波纹触发的中心点(当然也可以直接设定波纹触发的中心点为当前被点击控件的中心),再根据当前被点击的View的长宽,初始化一些与波纹半径相关的一些参数,每隔一定的时间,我们就绘制一次,每次绘制时,绘制波纹半径会在上一次的基础上,有一定的增量,可以匀速增,加速增,减速增,如果当绘制波纹半径大于我们设定的范围时,就停止绘制,这样就实现了RippleEffect效果。当然,绘制时可以用invalidate()+Handler的方式或者直接使用postInvalidate()刷新界面 。
来看看一个GIT上的 RippleEffect代码:
private void init(final Context context, final AttributeSet attrs) {
if (isInEditMode())
return;
final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RippleView);
rippleColor = typedArray.getColor(R.styleable.RippleView_rv_color, getResources().getColor(R.color.rippelColor));
rippleType = typedArray.getInt(R.styleable.RippleView_rv_type, 0);
hasToZoom = typedArray.getBoolean(R.styleable.RippleView_rv_zoom, false);
isCentered = typedArray.getBoolean(R.styleable.RippleView_rv_centered, false);
rippleDuration = typedArray.getInteger(R.styleable.RippleView_rv_rippleDuration, rippleDuration);
frameRate = typedArray.getInteger(R.styleable.RippleView_rv_framerate, frameRate);
rippleAlpha = typedArray.getInteger(R.styleable.RippleView_rv_alpha, rippleAlpha);
ripplePadding = typedArray.getDimensionPixelSize(R.styleable.RippleView_rv_ripplePadding, 0);
canvasHandler = new Handler();
zoomScale = typedArray.getFloat(R.styleable.RippleView_rv_zoomScale, 1.03f);
zoomDuration = typedArray.getInt(R.styleable.RippleView_rv_zoomDuration, 200);
typedArray.recycle();
paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setColor(rippleColor);
paint.setAlpha(rippleAlpha);
this.setWillNotDraw(false);
gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public void onLongPress(MotionEvent event) {
super.onLongPress(event);
animateRipple(event);
sendClickEvent(true);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return true;
}
});
this.setDrawingCacheEnabled(true);
this.setClickable(true);
}
和其它自定义View或者ViewGroup一样,构造方法中调用此方法初始化自定义属性及一些Paint,canvas等,这里也初始化了gestureDetector,是为了在长按事件中也会触发RippleEffect波纹效果。
看看attrs.xml中的自定义属性:
<declare-styleable name="RippleView">rv_zoomDuration
<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>
- rv_alpha即paint.setAlpha()中设定的值,就是透明度,其取值范围是0---255,数值越小,越透明,颜色上表现越淡。
- rv_framerate是RippleEffect波纹每次半径增加相关的一个参数,值越大,每次的半径增量越大,效果看起来也就越粗糙。
- rv_rippleDuration就是RippleEffect波纹的范围了,超过此范围,就不再绘制了。
- rv_zoomDuration为RippleEffect波纹的持续时间。
- rv_color为RippleEffect波纹的颜色。
- rv_centered值为true,则波纹的触发点为View的中心,否则为触摸点的位置。
- rv_type为波纹效果的类型,simpleRipple就是最常见的那种,只产生一圈涟漪效果,doubleRipple两圈涟漪,rectangle的效果代码中没具体实现。
- rv_zoom值为true,会有一个缩放的效果,rv_ripplePadding为和RippleEffect波纹半径相关的一个参数,rv_zoomScale为缩放动画结束时Y坐标上的伸缩尺寸。
最重要的draw方法:
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (animationRunning) {
if (rippleDuration <= timer * frameRate) {
animationRunning = false;
timer = 0;
durationEmpty = -1;
timerEmpty = 0;
canvas.restore();
invalidate();
if (onCompletionListener != null) onCompletionListener.onComplete(this);
return;
} else
canvasHandler.postDelayed(runnable, frameRate);
if (timer == 0)
canvas.save();
canvas.drawCircle(x, y, (radiusMax * (((float) timer * frameRate) / rippleDuration)), paint);
paint.setColor(Color.parseColor("#ffff4444"));
if (rippleType == 1 && originBitmap != null && (((float) timer * frameRate) / rippleDuration) > 0.4f) {
if (durationEmpty == -1)
durationEmpty = rippleDuration - timer * frameRate;
timerEmpty++;
final Bitmap tmpBitmap = getCircleBitmap((int) ((radiusMax) * (((float) timerEmpty * frameRate) / (durationEmpty))));
canvas.drawBitmap(tmpBitmap, 0, 0, paint);
tmpBitmap.recycle();
}
paint.setColor(rippleColor);
if (rippleType == 1) {
if ((((float) timer * frameRate) / rippleDuration) > 0.6f)
paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timerEmpty * frameRate) / (durationEmpty)))));
else
paint.setAlpha(rippleAlpha);
}
else
paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timer * frameRate) / rippleDuration))));
timer++;
}
}
在初始化的时候, animationRunning值已被置为true,主要是为了保证一次完整地波纹效果不会被打断。 首先执行 canvasHandler.postDelayed(runnable, frameRate);每隔frameRate的时间,执行一次invalidate()。 canvas.save()用来保存Canvas的状态,注意在绘制结束的时候调用了canvas.restore(),成对使用。然后就开始绘制波纹了,每次的半径增量为(radiusMax * (((float) 1 * frameRate) / rippleDuration),每隔rameRate绘制一次。rippleType == 1的时候,保证在绘制到一定的时候,setAlpha的值加深,形成两次涟漪,否则setAlpha值就一直递减,波纹越大效果越弱。最后当rippleDuration <= timer * frameRate的时候,就return了,RippleEffect波纹效果结束。
最后在XML中直接拿来用就可以了:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:ripple="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical" >
<!-- 1 rv_centered="true" rv_type="simpleRipple" -->
<com.example.RippleEffect.RippleView
android:id="@+id/more"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
ripple:rv_centered="true" >
<ImageView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#BAC9FF"
android:padding="15dp"
android:src="@drawable/ic_launcher" />
</com.example.RippleEffect.RippleView>
<!-- 2 rv_centered="false" rv_type="simpleRipple" -->
<com.example.RippleEffect.RippleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
ripple:rv_centered="false"
ripple:rv_type="simpleRipple" >
<ImageView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#BAC9FF"
android:padding="15dp"
android:src="@drawable/ic_launcher" />
</com.example.RippleEffect.RippleView>
<!-- 3 rv_type="doubleRipple" -->
<com.example.RippleEffect.RippleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
ripple:rv_type="doubleRipple" >
<ImageView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#BAC9FF"
android:padding="15dp"
android:src="@drawable/ic_launcher" />
</com.example.RippleEffect.RippleView>
<!-- 4 rv_type="rectangle" -->
<com.example.RippleEffect.RippleView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
ripple:rv_type="doubleRipple" >
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#BAC9FF"
android:padding="15dp"
android:src="@drawable/ic_launcher" />
</com.example.RippleEffect.RippleView>
<!-- 5 rv_zoom ="true" rv_ripplePadding ="20dp" ripple:rv_zoomScale="1.25" -->
<com.example.RippleEffect.RippleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
ripple:rv_centered="false"
ripple:rv_color="#D91615"
ripple:rv_rippleDuration="2000"
ripple:rv_ripplePadding="20dp"
ripple:rv_zoom="true"
ripple:rv_zoomDuration="200"
ripple:rv_zoomScale="1.25" >
<ImageView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#BAC9FF"
android:padding="15dp"
android:src="@drawable/ic_launcher" />
</com.example.RippleEffect.RippleView>
<!-- 6 rv_type="simpleRipple" rv_alpha="10" rv_framerate="100" -->
<com.example.RippleEffect.RippleView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
ripple:rv_alpha="200"
ripple:rv_framerate="100"
ripple:rv_type="simpleRipple" >
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#BAC9FF"
android:padding="15dp"
android:src="@drawable/ic_launcher" />
</com.example.RippleEffect.RippleView>
<!-- 7 rv_type="simpleRipple" rv_alpha="10" rv_framerate="2" -->
<com.example.RippleEffect.RippleView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
ripple:rv_alpha="200"
ripple:rv_framerate="2"
ripple:rv_type="simpleRipple" >
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#BAC9FF"
android:padding="15dp"
android:src="@drawable/ic_launcher" />
</com.example.RippleEffect.RippleView>
<!-- 8 rv_type="simpleRipple" rv_alpha="10" rv_framerate="2" -->
<com.example.RippleEffect.RippleView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
ripple:rv_alpha="200"
ripple:rv_framerate="2"
ripple:rv_type="simpleRipple" >
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#BAC9FF"
android:padding="15dp"
android:text="Button" />
</com.example.RippleEffect.RippleView>
</LinearLayout>
</ScrollView>
</LinearLayout>
DEMO 下载地址 : http://download.youkuaiyun.com/detail/yalinfendou/8803431