├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── runConfigurations.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── leavesc │ │ └── hello │ │ └── floatball │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── leavesc │ │ │ └── hello │ │ │ └── floatball │ │ │ ├── MainActivity.java │ │ │ ├── anager │ │ │ └── ViewManager.java │ │ │ ├── service │ │ │ └── StartFloatBallService.java │ │ │ └── view │ │ │ ├── FloatBall.java │ │ │ ├── FloatMenu.java │ │ │ └── ProgressBall.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── ninja.png │ │ ├── layout │ │ ├── activity_main.xml │ │ └── float_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── leavesc │ └── hello │ └── floatball │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | FloatBall -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 先来看一张动态图 2 | 3 | ![](http://upload-images.jianshu.io/upload_images/2552605-04d65b77a17a4a1e?imageMogr2/auto-orient/strip) 4 | 5 | 昨天跟着视频学了如何自定义View并做成仿360悬浮球与加速球的样式 6 | 7 | 可以看出来,做成的效果有: 8 | 9 | - 点击按钮后退出Activity,呈现一个圆形的悬浮球,可以随意拖动并会自动依靠到屏幕一侧,且拖动时会变成一张图片 10 | - 当点击悬浮球时,悬浮球隐藏,底部出现一个加速球,双击加速球时,呈现水量逐渐增高且波动幅度较小的效果,单击时波浪上下波动且幅度渐小 11 | - 点击屏幕不包含底部加速球的部位,加速球会隐藏,悬浮球重新出现 12 | 13 | 要做出这么一个效果,需要两个自定义View与一个自定义ViewGroup 14 | 15 | ![](http://upload-images.jianshu.io/upload_images/2552605-90e192425bbdb734?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 16 | 17 | 首先,需要先设计悬浮球View——FloatBall 18 | 简单起见,为FloatBall指定一个默认宽度和高度——150像素 19 | 然后在`onDraw(Canvas canvas)`方法中,判断FloatBall是否正在被拖动isDrag,如果是,则绘制一张默认图片bitmap,否则则根据绘图函数绘制圆形与居中文本 20 | 21 | ```java 22 | /** 23 | * Created by ZY on 2016/8/10. 24 | * 悬浮球 25 | */ 26 | public class FloatBall extends View { 27 | 28 | public int width = 150; 29 | 30 | public int height = 150; 31 | //默认显示的文本 32 | private String text = "50%"; 33 | //是否在拖动 34 | private boolean isDrag; 35 | 36 | private Paint ballPaint; 37 | 38 | private Paint textPaint; 39 | 40 | private Bitmap bitmap; 41 | 42 | public FloatBall(Context context) { 43 | super(context); 44 | init(); 45 | } 46 | 47 | public FloatBall(Context context, AttributeSet attrs) { 48 | super(context, attrs); 49 | init(); 50 | } 51 | 52 | public FloatBall(Context context, AttributeSet attrs, int defStyleAttr) { 53 | super(context, attrs, defStyleAttr); 54 | init(); 55 | } 56 | 57 | public void init() { 58 | ballPaint = new Paint(); 59 | ballPaint.setColor(Color.GRAY); 60 | ballPaint.setAntiAlias(true); 61 | 62 | textPaint = new Paint(); 63 | textPaint.setTextSize(25); 64 | textPaint.setColor(Color.WHITE); 65 | textPaint.setAntiAlias(true); 66 | textPaint.setFakeBoldText(true); 67 | 68 | Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.ninja); 69 | //将图片裁剪到指定大小 70 | bitmap = Bitmap.createScaledBitmap(src, width, height, true); 71 | } 72 | 73 | @Override 74 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 75 | setMeasuredDimension(width, height); 76 | } 77 | 78 | @Override 79 | protected void onDraw(Canvas canvas) { 80 | if (!isDrag) { 81 | canvas.drawCircle(width / 2, height / 2, width / 2, ballPaint); 82 | float textWidth = textPaint.measureText(text); 83 | Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); 84 | float dy = -(fontMetrics.descent + fontMetrics.ascent) / 2; 85 | canvas.drawText(text, width / 2 - textWidth / 2, height / 2 + dy, textPaint); 86 | } else { 87 | //正在被拖动时则显示指定图片 88 | canvas.drawBitmap(bitmap, 0, 0, null); 89 | } 90 | } 91 | 92 | //设置当前移动状态 93 | public void setDragState(boolean isDrag) { 94 | this.isDrag = isDrag; 95 | invalidate(); 96 | } 97 | } 98 | ``` 99 | 100 | 因为FloatBall是不存在于Activity中而在屏幕单独显示的,所以需要用WindowManager来添加View并显示 101 | 新建一个类,命名为ViewManager,用来总的管理View的显示与删除 102 | 私有化构造函数并采用单例模式 103 | 104 | ```java 105 | private static ViewManager manager; 106 | 107 | //私有化构造函数 108 | private ViewManager(Context context) { 109 | this.context = context; 110 | init(); 111 | } 112 | 113 | //获取ViewManager实例 114 | public static ViewManager getInstance(Context context) { 115 | if (manager == null) { 116 | manager = new ViewManager(context); 117 | } 118 | return manager; 119 | } 120 | ``` 121 | 122 | ViewManager包含有显示与隐藏悬浮球与加速球的函数 123 | 124 | ```java 125 | //显示浮动小球 126 | public void showFloatBall() { 127 | if (floatBallParams == null) { 128 | floatBallParams = new LayoutParams(); 129 | floatBallParams.width = floatBall.width; 130 | floatBallParams.height = floatBall.height - getStatusHeight(); 131 | floatBallParams.gravity = Gravity.TOP | Gravity.LEFT; 132 | floatBallParams.type = LayoutParams.TYPE_TOAST; 133 | floatBallParams.flags = LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_NOT_TOUCH_MODAL; 134 | floatBallParams.format = PixelFormat.RGBA_8888; 135 | } 136 | windowManager.addView(floatBall, floatBallParams); 137 | } 138 | 139 | //显示底部菜单 140 | private void showFloatMenu() { 141 | if (floatMenuParams == null) { 142 | floatMenuParams = new LayoutParams(); 143 | floatMenuParams.width = getScreenWidth(); 144 | floatMenuParams.height = getScreenHeight() - getStatusHeight(); 145 | floatMenuParams.gravity = Gravity.BOTTOM; 146 | floatMenuParams.type = LayoutParams.TYPE_TOAST; 147 | floatMenuParams.flags = LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_NOT_TOUCH_MODAL; 148 | floatMenuParams.format = PixelFormat.RGBA_8888; 149 | } 150 | windowManager.addView(floatMenu, floatMenuParams); 151 | } 152 | 153 | //隐藏底部菜单 154 | public void hideFloatMenu() { 155 | if (floatMenu != null) { 156 | windowManager.removeView(floatMenu); 157 | } 158 | } 159 | ``` 160 | 161 | 将悬浮球置于Service中开启,这样悬浮球就不那么容易被系统去除了 162 | 在onCreate()方法中调用showFloatBall() 163 | 164 | ```java 165 | public class StartFloatBallService extends Service { 166 | 167 | public StartFloatBallService() { 168 | } 169 | 170 | @Override 171 | public IBinder onBind(Intent intent) { 172 | // TODO: Return the communication channel to the service. 173 | throw new UnsupportedOperationException("Not yet implemented"); 174 | } 175 | 176 | @Override 177 | public void onCreate() { 178 | ViewManager manager = ViewManager.getInstance(this); 179 | manager.showFloatBall(); 180 | super.onCreate(); 181 | } 182 | } 183 | ``` 184 | 185 | 此时,只要为MainActivity添加一个按钮,并设定当点击按钮后开启Service,此时即可看到屏幕显示了一个悬浮球 186 | 187 | ```java 188 | public void startService(View view) { 189 | Intent intent = new Intent(this, StartFloatBallService.class); 190 | startService(intent); 191 | finish(); 192 | } 193 | ``` 194 | 195 | 不过此时悬浮球还不支持拖动与点击,还需要为其添加OnTouchListener与OnClickListener 196 | 197 | ```java 198 | View.OnTouchListener touchListener = new View.OnTouchListener() { 199 | float startX; 200 | float startY; 201 | float tempX; 202 | float tempY; 203 | 204 | @Override 205 | public boolean onTouch(View v, MotionEvent event) { 206 | switch (event.getAction()) { 207 | case MotionEvent.ACTION_DOWN: 208 | startX = event.getRawX(); 209 | startY = event.getRawY(); 210 | 211 | tempX = event.getRawX(); 212 | tempY = event.getRawY(); 213 | break; 214 | case MotionEvent.ACTION_MOVE: 215 | float x = event.getRawX() - startX; 216 | float y = event.getRawY() - startY; 217 | //计算偏移量,刷新视图 218 | floatBallParams.x += x; 219 | floatBallParams.y += y; 220 | floatBall.setDragState(true); 221 | windowManager.updateViewLayout(floatBall, floatBallParams); 222 | startX = event.getRawX(); 223 | startY = event.getRawY(); 224 | break; 225 | case MotionEvent.ACTION_UP: 226 | //判断松手时View的横坐标是靠近屏幕哪一侧,将View移动到依靠屏幕 227 | float endX = event.getRawX(); 228 | float endY = event.getRawY(); 229 | if (endX < getScreenWidth() / 2) { 230 | endX = 0; 231 | } else { 232 | endX = getScreenWidth() - floatBall.width; 233 | } 234 | floatBallParams.x = (int) endX; 235 | floatBall.setDragState(false); 236 | windowManager.updateViewLayout(floatBall, floatBallParams); 237 | //如果初始落点与松手落点的坐标差值超过6个像素,则拦截该点击事件 238 | //否则继续传递,将事件交给OnClickListener函数处理 239 | if (Math.abs(endX - tempX) > 6 && Math.abs(endY - tempY) > 6) { 240 | return true; 241 | } 242 | break; 243 | } 244 | return false; 245 | } 246 | }; 247 | OnClickListener clickListener = new OnClickListener() { 248 | 249 | @Override 250 | public void onClick(View v) { 251 | windowManager.removeView(floatBall); 252 | showFloatMenu(); 253 | floatMenu.startAnimation(); 254 | } 255 | }; 256 | floatBall.setOnTouchListener(touchListener); 257 | floatBall.setOnClickListener(clickListener); 258 | ``` 259 | 260 | 261 | 加速球ProgressBall的设计较为复杂,需要用到贝塞尔曲线来呈现波浪效果,且单击双击的效果也需要分开呈现 262 | 同样是让ProgressBall继承于View 263 | 进度值的意义在于限制水面最终上升到的高度,即根据目标进度值与最大进度值的比例来决定水面高度 264 | 波浪总的起伏次数Count用于在单击加速球时,水面上下波动的次数 265 | 266 | ```java 267 | //view的宽度 268 | private int width = 200; 269 | //view的高度 270 | private int height = 200; 271 | //最大进度值 272 | private final int maxProgress = 100; 273 | //当前进度值 274 | private int currentProgress = 0; 275 | //目标进度值 276 | private final int targetProgress = 70; 277 | //是否为单击 278 | private boolean isSingleTop; 279 | //设定波浪总的起伏次数 280 | private final int Count = 20; 281 | //当前起伏次数 282 | private int currentCount; 283 | //初始振幅大小 284 | private final int startAmplitude = 15; 285 | //波浪周期性出现的次数 286 | private final int cycleCount = width / (startAmplitude * 4) + 1; 287 | ``` 288 | 289 | 初始化画笔与监听函数 290 | 291 | ```java 292 | private void init() { 293 | //初始化小球画笔 294 | ballPaint = new Paint(); 295 | ballPaint.setAntiAlias(true); 296 | ballPaint.setColor(Color.argb(0xff, 0x3a, 0x8c, 0x6c)); 297 | //初始化(波浪)进度条画笔 298 | progressPaint = new Paint(); 299 | progressPaint.setAntiAlias(true); 300 | progressPaint.setColor(Color.argb(0xff, 0x4e, 0xc9, 0x63)); 301 | progressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); 302 | //初始化文字画笔 303 | textPaint = new Paint(); 304 | textPaint.setAntiAlias(true); 305 | textPaint.setColor(Color.WHITE); 306 | textPaint.setTextSize(25); 307 | 308 | handler = new Handler(); 309 | path = new Path(); 310 | bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 311 | bitmapCanvas = new Canvas(bitmap); 312 | 313 | //手势监听 314 | //重点在于将单击和双击操作分隔开 315 | SimpleOnGestureListener listener = new SimpleOnGestureListener() { 316 | //双击 317 | @Override 318 | public boolean onDoubleTap(MotionEvent e) { 319 | //当前波浪起伏次数为零,说明“单击效果”没有影响到现在 320 | if (currentCount == 0) { 321 | //当前进度为零或者已达到目标进度值,说明“双击效果”没有影响到现在,此时可以允许进行双击操作 322 | if (currentProgress == 0 || currentProgress == targetProgress) { 323 | currentProgress = 0; 324 | isSingleTop = false; 325 | startDoubleTapAnimation(); 326 | } 327 | } 328 | return super.onDoubleTap(e); 329 | } 330 | 331 | //单击 332 | @Override 333 | public boolean onSingleTapConfirmed(MotionEvent e) { 334 | //当前进度值等于目标进度值,且当前波动次数为零,则允许进行单击操作 335 | if (currentProgress == targetProgress && currentCount == 0) { 336 | isSingleTop = true; 337 | startSingleTapAnimation(); 338 | } 339 | return super.onSingleTapConfirmed(e); 340 | } 341 | }; 342 | gestureDetector = new GestureDetector(context, listener); 343 | setOnTouchListener(new OnTouchListener() { 344 | @Override 345 | public boolean onTouch(View v, MotionEvent event) { 346 | return gestureDetector.onTouchEvent(event); 347 | } 348 | }); 349 | //接受点击操作 350 | setClickable(true); 351 | } 352 | ``` 353 | 354 | 单击或双击后的渐变效果是利用Handler的`postDelayed(Runnable r, long delayMillis)`方法来实现的,可以设定一个延时时间去执行Runnable ,然后在Runnable 中再次调用自身 355 | 356 | ```java 357 | class DoubleTapRunnable implements Runnable { 358 | @Override 359 | public void run() { 360 | if (currentProgress < targetProgress) { 361 | invalidate(); 362 | handler.postDelayed(doubleTapRunnable, 50); 363 | currentProgress++; 364 | } else { 365 | handler.removeCallbacks(doubleTapRunnable); 366 | } 367 | } 368 | } 369 | 370 | //开启双击动作动画 371 | public void startDoubleTapAnimation() { 372 | handler.postDelayed(doubleTapRunnable, 50); 373 | } 374 | 375 | class SingleTapRunnable implements Runnable { 376 | @Override 377 | public void run() { 378 | if (currentCount < Count) { 379 | invalidate(); 380 | currentCount++; 381 | handler.postDelayed(singleTapRunnable, 100); 382 | } else { 383 | handler.removeCallbacks(singleTapRunnable); 384 | currentCount = 0; 385 | } 386 | } 387 | } 388 | 389 | //开启单击动作动画 390 | public void startSingleTapAnimation() { 391 | handler.postDelayed(singleTapRunnable, 100); 392 | } 393 | ``` 394 | 395 | onDraw(Canvas canvas)的重点在于根据比例值来计算水面高度 396 | 397 | ```java 398 | @Override 399 | protected void onDraw(Canvas canvas) { 400 | //绘制圆形 401 | bitmapCanvas.drawCircle(width / 2, height / 2, width / 2, ballPaint); 402 | path.reset(); 403 | //高度随当前进度值的变化而变化 404 | float y = (1 - (float) currentProgress / maxProgress) * height; 405 | //属性PorterDuff.Mode.SRC_IN代表了progressPaint只显示与下层层叠的部分, 406 | //所以以下四点虽然连起来是个矩形,可呈现出来的依然是圆形 407 | //右上角 408 | path.moveTo(width, y); 409 | //右下角 410 | path.lineTo(width, height); 411 | //左下角 412 | path.lineTo(0, height); 413 | //左上角 414 | path.lineTo(0, y); 415 | //绘制顶部波浪 416 | if (!isSingleTop) { 417 | //是双击 418 | //根据当前进度大小调整振幅大小,有逐渐减小的趋势 419 | float tempAmplitude = (1 - (float) currentProgress / targetProgress) * startAmplitude; 420 | for (int i = 0; i < cycleCount; i++) { 421 | path.rQuadTo(startAmplitude, tempAmplitude, 2 * startAmplitude, 0); 422 | path.rQuadTo(startAmplitude, -tempAmplitude, 2 * startAmplitude, 0); 423 | } 424 | } else { 425 | //是单击 426 | //根据当前次数调整振幅大小,有逐渐减小的趋势 427 | float tempAmplitude = (1 - (float) currentCount / Count) * startAmplitude; 428 | //因为想要形成波浪上下起伏的效果,所以根据currentCount的奇偶性来变化贝塞尔曲线转折点位置 429 | if (currentCount % 2 == 0) { 430 | for (int i = 0; i < cycleCount; i++) { 431 | path.rQuadTo(startAmplitude, tempAmplitude, 2 * startAmplitude, 0); 432 | path.rQuadTo(startAmplitude, -tempAmplitude, 2 * startAmplitude, 0); 433 | } 434 | } else { 435 | for (int i = 0; i < cycleCount; i++) { 436 | path.rQuadTo(startAmplitude, -tempAmplitude, 2 * startAmplitude, 0); 437 | path.rQuadTo(startAmplitude, tempAmplitude, 2 * startAmplitude, 0); 438 | } 439 | } 440 | } 441 | path.close(); 442 | bitmapCanvas.drawPath(path, progressPaint); 443 | String text = (int) (((float) currentProgress / maxProgress) * 100) + "%"; 444 | float textWidth = textPaint.measureText(text); 445 | Paint.FontMetrics metrics = textPaint.getFontMetrics(); 446 | float baseLine = height / 2 - (metrics.ascent + metrics.descent); 447 | bitmapCanvas.drawText(text, width / 2 - textWidth / 2, baseLine, textPaint); 448 | canvas.drawBitmap(bitmap, 0, 0, null); 449 | } 450 | ``` 451 | 452 | 因为要呈现ProgressBall时不仅仅是其本身,或者还需要背景色或者文本之类的内容,所以可以将其置于ViewGroup中来显示 453 | 布局文件 454 | 455 | ```java 456 | 457 | 460 | 461 | 470 | 471 | 478 | 479 | 483 | 484 | 485 | 486 | ``` 487 | 488 | FloatMenu就作为容纳ProgressBall的容器,并为其赋予从下往上滑动显示的动画效果 489 | 490 | ```java 491 | /** 492 | * Created by ZY on 2016/8/10. 493 | * 底部菜单栏 494 | */ 495 | public class FloatMenu extends LinearLayout { 496 | 497 | private LinearLayout layout; 498 | 499 | private TranslateAnimation animation; 500 | 501 | public FloatMenu(final Context context) { 502 | super(context); 503 | View root = View.inflate(context, R.layout.float_menu, null); 504 | layout = (LinearLayout) root.findViewById(R.id.layout); 505 | animation = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0, 506 | Animation.RELATIVE_TO_SELF, 0, 507 | Animation.RELATIVE_TO_SELF, 1.0f, 508 | Animation.RELATIVE_TO_SELF, 0); 509 | animation.setDuration(500); 510 | animation.setFillAfter(true); 511 | layout.setAnimation(animation); 512 | root.setOnTouchListener(new OnTouchListener() { 513 | @Override 514 | public boolean onTouch(View v, MotionEvent event) { 515 | ViewManager manager = ViewManager.getInstance(context); 516 | manager.showFloatBall(); 517 | manager.hideFloatMenu(); 518 | return false; 519 | } 520 | }); 521 | addView(root); 522 | } 523 | 524 | public void startAnimation() { 525 | animation.start(); 526 | } 527 | } 528 | ``` 529 | 530 | 这里提供源代码下载:https://github.com/leavesC/FloatBall -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "leavesc.hello.floatball" 7 | minSdkVersion 19 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | implementation 'com.android.support:appcompat-v7:28.0.0' 24 | testImplementation 'junit:junit:4.12' 25 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 26 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 27 | } 28 | -------------------------------------------------------------------------------- /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 C:\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/androidTest/java/leavesc/hello/floatball/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package leavesc.hello.floatball; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("leavesc.hello.floatball", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/leavesc/hello/floatball/MainActivity.java: -------------------------------------------------------------------------------- 1 | package leavesc.hello.floatball; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.view.View; 7 | 8 | import leavesc.hello.floatball.service.StartFloatBallService; 9 | 10 | /** 11 | * 作者:leavesC 12 | * 时间:2019/4/1 21:55 13 | * 描述: 14 | * GitHub:https://github.com/leavesC 15 | * Blog:https://www.jianshu.com/u/9df45b87cfdf 16 | */ 17 | public class MainActivity extends AppCompatActivity { 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_main); 23 | } 24 | 25 | public void startService(View view) { 26 | Intent intent = new Intent(this, StartFloatBallService.class); 27 | startService(intent); 28 | finish(); 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /app/src/main/java/leavesc/hello/floatball/anager/ViewManager.java: -------------------------------------------------------------------------------- 1 | package leavesc.hello.floatball.anager; 2 | 3 | import android.content.Context; 4 | import android.graphics.PixelFormat; 5 | import android.graphics.Point; 6 | import android.os.Build; 7 | import android.view.Gravity; 8 | import android.view.MotionEvent; 9 | import android.view.View; 10 | import android.view.View.OnClickListener; 11 | import android.view.WindowManager; 12 | import android.view.WindowManager.LayoutParams; 13 | 14 | import java.lang.reflect.Field; 15 | 16 | import leavesc.hello.floatball.view.FloatBall; 17 | import leavesc.hello.floatball.view.FloatMenu; 18 | 19 | /** 20 | * Created by ZY on 2016/8/10. 21 | * 管理者,单例模式 22 | */ 23 | public class ViewManager { 24 | 25 | private FloatBall floatBall; 26 | 27 | private FloatMenu floatMenu; 28 | 29 | private WindowManager windowManager; 30 | 31 | private static ViewManager manager; 32 | 33 | private LayoutParams floatBallParams; 34 | 35 | private LayoutParams floatMenuParams; 36 | 37 | private Context context; 38 | 39 | //私有化构造函数 40 | private ViewManager(Context context) { 41 | this.context = context; 42 | init(); 43 | } 44 | 45 | //获取ViewManager实例 46 | public static ViewManager getInstance(Context context) { 47 | if (manager == null) { 48 | synchronized (ViewManager.class) { 49 | if (manager == null) { 50 | manager = new ViewManager(context); 51 | } 52 | } 53 | } 54 | return manager; 55 | } 56 | 57 | private void init() { 58 | windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 59 | floatBall = new FloatBall(context); 60 | floatMenu = new FloatMenu(context); 61 | View.OnTouchListener touchListener = new View.OnTouchListener() { 62 | float startX; 63 | float startY; 64 | float tempX; 65 | float tempY; 66 | 67 | @Override 68 | public boolean onTouch(View v, MotionEvent event) { 69 | switch (event.getAction()) { 70 | case MotionEvent.ACTION_DOWN: 71 | startX = event.getRawX(); 72 | startY = event.getRawY(); 73 | 74 | tempX = event.getRawX(); 75 | tempY = event.getRawY(); 76 | break; 77 | case MotionEvent.ACTION_MOVE: 78 | float x = event.getRawX() - startX; 79 | float y = event.getRawY() - startY; 80 | //计算偏移量,刷新视图 81 | floatBallParams.x += x; 82 | floatBallParams.y += y; 83 | floatBall.setDragState(true); 84 | windowManager.updateViewLayout(floatBall, floatBallParams); 85 | startX = event.getRawX(); 86 | startY = event.getRawY(); 87 | break; 88 | case MotionEvent.ACTION_UP: 89 | //判断松手时View的横坐标是靠近屏幕哪一侧,将View移动到依靠屏幕 90 | float endX = event.getRawX(); 91 | float endY = event.getRawY(); 92 | if (endX < getScreenWidth() / 2) { 93 | endX = 0; 94 | } else { 95 | endX = getScreenWidth() - floatBall.width; 96 | } 97 | floatBallParams.x = (int) endX; 98 | floatBall.setDragState(false); 99 | windowManager.updateViewLayout(floatBall, floatBallParams); 100 | //如果初始落点与松手落点的坐标差值超过6个像素,则拦截该点击事件 101 | //否则继续传递,将事件交给OnClickListener函数处理 102 | if (Math.abs(endX - tempX) > 6 && Math.abs(endY - tempY) > 6) { 103 | return true; 104 | } 105 | break; 106 | } 107 | return false; 108 | } 109 | }; 110 | OnClickListener clickListener = new OnClickListener() { 111 | 112 | @Override 113 | public void onClick(View v) { 114 | windowManager.removeView(floatBall); 115 | showFloatMenu(); 116 | floatMenu.startAnimation(); 117 | } 118 | }; 119 | floatBall.setOnTouchListener(touchListener); 120 | floatBall.setOnClickListener(clickListener); 121 | } 122 | 123 | //显示浮动小球 124 | public void showFloatBall() { 125 | if (floatBallParams == null) { 126 | floatBallParams = new LayoutParams(); 127 | floatBallParams.width = floatBall.width; 128 | floatBallParams.height = floatBall.height - getStatusHeight(); 129 | floatBallParams.gravity = Gravity.TOP | Gravity.START; 130 | floatBallParams.flags = LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_NOT_TOUCH_MODAL; 131 | floatBallParams.format = PixelFormat.RGBA_8888; 132 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 133 | floatBallParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 134 | } else { 135 | floatBallParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; 136 | } 137 | } 138 | windowManager.addView(floatBall, floatBallParams); 139 | } 140 | 141 | //显示底部菜单 142 | private void showFloatMenu() { 143 | if (floatMenuParams == null) { 144 | floatMenuParams = new LayoutParams(); 145 | floatMenuParams.width = getScreenWidth(); 146 | floatMenuParams.height = getScreenHeight() - getStatusHeight(); 147 | floatMenuParams.gravity = Gravity.BOTTOM; 148 | floatMenuParams.flags = LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_NOT_TOUCH_MODAL; 149 | floatMenuParams.format = PixelFormat.RGBA_8888; 150 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 151 | floatMenuParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 152 | } else { 153 | floatMenuParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; 154 | } 155 | } 156 | windowManager.addView(floatMenu, floatMenuParams); 157 | } 158 | 159 | //隐藏底部菜单 160 | public void hideFloatMenu() { 161 | if (floatMenu != null) { 162 | windowManager.removeView(floatMenu); 163 | } 164 | } 165 | 166 | //获取屏幕宽度 167 | private int getScreenWidth() { 168 | Point point = new Point(); 169 | windowManager.getDefaultDisplay().getSize(point); 170 | return point.x; 171 | } 172 | 173 | //获取屏幕高度 174 | private int getScreenHeight() { 175 | Point point = new Point(); 176 | windowManager.getDefaultDisplay().getSize(point); 177 | return point.y; 178 | } 179 | 180 | //获取状态栏高度 181 | private int getStatusHeight() { 182 | try { 183 | Class c = Class.forName("com.android.internal.R$dimen"); 184 | Object object = c.newInstance(); 185 | Field field = c.getField("status_bar_height"); 186 | int x = (Integer) field.get(object); 187 | return context.getResources().getDimensionPixelSize(x); 188 | } catch (Exception e) { 189 | return 0; 190 | } 191 | } 192 | 193 | } -------------------------------------------------------------------------------- /app/src/main/java/leavesc/hello/floatball/service/StartFloatBallService.java: -------------------------------------------------------------------------------- 1 | package leavesc.hello.floatball.service; 2 | 3 | import android.app.Service; 4 | import android.content.Intent; 5 | import android.os.IBinder; 6 | 7 | import leavesc.hello.floatball.anager.ViewManager; 8 | 9 | public class StartFloatBallService extends Service { 10 | 11 | public StartFloatBallService() { 12 | } 13 | 14 | @Override 15 | public IBinder onBind(Intent intent) { 16 | // TODO: Return the communication channel to the service. 17 | throw new UnsupportedOperationException("Not yet implemented"); 18 | } 19 | 20 | @Override 21 | public void onCreate() { 22 | ViewManager manager = ViewManager.getInstance(this); 23 | manager.showFloatBall(); 24 | super.onCreate(); 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /app/src/main/java/leavesc/hello/floatball/view/FloatBall.java: -------------------------------------------------------------------------------- 1 | package leavesc.hello.floatball.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.Paint; 9 | import android.util.AttributeSet; 10 | import android.view.View; 11 | 12 | import leavesc.hello.floatball.R; 13 | 14 | /** 15 | * Created by ZY on 2016/8/10. 16 | * 悬浮球 17 | */ 18 | public class FloatBall extends View { 19 | 20 | public int width = 150; 21 | 22 | public int height = 150; 23 | //默认显示的文本 24 | private String text = "50%"; 25 | //是否在拖动 26 | private boolean isDrag; 27 | 28 | private Paint ballPaint; 29 | 30 | private Paint textPaint; 31 | 32 | private Bitmap bitmap; 33 | 34 | public FloatBall(Context context) { 35 | super(context); 36 | init(); 37 | } 38 | 39 | public FloatBall(Context context, AttributeSet attrs) { 40 | super(context, attrs); 41 | init(); 42 | } 43 | 44 | public FloatBall(Context context, AttributeSet attrs, int defStyleAttr) { 45 | super(context, attrs, defStyleAttr); 46 | init(); 47 | } 48 | 49 | public void init() { 50 | ballPaint = new Paint(); 51 | ballPaint.setColor(Color.GRAY); 52 | ballPaint.setAntiAlias(true); 53 | 54 | textPaint = new Paint(); 55 | textPaint.setTextSize(25); 56 | textPaint.setColor(Color.WHITE); 57 | textPaint.setAntiAlias(true); 58 | textPaint.setFakeBoldText(true); 59 | 60 | Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.ninja); 61 | //将图片裁剪到指定大小 62 | bitmap = Bitmap.createScaledBitmap(src, width, height, true); 63 | } 64 | 65 | @Override 66 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 67 | setMeasuredDimension(width, height); 68 | } 69 | 70 | @Override 71 | protected void onDraw(Canvas canvas) { 72 | if (!isDrag) { 73 | canvas.drawCircle(width / 2, height / 2, width / 2, ballPaint); 74 | float textWidth = textPaint.measureText(text); 75 | Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); 76 | float dy = -(fontMetrics.descent + fontMetrics.ascent) / 2; 77 | canvas.drawText(text, width / 2 - textWidth / 2, height / 2 + dy, textPaint); 78 | } else { 79 | //正在被拖动时则显示指定图片 80 | canvas.drawBitmap(bitmap, 0, 0, null); 81 | } 82 | } 83 | 84 | //设置当前移动状态 85 | public void setDragState(boolean isDrag) { 86 | this.isDrag = isDrag; 87 | invalidate(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/leavesc/hello/floatball/view/FloatMenu.java: -------------------------------------------------------------------------------- 1 | package leavesc.hello.floatball.view; 2 | 3 | import android.content.Context; 4 | import android.view.MotionEvent; 5 | import android.view.View; 6 | import android.view.animation.Animation; 7 | import android.view.animation.TranslateAnimation; 8 | import android.widget.LinearLayout; 9 | 10 | import leavesc.hello.floatball.R; 11 | import leavesc.hello.floatball.anager.ViewManager; 12 | 13 | /** 14 | * Created by ZY on 2016/8/10. 15 | * 底部菜单栏 16 | */ 17 | public class FloatMenu extends LinearLayout { 18 | 19 | private TranslateAnimation animation; 20 | 21 | public FloatMenu(final Context context) { 22 | super(context); 23 | View root = View.inflate(context, R.layout.float_menu, null); 24 | LinearLayout layout = root.findViewById(R.id.layout); 25 | animation = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0, 26 | Animation.RELATIVE_TO_SELF, 0, 27 | Animation.RELATIVE_TO_SELF, 1.0f, 28 | Animation.RELATIVE_TO_SELF, 0); 29 | animation.setDuration(500); 30 | animation.setFillAfter(true); 31 | layout.setAnimation(animation); 32 | root.setOnTouchListener(new OnTouchListener() { 33 | @Override 34 | public boolean onTouch(View v, MotionEvent event) { 35 | ViewManager manager = ViewManager.getInstance(context); 36 | manager.showFloatBall(); 37 | manager.hideFloatMenu(); 38 | return false; 39 | } 40 | }); 41 | addView(root); 42 | } 43 | 44 | public void startAnimation() { 45 | animation.start(); 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /app/src/main/java/leavesc/hello/floatball/view/ProgressBall.java: -------------------------------------------------------------------------------- 1 | package leavesc.hello.floatball.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.graphics.Path; 9 | import android.graphics.PorterDuff; 10 | import android.graphics.PorterDuffXfermode; 11 | import android.os.Handler; 12 | import android.util.AttributeSet; 13 | import android.view.GestureDetector; 14 | import android.view.GestureDetector.SimpleOnGestureListener; 15 | import android.view.MotionEvent; 16 | import android.view.View; 17 | 18 | /** 19 | * Created by ZY on 2016/8/10. 20 | * 实现的效果为: 21 | * 双击后小球内的波浪高度从零增加到目标进度值targetProgress 22 | * 单击后波浪呈现上下浮动且波动渐小的效果 23 | */ 24 | public class ProgressBall extends View { 25 | 26 | //view的宽度 27 | private int width = 200; 28 | //view的高度 29 | private int height = 200; 30 | //最大进度值 31 | private static final int maxProgress = 100; 32 | //当前进度值 33 | private int currentProgress = 0; 34 | //目标进度值 35 | private final int targetProgress = 70; 36 | //是否为单击 37 | private boolean isSingleTop; 38 | //设定波浪总的起伏次数 39 | private final int Count = 20; 40 | //当前起伏次数 41 | private int currentCount; 42 | //初始振幅大小 43 | private final int startAmplitude = 15; 44 | //波浪周期性出现的次数 45 | private final int cycleCount = width / (startAmplitude * 4) + 1; 46 | 47 | private DoubleTapRunnable doubleTapRunnable = new DoubleTapRunnable(); 48 | 49 | private SingleTapRunnable singleTapRunnable = new SingleTapRunnable(); 50 | 51 | private Canvas bitmapCanvas; 52 | 53 | private Bitmap bitmap; 54 | 55 | private Path path; 56 | 57 | private Paint ballPaint; 58 | 59 | private Paint progressPaint; 60 | 61 | private Paint textPaint; 62 | 63 | private Context context; 64 | 65 | private Handler handler; 66 | 67 | private GestureDetector gestureDetector; 68 | 69 | public ProgressBall(Context context) { 70 | super(context); 71 | this.context = context; 72 | init(); 73 | } 74 | 75 | public ProgressBall(Context context, AttributeSet attrs) { 76 | super(context, attrs); 77 | this.context = context; 78 | init(); 79 | } 80 | 81 | public ProgressBall(Context context, AttributeSet attrs, int defStyleAttr) { 82 | super(context, attrs, defStyleAttr); 83 | this.context = context; 84 | init(); 85 | } 86 | 87 | private void init() { 88 | //初始化小球画笔 89 | ballPaint = new Paint(); 90 | ballPaint.setAntiAlias(true); 91 | ballPaint.setColor(Color.argb(0xff, 0x3a, 0x8c, 0x6c)); 92 | //初始化(波浪)进度条画笔 93 | progressPaint = new Paint(); 94 | progressPaint.setAntiAlias(true); 95 | progressPaint.setColor(Color.argb(0xff, 0x4e, 0xc9, 0x63)); 96 | progressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); 97 | //初始化文字画笔 98 | textPaint = new Paint(); 99 | textPaint.setAntiAlias(true); 100 | textPaint.setColor(Color.WHITE); 101 | textPaint.setTextSize(25); 102 | 103 | handler = new Handler(); 104 | path = new Path(); 105 | bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 106 | bitmapCanvas = new Canvas(bitmap); 107 | 108 | //手势监听 109 | //重点在于将单击和双击操作分隔开 110 | SimpleOnGestureListener listener = new SimpleOnGestureListener() { 111 | //双击 112 | @Override 113 | public boolean onDoubleTap(MotionEvent e) { 114 | //当前波浪起伏次数为零,说明“单击效果”没有影响到现在 115 | if (currentCount == 0) { 116 | //当前进度为零或者已达到目标进度值,说明“双击效果”没有影响到现在,此时可以允许进行双击操作 117 | if (currentProgress == 0 || currentProgress == targetProgress) { 118 | currentProgress = 0; 119 | isSingleTop = false; 120 | startDoubleTapAnimation(); 121 | } 122 | } 123 | return super.onDoubleTap(e); 124 | } 125 | 126 | //单击 127 | @Override 128 | public boolean onSingleTapConfirmed(MotionEvent e) { 129 | //当前进度值等于目标进度值,且当前波动次数为零,则允许进行单击操作 130 | if (currentProgress == targetProgress && currentCount == 0) { 131 | isSingleTop = true; 132 | startSingleTapAnimation(); 133 | } 134 | return super.onSingleTapConfirmed(e); 135 | } 136 | }; 137 | gestureDetector = new GestureDetector(context, listener); 138 | setOnTouchListener(new OnTouchListener() { 139 | @Override 140 | public boolean onTouch(View v, MotionEvent event) { 141 | return gestureDetector.onTouchEvent(event); 142 | } 143 | }); 144 | //接受点击操作 145 | setClickable(true); 146 | } 147 | 148 | private class DoubleTapRunnable implements Runnable { 149 | @Override 150 | public void run() { 151 | if (currentProgress < targetProgress) { 152 | invalidate(); 153 | handler.postDelayed(doubleTapRunnable, 50); 154 | currentProgress++; 155 | } else { 156 | handler.removeCallbacks(doubleTapRunnable); 157 | } 158 | } 159 | } 160 | 161 | //开启双击动作动画 162 | private void startDoubleTapAnimation() { 163 | handler.postDelayed(doubleTapRunnable, 50); 164 | } 165 | 166 | private class SingleTapRunnable implements Runnable { 167 | @Override 168 | public void run() { 169 | if (currentCount < Count) { 170 | invalidate(); 171 | currentCount++; 172 | handler.postDelayed(singleTapRunnable, 100); 173 | } else { 174 | handler.removeCallbacks(singleTapRunnable); 175 | currentCount = 0; 176 | } 177 | } 178 | } 179 | 180 | //开启单击动作动画 181 | private void startSingleTapAnimation() { 182 | handler.postDelayed(singleTapRunnable, 100); 183 | } 184 | 185 | @Override 186 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 187 | setMeasuredDimension(width, height); 188 | } 189 | 190 | @Override 191 | protected void onDraw(Canvas canvas) { 192 | //绘制圆形 193 | bitmapCanvas.drawCircle(width / 2, height / 2, width / 2, ballPaint); 194 | path.reset(); 195 | //高度随当前进度值的变化而变化 196 | float y = (1 - (float) currentProgress / maxProgress) * height; 197 | //属性PorterDuff.Mode.SRC_IN代表了progressPaint只显示与下层层叠的部分, 198 | //所以以下四点虽然连起来是个矩形,可呈现出来的依然是圆形 199 | //右上角 200 | path.moveTo(width, y); 201 | //右下角 202 | path.lineTo(width, height); 203 | //左下角 204 | path.lineTo(0, height); 205 | //左上角 206 | path.lineTo(0, y); 207 | //绘制顶部波浪 208 | if (!isSingleTop) { 209 | //是双击 210 | //根据当前进度大小调整振幅大小,有逐渐减小的趋势 211 | float tempAmplitude = (1 - (float) currentProgress / targetProgress) * startAmplitude; 212 | for (int i = 0; i < cycleCount; i++) { 213 | path.rQuadTo(startAmplitude, tempAmplitude, 2 * startAmplitude, 0); 214 | path.rQuadTo(startAmplitude, -tempAmplitude, 2 * startAmplitude, 0); 215 | } 216 | } else { 217 | //是单击 218 | //根据当前次数调整振幅大小,有逐渐减小的趋势 219 | float tempAmplitude = (1 - (float) currentCount / Count) * startAmplitude; 220 | //因为想要形成波浪上下起伏的效果,所以根据currentCount的奇偶性来变化贝塞尔曲线转折点位置 221 | if (currentCount % 2 == 0) { 222 | for (int i = 0; i < cycleCount; i++) { 223 | path.rQuadTo(startAmplitude, tempAmplitude, 2 * startAmplitude, 0); 224 | path.rQuadTo(startAmplitude, -tempAmplitude, 2 * startAmplitude, 0); 225 | } 226 | } else { 227 | for (int i = 0; i < cycleCount; i++) { 228 | path.rQuadTo(startAmplitude, -tempAmplitude, 2 * startAmplitude, 0); 229 | path.rQuadTo(startAmplitude, tempAmplitude, 2 * startAmplitude, 0); 230 | } 231 | } 232 | } 233 | path.close(); 234 | bitmapCanvas.drawPath(path, progressPaint); 235 | String text = (int) (((float) currentProgress / maxProgress) * 100) + "%"; 236 | float textWidth = textPaint.measureText(text); 237 | Paint.FontMetrics metrics = textPaint.getFontMetrics(); 238 | float baseLine = height / 2 - (metrics.ascent + metrics.descent); 239 | bitmapCanvas.drawText(text, width / 2 - textWidth / 2, baseLine, textPaint); 240 | canvas.drawBitmap(bitmap, 0, 0, null); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ninja.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leavesCZY/FloatBall/c605d9e2ebe425ef57b3bc3de45d23d1e71c036d/app/src/main/res/drawable/ninja.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 |