├── app ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── values │ │ │ ├── strings.xml │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── styles.xml │ │ │ └── attrs.xml │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ │ └── dimens.xml │ │ └── layout │ │ │ └── activity_main.xml │ │ ├── java │ │ └── zhuwentao │ │ │ └── com │ │ │ └── rippleview │ │ │ ├── MainActivity.java │ │ │ ├── DensityUtil.java │ │ │ └── RippleView.java │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── README.md └── gradle.properties /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | zwt_RippleView 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuwentao2150/RippleView/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuwentao2150/RippleView/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuwentao2150/RippleView/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuwentao2150/RippleView/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuwentao2150/RippleView/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RippleView 2 | 3 | Android 自定义View一个控件搞定多种水波纹涟漪扩散效果 4 | 5 | > 博客地址:http://blog.csdn.net/zhuwentao2150/article/details/79452735 6 | 7 | - 圆圈从中心可循环向外扩散 8 | - 圆圈之间的扩散间距可以改变 9 | - 可控制扩散圆的渐变度 10 | - 圆圈可以是线条样式或者实心样式 11 | - 圆圈扩散的速度可以控制 12 | - 适配圆圈不同大小下的扩散效果 13 | 14 | ![image](http://img-blog.csdn.net/20180318221722414?watermark/2/text/Ly9ibG9nLmNzZG4ubmV0L3podXdlbnRhbzIxNTA=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) 15 | -------------------------------------------------------------------------------- /app/src/main/java/zhuwentao/com/rippleview/MainActivity.java: -------------------------------------------------------------------------------- 1 | package zhuwentao.com.rippleview; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | 6 | public class MainActivity extends AppCompatActivity { 7 | 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | setContentView(R.layout.activity_main); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Users\Administrator\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.3" 6 | defaultConfig { 7 | applicationId "zhuwentao.com.rippleview" 8 | minSdkVersion 14 9 | targetSdkVersion 23 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:23.4.0' 28 | testCompile 'junit:junit:4.12' 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/zhuwentao/com/rippleview/DensityUtil.java: -------------------------------------------------------------------------------- 1 | 2 | package zhuwentao.com.rippleview; 3 | 4 | import android.content.Context; 5 | 6 | /** 7 | * dp与px相互转换的工具类 8 | */ 9 | public class DensityUtil { 10 | 11 | /** 12 | * 根据手机的分辨率从 dp 的单位 转成为 px(像素) 13 | * @param context 上下文 14 | * @param dpValue dp值 15 | * @return px值 16 | */ 17 | public static int dip2px(Context context, float dpValue) { 18 | final float scale = context.getResources().getDisplayMetrics().density; 19 | return (int) (dpValue * scale + 0.5f); 20 | } 21 | 22 | /** 23 | * 根据手机的分辨率从 px(像素) 的单位 转成为 dp 24 | * @param context 上下文 25 | * @param pxValue px值 26 | * @return dp值 27 | */ 28 | public static int px2dip(Context context, float pxValue) { 29 | final float scale = context.getResources().getDisplayMetrics().density; 30 | return (int) (pxValue / scale + 0.5f); 31 | } 32 | 33 | /** 34 | * 将px值转换为sp值,保证文字大小不变 35 | * 36 | * @param context 上下文对象 37 | * @param pxValue 字体大小像素 38 | * @return 字体大小sp 39 | */ 40 | public static int px2sp(Context context, float pxValue) { 41 | final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; 42 | return (int) (pxValue / fontScale + 0.5f); 43 | } 44 | 45 | /** 46 | * 将sp值转换为px值,保证文字大小不变 47 | * 48 | * @param context 上下文对象 49 | * @param spValue 字体大小sp 50 | * @return 字体大小像素 51 | */ 52 | public static int sp2px(Context context, float spValue) { 53 | final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; 54 | return (int) (spValue * fontScale + 0.5f); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 16 | 17 | 24 | 25 | 32 | 33 | 40 | 41 | 42 | 47 | 48 | 55 | 56 | 62 | 63 | 69 | 70 | 71 | 76 | 77 | 83 | 84 | 90 | 91 | 97 | 98 | 99 | 104 | 105 | 112 | 113 | 120 | 121 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /app/src/main/java/zhuwentao/com/rippleview/RippleView.java: -------------------------------------------------------------------------------- 1 | package zhuwentao.com.rippleview; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.util.AttributeSet; 9 | import android.view.View; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | /** 15 | * 涟漪效果 16 | *

17 | * Created by zhuwentao on 2018-03-07. 18 | */ 19 | public class RippleView extends View { 20 | 21 | private Context mContext; 22 | 23 | // 画笔对象 24 | private Paint mPaint; 25 | 26 | // View宽 27 | private float mWidth; 28 | 29 | // View高 30 | private float mHeight; 31 | 32 | // 声波的圆圈集合 33 | private List mRipples; 34 | 35 | private int sqrtNumber; 36 | 37 | // 圆圈扩散的速度 38 | private int mSpeed; 39 | 40 | // 圆圈之间的密度 41 | private int mDensity; 42 | 43 | // 圆圈的颜色 44 | private int mColor; 45 | 46 | // 圆圈是否为填充模式 47 | private boolean mIsFill; 48 | 49 | // 圆圈是否为渐变模式 50 | private boolean mIsAlpha; 51 | 52 | public RippleView(Context context) { 53 | this(context, null); 54 | } 55 | 56 | public RippleView(Context context, AttributeSet attrs) { 57 | this(context, attrs, 0); 58 | } 59 | 60 | public RippleView(Context context, AttributeSet attrs, int defStyleAttr) { 61 | super(context, attrs, defStyleAttr); 62 | // 获取用户配置属性 63 | TypedArray tya = context.obtainStyledAttributes(attrs, R.styleable.mRippleView); 64 | mColor = tya.getColor(R.styleable.mRippleView_cColor, Color.BLUE); 65 | mSpeed = tya.getInt(R.styleable.mRippleView_cSpeed, 1); 66 | mDensity = tya.getInt(R.styleable.mRippleView_cDensity, 10); 67 | mIsFill = tya.getBoolean(R.styleable.mRippleView_cIsFill, false); 68 | mIsAlpha = tya.getBoolean(R.styleable.mRippleView_cIsAlpha, false); 69 | tya.recycle(); 70 | 71 | init(); 72 | } 73 | 74 | private void init() { 75 | mContext = getContext(); 76 | 77 | // 设置画笔样式 78 | mPaint = new Paint(); 79 | mPaint.setColor(mColor); 80 | mPaint.setStrokeWidth(DensityUtil.dip2px(mContext, 1)); 81 | if (mIsFill) { 82 | mPaint.setStyle(Paint.Style.FILL); 83 | } else { 84 | mPaint.setStyle(Paint.Style.STROKE); 85 | } 86 | mPaint.setStrokeCap(Paint.Cap.ROUND); 87 | mPaint.setAntiAlias(true); 88 | 89 | // 添加第一个圆圈 90 | mRipples = new ArrayList<>(); 91 | Circle c = new Circle(0, 255); 92 | mRipples.add(c); 93 | 94 | mDensity = DensityUtil.dip2px(mContext, mDensity); 95 | 96 | // 设置View的圆为半透明 97 | setBackgroundColor(Color.TRANSPARENT); 98 | } 99 | 100 | @Override 101 | public void onDraw(Canvas canvas) { 102 | super.onDraw(canvas); 103 | 104 | // 内切正方形 105 | drawInCircle(canvas); 106 | 107 | // 外切正方形 108 | // drawOutCircle(canvas); 109 | } 110 | 111 | /** 112 | * 圆到宽度 113 | * 114 | * @param canvas 115 | */ 116 | private void drawInCircle(Canvas canvas) { 117 | canvas.save(); 118 | 119 | // 处理每个圆的宽度和透明度 120 | for (int i = 0; i < mRipples.size(); i++) { 121 | Circle c = mRipples.get(i); 122 | mPaint.setAlpha(c.alpha);// (透明)0~255(不透明) 123 | canvas.drawCircle(mWidth / 2, mHeight / 2, c.width - mPaint.getStrokeWidth(), mPaint); 124 | 125 | // 当圆超出View的宽度后删除 126 | if (c.width > mWidth / 2) { 127 | mRipples.remove(i); 128 | } else { 129 | // 计算不透明的数值,这里有个小知识,就是如果不加上double的话,255除以一个任意比它大的数都将是0 130 | if (mIsAlpha) { 131 | double alpha = 255 - c.width * (255 / ((double) mWidth / 2)); 132 | c.alpha = (int) alpha; 133 | } 134 | // 修改这个值控制速度 135 | c.width += mSpeed; 136 | } 137 | } 138 | 139 | 140 | // 里面添加圆 141 | if (mRipples.size() > 0) { 142 | // 控制第二个圆出来的间距 143 | if (mRipples.get(mRipples.size() - 1).width > DensityUtil.dip2px(mContext, mDensity)) { 144 | mRipples.add(new Circle(0, 255)); 145 | } 146 | } 147 | 148 | 149 | invalidate(); 150 | 151 | canvas.restore(); 152 | } 153 | 154 | 155 | /** 156 | * 圆到对角线 157 | * 158 | * @param canvas 159 | */ 160 | private void drawOutCircle(Canvas canvas) { 161 | canvas.save(); 162 | 163 | // 使用勾股定律求得一个外切正方形中心点离角的距离 164 | sqrtNumber = (int) (Math.sqrt(mWidth * mWidth + mHeight * mHeight) / 2); 165 | 166 | // 变大 167 | for (int i = 0; i < mRipples.size(); i++) { 168 | 169 | // 启动圆圈 170 | Circle c = mRipples.get(i); 171 | mPaint.setAlpha(c.alpha);// (透明)0~255(不透明) 172 | canvas.drawCircle(mWidth / 2, mHeight / 2, c.width - mPaint.getStrokeWidth(), mPaint); 173 | 174 | // 当圆超出对角线后删掉 175 | if (c.width > sqrtNumber) { 176 | mRipples.remove(i); 177 | } else { 178 | // 计算不透明的度数 179 | double degree = 255 - c.width * (255 / (double) sqrtNumber); 180 | c.alpha = (int) degree; 181 | c.width += 1; 182 | } 183 | } 184 | 185 | // 里面添加圆 186 | if (mRipples.size() > 0) { 187 | // 控制第二个圆出来的间距 188 | if (mRipples.get(mRipples.size() - 1).width == 50) { 189 | mRipples.add(new Circle(0, 255)); 190 | } 191 | } 192 | invalidate(); 193 | canvas.restore(); 194 | } 195 | 196 | @Override 197 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 198 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 199 | int myWidthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 200 | int myWidthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 201 | int myHeightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 202 | int myHeightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 203 | 204 | // 获取宽度 205 | if (myWidthSpecMode == MeasureSpec.EXACTLY) { 206 | // match_parent 207 | mWidth = myWidthSpecSize; 208 | } else { 209 | // wrap_content 210 | mWidth = DensityUtil.dip2px(mContext, 120); 211 | } 212 | 213 | // 获取高度 214 | if (myHeightSpecMode == MeasureSpec.EXACTLY) { 215 | mHeight = myHeightSpecSize; 216 | } else { 217 | // wrap_content 218 | mHeight = DensityUtil.dip2px(mContext, 120); 219 | } 220 | 221 | // 设置该view的宽高 222 | setMeasuredDimension((int) mWidth, (int) mHeight); 223 | } 224 | 225 | 226 | class Circle { 227 | Circle(int width, int alpha) { 228 | this.width = width; 229 | this.alpha = alpha; 230 | } 231 | 232 | int width; 233 | 234 | int alpha; 235 | } 236 | 237 | } --------------------------------------------------------------------------------