├── .gitignore ├── FloatMenu ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── yw │ │ └── game │ │ └── floatmenu │ │ ├── DotImageView.java │ │ ├── FloatItem.java │ │ ├── FloatLogoMenu.java │ │ ├── FloatMenuView.java │ │ ├── Utils.java │ │ └── customfloat │ │ └── BaseFloatDialog.java │ └── res │ └── values │ └── strings.xml ├── FloatMenuDemo ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── yw │ │ └── game │ │ └── floatmenu │ │ └── demo │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── yw │ │ │ └── game │ │ │ └── floatmenu │ │ │ └── demo │ │ │ ├── MainActivity.java │ │ │ └── MyFloatDialog.java │ └── res │ │ ├── drawable-xxxhdpi │ │ ├── yw_game_logo.png │ │ ├── yw_image_float_logo.png │ │ ├── yw_menu_account.png │ │ ├── yw_menu_fb.png │ │ ├── yw_menu_msg.png │ │ ├── ywgame_floatmenu_gift.png │ │ └── ywgame_floatmenu_user.png │ │ ├── drawable │ │ ├── yw_game_float_menu_bg.xml │ │ └── yw_game_menu_black_bg.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── layout_menu_left.xml │ │ ├── layout_menu_logo.xml │ │ └── layout_menu_right.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 │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── yw │ └── game │ └── floatmenu │ └── demo │ └── ExampleUnitTest.java ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── picture ├── 20160503125603.png ├── 201605031543.gif ├── 201605041543.gif ├── 201606161036.gif ├── 2016112315.gif ├── chinareadlogo.png ├── floatmen.png └── floatmenu2.gif └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | /.idea 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | -------------------------------------------------------------------------------- /FloatMenu/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /FloatMenu/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | 3 | apply plugin: 'com.android.library' 4 | apply plugin: 'com.github.dcendents.android-maven' 5 | apply plugin: 'com.jfrog.bintray' 6 | 7 | version = "2.2.0" 8 | 9 | android { 10 | compileSdkVersion 28 11 | 12 | defaultConfig { 13 | minSdkVersion 11 14 | targetSdkVersion 28 15 | versionCode 1 16 | versionName "1.1" 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | lintOptions{ 26 | abortOnError false 27 | // This seems to be firing due to okio referencing java.nio.File 28 | // which is harmless for us. 29 | warning 'InvalidPackage', 30 | // don't need parcel creator for the sub-class of MessageSnapshot. 31 | 'ParcelCreator' 32 | } 33 | 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_1_7 36 | targetCompatibility JavaVersion.VERSION_1_7 37 | } 38 | 39 | } 40 | 41 | dependencies { 42 | implementation fileTree(include: ['*.jar'], dir: 'libs') 43 | implementation 'com.android.support:support-v4:28.0.0' 44 | } 45 | 46 | 47 | def siteUrl = 'https://github.com/fanOfDemo/FloatMenuSample' // 项目的主页 48 | def gitUrl = 'https://github.com/fanOfDemo/FloatMenuSample.git' // Git仓库的url 49 | group = "com.yw.game.floatmenu" // Maven Group ID for the artifact,一般填你唯一的包名 50 | install { 51 | repositories.mavenInstaller { 52 | // This generates POM.xml with proper parameters 53 | pom { 54 | project { 55 | packaging 'aar' 56 | // Add your description here 57 | name 'FloatMenu' //项目描述 58 | url siteUrl 59 | // Set your license 60 | licenses { 61 | license { 62 | name 'The BSD 3-Clause License' 63 | url 'http://www.linfo.org/bsdlicense.html' 64 | } 65 | } 66 | developers { 67 | developer { 68 | id 'yw' //填写开发者基本信息 69 | name 'fanofdemo' 70 | email '18720625976@163.com' 71 | } 72 | } 73 | scm { 74 | connection gitUrl 75 | developerConnection gitUrl 76 | url siteUrl 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | task sourcesJar(type: Jar) { 84 | from android.sourceSets.main.java.srcDirs 85 | classifier = 'sources' 86 | } 87 | 88 | 89 | 90 | //tasks.findByPath(":FloatMenu:androidJavadocs").enabled = false 91 | 92 | task javadoc(type: Javadoc) { 93 | options.encoding = "utf-8" 94 | source = android.sourceSets.main.java.srcDirs 95 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 96 | } 97 | 98 | task javadocJar(type: Jar, dependsOn: javadoc) { 99 | classifier = 'javadoc' 100 | from javadoc.destinationDir 101 | } 102 | 103 | artifacts { 104 | archives javadocJar 105 | archives sourcesJar 106 | } 107 | 108 | 109 | Properties properties = new Properties() 110 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 111 | 112 | bintray { 113 | user = properties.getProperty("bintray.user") 114 | key = properties.getProperty("bintray.apikey") 115 | 116 | configurations = ['archives'] 117 | pkg { 118 | repo = "maven" 119 | name = "FloatMenu" 120 | websiteUrl = siteUrl 121 | vcsUrl = gitUrl 122 | licenses = ["The BSD 3-Clause License"] 123 | publish = true 124 | } 125 | } 126 | 127 | 128 | -------------------------------------------------------------------------------- /FloatMenu/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:\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 | -------------------------------------------------------------------------------- /FloatMenu/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /FloatMenu/src/main/java/com/yw/game/floatmenu/DotImageView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, Shanghai YUEWEN Information Technology Co., Ltd. 3 | * All rights reserved. 4 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | * 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | * Neither the name of Shanghai YUEWEN Information Technology Co., Ltd. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | * 10 | * THIS SOFTWARE IS PROVIDED BY SHANGHAI YUEWEN INFORMATION TECHNOLOGY CO., LTD. AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | * 12 | */ 13 | package com.yw.game.floatmenu; 14 | 15 | import android.animation.Animator; 16 | import android.animation.ValueAnimator; 17 | import android.content.Context; 18 | import android.graphics.Bitmap; 19 | import android.graphics.Camera; 20 | import android.graphics.Canvas; 21 | import android.graphics.Color; 22 | import android.graphics.Matrix; 23 | import android.graphics.Paint; 24 | import android.graphics.Rect; 25 | import android.support.annotation.Nullable; 26 | import android.text.TextUtils; 27 | import android.util.AttributeSet; 28 | import android.view.View; 29 | import android.view.animation.LinearInterpolator; 30 | 31 | 32 | /** 33 | * Created by wengyiming on 2017/7/21. 34 | */ 35 | 36 | /** 37 | * 00%=FF(不透明) 5%=F2 10%=E5 15%=D8 20%=CC 25%=BF 30%=B2 35%=A5 40%=99 45%=8c 50%=7F 38 | * 55%=72 60%=66 65%=59 70%=4c 75%=3F 80%=33 85%=21 90%=19 95%=0c 100%=00(全透明) 39 | */ 40 | public class DotImageView extends View { 41 | private static final String TAG = DotImageView.class.getSimpleName(); 42 | public static final int NORMAL = 0;//不隐藏 43 | public static final int HIDE_LEFT = 1;//左边隐藏 44 | public static final int HIDE_RIGHT = 2;//右边隐藏 45 | private Paint mPaint;//用于画anything 46 | 47 | private Paint mPaintBg;//用于画anything 48 | private String dotNum = null;//红点数字 49 | private float mAlphaValue;//透明度动画值 50 | private float mRotateValue = 1f;//旋转动画值 51 | private boolean inited = false;//标记透明动画是否执行过,防止因onreseme 切换导致重复执行 52 | 53 | 54 | private Bitmap mBitmap;//logo 55 | private final int mLogoBackgroundRadius = dip2px(25);//logo的灰色背景圆的半径 56 | private final int mLogoWhiteRadius = dip2px(20);//logo的白色背景的圆的半径 57 | private final int mRedPointRadiusWithNum = dip2px(6);//红点圆半径 58 | private final int mRedPointRadius = dip2px(3);//红点圆半径 59 | private final int mRedPointOffset = dip2px(10);//红点对logo的偏移量,比如左红点就是logo中心的 x - mRedPointOffset 60 | 61 | private boolean isDrag = false;//是否 绘制旋转放大动画,只有 非停靠边缘才绘制 62 | private float scaleOffset;//放大偏移值 63 | private ValueAnimator mDragValueAnimator;//放大、旋转 属性动画 64 | private LinearInterpolator mLinearInterpolator = new LinearInterpolator();//通用用加速器 65 | public boolean mDrawDarkBg = true;//是否绘制黑色背景,当菜单关闭时,才绘制灰色背景 66 | private static final float hideOffset = 0.4f;//往左右隐藏多少宽度的偏移值, 隐藏宽度的0.4 67 | private Camera mCamera;//camera用于执行3D动画 68 | 69 | private boolean mDrawNum = false;//只绘制红点还是红点+白色数字 70 | 71 | private int mStatus = NORMAL;//0 正常,1 左,2右,3 中间方法旋转 72 | private int mLastStatus = mStatus; 73 | private Matrix mMatrix; 74 | private boolean mIsResetPosition; 75 | 76 | private int mBgColor = 0x99000000; 77 | 78 | 79 | public void setBgColor(int bgColor) { 80 | mBgColor = bgColor; 81 | } 82 | 83 | 84 | public void setDrawNum(boolean drawNum) { 85 | this.mDrawNum = drawNum; 86 | } 87 | 88 | public void setDrawDarkBg(boolean drawDarkBg) { 89 | mDrawDarkBg = drawDarkBg; 90 | invalidate(); 91 | } 92 | 93 | public int getStatus() { 94 | return mStatus; 95 | } 96 | 97 | 98 | public void setStatus(int status) { 99 | this.mStatus = status; 100 | isDrag = false; 101 | if (this.mStatus != NORMAL) { 102 | setDrawNum(mDrawNum); 103 | this.mDrawDarkBg = true; 104 | } 105 | invalidate(); 106 | 107 | } 108 | 109 | public void setBitmap(Bitmap bitmap) { 110 | mBitmap = bitmap; 111 | } 112 | 113 | public DotImageView(Context context, Bitmap bitmap) { 114 | super(context); 115 | this.mBitmap = bitmap; 116 | init(); 117 | } 118 | 119 | 120 | public DotImageView(Context context) { 121 | super(context); 122 | init(); 123 | } 124 | 125 | public DotImageView(Context context, @Nullable AttributeSet attrs) { 126 | super(context, attrs); 127 | init(); 128 | } 129 | 130 | public DotImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 131 | super(context, attrs, defStyleAttr); 132 | init(); 133 | } 134 | 135 | private void init() { 136 | mPaint = new Paint(); 137 | mPaint.setAntiAlias(true); 138 | mPaint.setTextSize(sp2px(10)); 139 | mPaint.setStyle(Paint.Style.FILL); 140 | 141 | mPaintBg = new Paint(); 142 | mPaintBg.setAntiAlias(true); 143 | mPaintBg.setStyle(Paint.Style.FILL); 144 | mPaintBg.setColor(mBgColor);//60% 黑色背景 (透明度 40%) 145 | 146 | mCamera = new Camera(); 147 | mMatrix = new Matrix(); 148 | } 149 | 150 | /** 151 | * 这个方法是否有优化空间 152 | */ 153 | @Override 154 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 155 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 156 | int wh = mLogoBackgroundRadius * 2; 157 | setMeasuredDimension(wh, wh); 158 | } 159 | 160 | @Override 161 | protected void onDraw(Canvas canvas) { 162 | super.onDraw(canvas); 163 | float centerX = getWidth() / 2; 164 | float centerY = getHeight() / 2; 165 | canvas.save();//保存一份快照,方便后面恢复 166 | mCamera.save(); 167 | if (mStatus == NORMAL) { 168 | if (mLastStatus != NORMAL) { 169 | canvas.restore();//恢复画布的原始快照 170 | mCamera.restore(); 171 | } 172 | 173 | if (isDrag) { 174 | //如果当前是拖动状态则放大并旋转 175 | canvas.scale((scaleOffset + 1f), (scaleOffset + 1f), getWidth() / 2, getHeight() / 2); 176 | if (mIsResetPosition) { 177 | //手指拖动后离开屏幕复位时使用 x轴旋转 3d动画 178 | mCamera.save(); 179 | mCamera.rotateX(720 * scaleOffset);//0-720度 最多转两圈 180 | mCamera.getMatrix(mMatrix); 181 | 182 | mMatrix.preTranslate(-getWidth() / 2, -getHeight() / 2); 183 | mMatrix.postTranslate(getWidth() / 2, getHeight() / 2); 184 | canvas.concat(mMatrix); 185 | mCamera.restore(); 186 | } else { 187 | //手指拖动且手指未离开屏幕则使用 绕图心2d旋转动画 188 | canvas.rotate(60 * mRotateValue, getWidth() / 2, getHeight() / 2); 189 | } 190 | } 191 | 192 | 193 | } else if (mStatus == HIDE_LEFT) { 194 | canvas.translate(-getWidth() * hideOffset, 0); 195 | canvas.rotate(-45, getWidth() / 2, getHeight() / 2); 196 | 197 | } else if (mStatus == HIDE_RIGHT) { 198 | canvas.translate(getWidth() * hideOffset, 0); 199 | canvas.rotate(45, getWidth() / 2, getHeight() / 2); 200 | } 201 | canvas.save(); 202 | if (!isDrag) { 203 | if (mDrawDarkBg) { 204 | mPaintBg.setColor(mBgColor); 205 | canvas.drawCircle(centerX, centerY, mLogoBackgroundRadius, mPaintBg); 206 | // 60% 白色 (透明度 40%) 207 | mPaint.setColor(0x99ffffff); 208 | } else { 209 | //100% 白色背景 (透明度 0%) 210 | mPaint.setColor(0xFFFFFFFF); 211 | } 212 | if (mAlphaValue != 0) { 213 | mPaint.setAlpha((int) (mAlphaValue * 255)); 214 | } 215 | canvas.drawCircle(centerX, centerY, mLogoWhiteRadius, mPaint); 216 | } 217 | 218 | canvas.restore(); 219 | //100% 白色背景 (透明度 0%) 220 | mPaint.setColor(0xFFFFFFFF); 221 | int left = (int) (centerX - mBitmap.getWidth() / 2); 222 | int top = (int) (centerY - mBitmap.getHeight() / 2); 223 | canvas.drawBitmap(mBitmap, left, top, mPaint); 224 | 225 | 226 | if (!TextUtils.isEmpty(dotNum)) { 227 | int readPointRadus = (mDrawNum ? mRedPointRadiusWithNum : mRedPointRadius); 228 | mPaint.setColor(Color.RED); 229 | if (mStatus == HIDE_LEFT) { 230 | canvas.drawCircle(centerX + mRedPointOffset, centerY - mRedPointOffset, readPointRadus, mPaint); 231 | if (mDrawNum) { 232 | mPaint.setColor(Color.WHITE); 233 | canvas.drawText(dotNum, centerX + mRedPointOffset - getTextWidth(dotNum, mPaint) / 2, centerY - mRedPointOffset + getTextHeight(dotNum, mPaint) / 2, mPaint); 234 | } 235 | } else if (mStatus == HIDE_RIGHT) { 236 | canvas.drawCircle(centerX - mRedPointOffset, centerY - mRedPointOffset, readPointRadus, mPaint); 237 | if (mDrawNum) { 238 | mPaint.setColor(Color.WHITE); 239 | canvas.drawText(dotNum, centerX - mRedPointOffset - getTextWidth(dotNum, mPaint) / 2, centerY - mRedPointOffset + getTextHeight(dotNum, mPaint) / 2, mPaint); 240 | } 241 | } else { 242 | if (mLastStatus == HIDE_LEFT) { 243 | canvas.drawCircle(centerX + mRedPointOffset, centerY - mRedPointOffset, readPointRadus, mPaint); 244 | if (mDrawNum) { 245 | mPaint.setColor(Color.WHITE); 246 | canvas.drawText(dotNum, centerX + mRedPointOffset - getTextWidth(dotNum, mPaint) / 2, centerY - mRedPointOffset + getTextHeight(dotNum, mPaint) / 2, mPaint); 247 | } 248 | } else if (mLastStatus == HIDE_RIGHT) { 249 | canvas.drawCircle(centerX - mRedPointOffset, centerY - mRedPointOffset, readPointRadus, mPaint); 250 | if (mDrawNum) { 251 | mPaint.setColor(Color.WHITE); 252 | canvas.drawText(dotNum, centerX - mRedPointOffset - getTextWidth(dotNum, mPaint) / 2, centerY - mRedPointOffset + getTextHeight(dotNum, mPaint) / 2, mPaint); 253 | } 254 | } 255 | } 256 | } 257 | mLastStatus = mStatus; 258 | } 259 | 260 | 261 | public void setDotNum(int num, Animator.AnimatorListener l) { 262 | if (!inited) { 263 | startAnim(num, l); 264 | } else { 265 | refreshDot(num); 266 | } 267 | } 268 | 269 | private void refreshDot(int num) { 270 | if (num > 0) { 271 | String dotNumTmp = String.valueOf(num); 272 | if (!TextUtils.equals(dotNum, dotNumTmp)) { 273 | dotNum = dotNumTmp; 274 | invalidate(); 275 | } 276 | } else { 277 | dotNum = null; 278 | } 279 | } 280 | 281 | 282 | public void startAnim(final int num, Animator.AnimatorListener l) { 283 | ValueAnimator valueAnimator = ValueAnimator.ofFloat(1.f, 0.6f, 1f, 0.6f); 284 | valueAnimator.setInterpolator(mLinearInterpolator); 285 | valueAnimator.setDuration(3000); 286 | valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 287 | @Override 288 | public void onAnimationUpdate(ValueAnimator animation) { 289 | mAlphaValue = (float) animation.getAnimatedValue(); 290 | invalidate(); 291 | 292 | } 293 | }); 294 | valueAnimator.addListener(l); 295 | valueAnimator.addListener(new Animator.AnimatorListener() { 296 | @Override 297 | public void onAnimationStart(Animator animation) { 298 | 299 | } 300 | 301 | @Override 302 | public void onAnimationEnd(Animator animation) { 303 | inited = true; 304 | refreshDot(num); 305 | mAlphaValue = 0; 306 | 307 | } 308 | 309 | @Override 310 | public void onAnimationCancel(Animator animation) { 311 | mAlphaValue = 0; 312 | } 313 | 314 | @Override 315 | public void onAnimationRepeat(Animator animation) { 316 | 317 | } 318 | }); 319 | valueAnimator.start(); 320 | } 321 | 322 | public void setDrag(boolean drag, float offset, boolean isResetPosition) { 323 | isDrag = drag; 324 | this.mIsResetPosition = isResetPosition; 325 | if (offset > 0 && offset != this.scaleOffset) { 326 | this.scaleOffset = offset; 327 | } 328 | if (isDrag && mStatus == NORMAL) { 329 | if (mDragValueAnimator != null) { 330 | if (mDragValueAnimator.isRunning()) return; 331 | } 332 | mDragValueAnimator = ValueAnimator.ofFloat(0, 6f, 12f, 0f); 333 | mDragValueAnimator.setInterpolator(mLinearInterpolator); 334 | mDragValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 335 | @Override 336 | public void onAnimationUpdate(ValueAnimator animation) { 337 | mRotateValue = (float) animation.getAnimatedValue(); 338 | invalidate(); 339 | } 340 | }); 341 | mDragValueAnimator.addListener(new Animator.AnimatorListener() { 342 | @Override 343 | public void onAnimationStart(Animator animation) { 344 | 345 | } 346 | 347 | @Override 348 | public void onAnimationEnd(Animator animation) { 349 | isDrag = false; 350 | mIsResetPosition = false; 351 | } 352 | 353 | @Override 354 | public void onAnimationCancel(Animator animation) { 355 | 356 | } 357 | 358 | @Override 359 | public void onAnimationRepeat(Animator animation) { 360 | 361 | } 362 | }); 363 | mDragValueAnimator.setDuration(1000); 364 | mDragValueAnimator.start(); 365 | } 366 | } 367 | 368 | private int dip2px(float dipValue) { 369 | final float scale = getContext().getResources().getDisplayMetrics().density; 370 | return (int) (dipValue * scale + 0.5f); 371 | } 372 | 373 | private int sp2px(float spValue) { 374 | final float fontScale = getContext().getResources().getDisplayMetrics().scaledDensity; 375 | return (int) (spValue * fontScale + 0.5f); 376 | } 377 | 378 | private float getTextHeight(String text, Paint paint) { 379 | Rect rect = new Rect(); 380 | paint.getTextBounds(text, 0, text.length(), rect); 381 | return rect.height() / 1.1f; 382 | } 383 | 384 | private float getTextWidth(String text, Paint paint) { 385 | return paint.measureText(text); 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /FloatMenu/src/main/java/com/yw/game/floatmenu/FloatItem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, Shanghai YUEWEN Information Technology Co., Ltd. 3 | * All rights reserved. 4 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | * 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | * Neither the name of Shanghai YUEWEN Information Technology Co., Ltd. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | * 10 | * THIS SOFTWARE IS PROVIDED BY SHANGHAI YUEWEN INFORMATION TECHNOLOGY CO., LTD. AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | * 12 | */ 13 | package com.yw.game.floatmenu; 14 | 15 | import android.graphics.Bitmap; 16 | import android.graphics.Color; 17 | 18 | /** 19 | * Created by wengyiming on 2017/7/21. 20 | */ 21 | 22 | public class FloatItem { 23 | public String title; 24 | public int titleColor = Color.BLACK; 25 | public int bgColor = Color.WHITE; 26 | public Bitmap icon; 27 | public String dotNum = null; 28 | 29 | public FloatItem(String title, int titleColor, int bgColor, Bitmap icon, String dotNum) { 30 | this.title = title; 31 | this.titleColor = titleColor; 32 | this.bgColor = bgColor; 33 | this.icon = icon; 34 | this.dotNum = dotNum; 35 | } 36 | 37 | public String getDotNum() { 38 | return dotNum; 39 | } 40 | 41 | 42 | public FloatItem(String title, int titleColor, int bgColor, Bitmap bitmap) { 43 | this.title = title; 44 | this.titleColor = titleColor; 45 | this.bgColor = bgColor; 46 | this.icon = bitmap; 47 | } 48 | 49 | public String getTitle() { 50 | return title; 51 | } 52 | 53 | public void setTitle(String title) { 54 | this.title = title; 55 | } 56 | 57 | public int getTitleColor() { 58 | return titleColor; 59 | } 60 | 61 | public void setTitleColor(int titleColor) { 62 | this.titleColor = titleColor; 63 | } 64 | 65 | public int getBgColor() { 66 | return bgColor; 67 | } 68 | 69 | public void setBgColor(int bgColor) { 70 | this.bgColor = bgColor; 71 | } 72 | 73 | public Bitmap getIcon() { 74 | return icon; 75 | } 76 | 77 | public void setIcon(Bitmap icon) { 78 | this.icon = icon; 79 | } 80 | 81 | 82 | @Override 83 | public boolean equals(Object obj) { 84 | if (obj == null) { 85 | return false; 86 | } 87 | if (obj == this) return true; 88 | 89 | if (obj instanceof FloatItem) { 90 | FloatItem floatItem = (FloatItem) obj; 91 | return floatItem.title.equals(this.title); 92 | } else { 93 | return false; 94 | } 95 | } 96 | 97 | @Override 98 | public int hashCode() { 99 | return title.hashCode(); 100 | } 101 | 102 | @Override 103 | public String toString() { 104 | return "FloatItem{" + 105 | "title='" + title + '\'' + 106 | ", titleColor=" + titleColor + 107 | ", bgColor=" + bgColor + 108 | ", icon=" + icon + 109 | ", dotNum='" + dotNum + '\'' + 110 | '}'; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /FloatMenu/src/main/java/com/yw/game/floatmenu/FloatLogoMenu.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, Shanghai YUEWEN Information Technology Co., Ltd. 3 | * All rights reserved. 4 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | * 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | * Neither the name of Shanghai YUEWEN Information Technology Co., Ltd. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | * 10 | * THIS SOFTWARE IS PROVIDED BY SHANGHAI YUEWEN INFORMATION TECHNOLOGY CO., LTD. AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | * 12 | */ 13 | package com.yw.game.floatmenu; 14 | 15 | import android.animation.Animator; 16 | import android.animation.ValueAnimator; 17 | import android.app.Activity; 18 | import android.content.Context; 19 | import android.content.SharedPreferences; 20 | import android.graphics.Bitmap; 21 | import android.graphics.Color; 22 | import android.graphics.PixelFormat; 23 | import android.graphics.drawable.Drawable; 24 | import android.os.Build; 25 | import android.os.CountDownTimer; 26 | import android.os.Handler; 27 | import android.os.Looper; 28 | import android.text.TextUtils; 29 | import android.util.TypedValue; 30 | import android.view.Gravity; 31 | import android.view.MotionEvent; 32 | import android.view.View; 33 | import android.view.View.OnClickListener; 34 | import android.view.View.OnTouchListener; 35 | import android.view.ViewGroup; 36 | import android.view.WindowManager; 37 | import android.view.animation.Interpolator; 38 | import android.view.animation.LinearInterpolator; 39 | import android.widget.LinearLayout; 40 | 41 | 42 | import java.util.ArrayList; 43 | import java.util.List; 44 | 45 | /** 46 | * Created by wengyiming on 2017/7/20. 47 | */ 48 | public class FloatLogoMenu { 49 | /** 50 | * 记录 logo 停放的位置,以备下次恢复 51 | */ 52 | private static final String LOCATION_X = "hintLocation"; 53 | private static final String LOCATION_Y = "locationY"; 54 | 55 | /** 56 | * 悬浮球 坐落 左 右 标记 57 | */ 58 | public static final int LEFT = 0; 59 | public static final int RIGHT = 1; 60 | 61 | /** 62 | * 记录系统状态栏的高度 63 | */ 64 | private int mStatusBarHeight; 65 | /** 66 | * 记录当前手指位置在屏幕上的横坐标值 67 | */ 68 | private float mXInScreen; 69 | 70 | /** 71 | * 记录当前手指位置在屏幕上的纵坐标值 72 | */ 73 | private float mYInScreen; 74 | 75 | /** 76 | * 记录手指按下时在屏幕上的横坐标的值 77 | */ 78 | private float mXDownInScreen; 79 | 80 | /** 81 | * 记录手指按下时在屏幕上的纵坐标的值 82 | */ 83 | private float mYDownInScreen; 84 | 85 | /** 86 | * 记录手指按下时在小悬浮窗的View上的横坐标的值 87 | */ 88 | private float mXInView; 89 | 90 | /** 91 | * 记录手指按下时在小悬浮窗的View上的纵坐标的值 92 | */ 93 | private float mYinView; 94 | 95 | /** 96 | * 记录屏幕的宽度 97 | */ 98 | private int mScreenWidth; 99 | 100 | /** 101 | * 来自 activity 的 wManager 102 | */ 103 | private WindowManager wManager; 104 | 105 | 106 | /** 107 | * 为 wManager 设置 LayoutParams 108 | */ 109 | private WindowManager.LayoutParams wmParams; 110 | 111 | /** 112 | * 带透明度动画、旋转、放大的悬浮球 113 | */ 114 | private DotImageView mFloatLogo; 115 | 116 | 117 | /** 118 | * 用于 定时 隐藏 logo的定时器 119 | */ 120 | private CountDownTimer mHideTimer; 121 | 122 | 123 | /** 124 | * float menu的高度 125 | */ 126 | private Handler mHandler = new Handler(Looper.getMainLooper()); 127 | 128 | 129 | /** 130 | * 悬浮窗左右移动到默认位置 动画的 加速器 131 | */ 132 | private Interpolator mLinearInterpolator = new LinearInterpolator(); 133 | 134 | /** 135 | * 用于记录上次菜单打开的时间,判断时间间隔 136 | */ 137 | private static double DOUBLE_CLICK_TIME = 0L; 138 | 139 | /** 140 | * 标记是否拖动中 141 | */ 142 | private boolean isDrag = false; 143 | 144 | /** 145 | * 用于恢复悬浮球的location的属性动画值 146 | */ 147 | private int mResetLocationValue; 148 | 149 | /** 150 | * 手指离开屏幕后 用于恢复 悬浮球的 logo 的左右位置 151 | */ 152 | private Runnable updatePositionRunnable = new Runnable() { 153 | @Override 154 | public void run() { 155 | isDrag = true; 156 | checkPosition(); 157 | } 158 | }; 159 | 160 | /** 161 | * 这个事件不做任何事情、直接return false则 onclick 事件生效 162 | */ 163 | private OnTouchListener mDefaultOnTouchListerner = new OnTouchListener() { 164 | @Override 165 | public boolean onTouch(View v, MotionEvent event) { 166 | isDrag = false; 167 | return false; 168 | } 169 | }; 170 | 171 | /** 172 | * 这个事件用于处理移动、自定义点击或者其它事情,return true可以保证onclick事件失效 173 | */ 174 | private OnTouchListener touchListener = new OnTouchListener() { 175 | @Override 176 | public boolean onTouch(View v, MotionEvent event) { 177 | switch (event.getAction()) { 178 | case MotionEvent.ACTION_DOWN: 179 | floatEventDown(event); 180 | break; 181 | case MotionEvent.ACTION_MOVE: 182 | floatEventMove(event); 183 | break; 184 | case MotionEvent.ACTION_UP: 185 | case MotionEvent.ACTION_CANCEL: 186 | floatEventUp(); 187 | break; 188 | } 189 | return true; 190 | } 191 | }; 192 | 193 | 194 | /** 195 | * 菜单背景颜色 196 | */ 197 | private int mBackMenuColor = 0xffe4e3e1; 198 | 199 | /** 200 | * 是否绘制红点数字 201 | */ 202 | private boolean mDrawRedPointNum; 203 | 204 | 205 | /** 206 | * 是否绘制圆形菜单项,false绘制方形 207 | */ 208 | private boolean mCircleMenuBg; 209 | 210 | 211 | /** 212 | * R.drawable.yw_game_logo 213 | * 214 | * @param floatItems 215 | */ 216 | private Bitmap mLogoRes; 217 | 218 | /** 219 | * 用于显示在 mActivity 上的 mActivity 220 | */ 221 | private Context mActivity; 222 | 223 | /** 224 | * 菜单 点击、关闭 监听 225 | */ 226 | private FloatMenuView.OnMenuClickListener mOnMenuClickListener; 227 | 228 | 229 | /** 230 | * 停靠默认位置 231 | */ 232 | private int mDefaultLocation = RIGHT; 233 | 234 | 235 | /** 236 | * 悬浮窗 坐落 位置 237 | */ 238 | private int mHintLocation = mDefaultLocation; 239 | 240 | 241 | /** 242 | * 用于记录菜单项内容 243 | */ 244 | private List mFloatItems = new ArrayList<>(); 245 | 246 | private LinearLayout rootViewRight; 247 | 248 | private LinearLayout rootView; 249 | 250 | private ValueAnimator valueAnimator; 251 | 252 | private boolean isExpanded = false; 253 | 254 | private Drawable mBackground; 255 | 256 | 257 | private FloatLogoMenu(Builder builder) { 258 | mBackMenuColor = builder.mBackMenuColor; 259 | mDrawRedPointNum = builder.mDrawRedPointNum; 260 | mCircleMenuBg = builder.mCircleMenuBg; 261 | mLogoRes = builder.mLogoRes; 262 | mActivity = builder.mActivity; 263 | mOnMenuClickListener = builder.mOnMenuClickListener; 264 | mDefaultLocation = builder.mDefaultLocation; 265 | mFloatItems = builder.mFloatItems; 266 | mBackground = builder.mDrawable; 267 | 268 | // if (mActivity == null || mActivity.isFinishing() || mActivity.getWindowManager() == null) { 269 | // throw new IllegalArgumentException("Activity = null, or Activity is isFinishing ,or this Activity`s token is bad"); 270 | // } 271 | 272 | if (mLogoRes == null) { 273 | throw new IllegalArgumentException("No logo found,you can setLogo/showWithLogo to set a FloatLogo "); 274 | } 275 | 276 | if (mFloatItems.isEmpty()) { 277 | throw new IllegalArgumentException("At least one menu item!"); 278 | } 279 | 280 | initFloatWindow(); 281 | initTimer(); 282 | initFloat(); 283 | 284 | } 285 | 286 | public void setFloatItemList(List floatItems) { 287 | this.mFloatItems = floatItems; 288 | calculateDotNum(); 289 | } 290 | 291 | /** 292 | * 初始化悬浮球 window 293 | */ 294 | private void initFloatWindow() { 295 | wmParams = new WindowManager.LayoutParams(); 296 | if (mActivity instanceof Activity) { 297 | Activity activity = (Activity) mActivity; 298 | wManager = activity.getWindowManager(); 299 | //类似dialog,寄托在activity的windows上,activity关闭时需要关闭当前float 300 | wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION; 301 | } else { 302 | wManager = (WindowManager) mActivity.getSystemService(Context.WINDOW_SERVICE); 303 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 304 | //在android7.1以上系统需要使用TYPE_PHONE类型 配合运行时权限 305 | wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 306 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 307 | //在android7.1以上系统需要使用TYPE_PHONE类型 配合运行时权限 308 | wmParams.type = WindowManager.LayoutParams.TYPE_PHONE; 309 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 310 | wmParams.type = WindowManager.LayoutParams.TYPE_TOAST; 311 | } else { 312 | wmParams.type = WindowManager.LayoutParams.TYPE_PHONE; 313 | } 314 | } 315 | mScreenWidth = wManager.getDefaultDisplay().getWidth(); 316 | int screenHeight = wManager.getDefaultDisplay().getHeight(); 317 | 318 | //判断状态栏是否显示 如果不显示则statusBarHeight为0 319 | mStatusBarHeight = dp2Px(25, mActivity); 320 | 321 | wmParams.format = PixelFormat.RGBA_8888; 322 | wmParams.gravity = Gravity.LEFT | Gravity.TOP; 323 | wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; 324 | mHintLocation = getSetting(LOCATION_X, mDefaultLocation); 325 | int defaultY = ((screenHeight - mStatusBarHeight) / 2) / 3; 326 | int y = getSetting(LOCATION_Y, defaultY); 327 | if (mHintLocation == LEFT) { 328 | wmParams.x = 0; 329 | } else { 330 | wmParams.x = mScreenWidth; 331 | } 332 | 333 | if (y != 0 && y != defaultY) { 334 | wmParams.y = y; 335 | } else { 336 | wmParams.y = defaultY; 337 | } 338 | wmParams.alpha = 1; 339 | wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT; 340 | wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT; 341 | } 342 | 343 | 344 | /** 345 | * 初始化悬浮球 346 | */ 347 | private void initFloat() { 348 | generateLeftLineLayout(); 349 | generateRightLineLayout(); 350 | mFloatLogo = new DotImageView(mActivity, mLogoRes); 351 | mFloatLogo.setLayoutParams(new WindowManager.LayoutParams(dp2Px(50, mActivity), dp2Px(50, mActivity))); 352 | mFloatLogo.setDrawNum(mDrawRedPointNum); 353 | mFloatLogo.setBgColor(mBackMenuColor); 354 | mFloatLogo.setDrawDarkBg(true); 355 | calculateDotNum(); 356 | floatBtnEvent(); 357 | try { 358 | wManager.addView(mFloatLogo, wmParams); 359 | } catch (Exception e) { 360 | e.printStackTrace(); 361 | } 362 | 363 | } 364 | 365 | private void generateLeftLineLayout() { 366 | DotImageView floatLogo = new DotImageView(mActivity, mLogoRes); 367 | floatLogo.setLayoutParams(new WindowManager.LayoutParams(dp2Px(50, mActivity), dp2Px(50, mActivity))); 368 | floatLogo.setDrawNum(mDrawRedPointNum); 369 | floatLogo.setDrawDarkBg(false); 370 | 371 | rootView = new LinearLayout(mActivity); 372 | rootView.setOrientation(LinearLayout.HORIZONTAL); 373 | rootView.setGravity(Gravity.CENTER); 374 | rootView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, dp2Px(50, mActivity))); 375 | 376 | rootView.setBackgroundDrawable(mBackground); 377 | 378 | 379 | FloatMenuView mFloatMenuView = new FloatMenuView.Builder(mActivity) 380 | .setFloatItems(mFloatItems) 381 | .setBackgroundColor(Color.TRANSPARENT) 382 | .setCicleBg(mCircleMenuBg) 383 | .setStatus(FloatMenuView.STATUS_LEFT) 384 | .setMenuBackgroundColor(Color.TRANSPARENT) 385 | .drawNum(mDrawRedPointNum) 386 | .create(); 387 | setMenuClickListener(mFloatMenuView); 388 | 389 | rootView.addView(floatLogo); 390 | rootView.addView(mFloatMenuView); 391 | 392 | 393 | floatLogo.setOnClickListener(new OnClickListener() { 394 | @Override 395 | public void onClick(View v) { 396 | if (isExpanded) { 397 | try { 398 | wManager.removeViewImmediate(rootView); 399 | wManager.addView(FloatLogoMenu.this.mFloatLogo, wmParams); 400 | } catch (Exception e) { 401 | e.printStackTrace(); 402 | } 403 | isExpanded = false; 404 | } 405 | } 406 | }); 407 | } 408 | 409 | private void generateRightLineLayout() { 410 | final DotImageView floatLogo = new DotImageView(mActivity, mLogoRes); 411 | floatLogo.setLayoutParams(new WindowManager.LayoutParams(dp2Px(50, mActivity), dp2Px(50, mActivity))); 412 | floatLogo.setDrawNum(mDrawRedPointNum); 413 | floatLogo.setDrawDarkBg(false); 414 | 415 | floatLogo.setOnClickListener(new OnClickListener() { 416 | @Override 417 | public void onClick(View v) { 418 | if (isExpanded) { 419 | try { 420 | wManager.removeViewImmediate(rootViewRight); 421 | wManager.addView(FloatLogoMenu.this.mFloatLogo, wmParams); 422 | } catch (Exception e) { 423 | e.printStackTrace(); 424 | } 425 | isExpanded = false; 426 | } 427 | } 428 | }); 429 | 430 | rootViewRight = new LinearLayout(mActivity); 431 | rootViewRight.setOrientation(LinearLayout.HORIZONTAL); 432 | rootViewRight.setGravity(Gravity.CENTER); 433 | rootViewRight.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, dp2Px(50, mActivity))); 434 | 435 | 436 | rootViewRight.setBackgroundDrawable(mBackground); 437 | 438 | 439 | FloatMenuView mFloatMenuView = new FloatMenuView.Builder(mActivity) 440 | .setFloatItems(mFloatItems) 441 | .setBackgroundColor(Color.TRANSPARENT) 442 | .setCicleBg(mCircleMenuBg) 443 | .setStatus(FloatMenuView.STATUS_RIGHT) 444 | .setMenuBackgroundColor(Color.TRANSPARENT) 445 | .drawNum(mDrawRedPointNum) 446 | .create(); 447 | setMenuClickListener(mFloatMenuView); 448 | 449 | rootViewRight.addView(mFloatMenuView); 450 | rootViewRight.addView(floatLogo); 451 | 452 | 453 | } 454 | 455 | /** 456 | * 初始化 隐藏悬浮球的定时器 457 | */ 458 | private void initTimer() { 459 | mHideTimer = new CountDownTimer(2000, 10) { //悬浮窗超过5秒没有操作的话会自动隐藏 460 | @Override 461 | public void onTick(long millisUntilFinished) { 462 | if (isExpanded) { 463 | mHideTimer.cancel(); 464 | } 465 | } 466 | 467 | @Override 468 | public void onFinish() { 469 | if (isExpanded) { 470 | mHideTimer.cancel(); 471 | return; 472 | } 473 | if (!isDrag) { 474 | if (mHintLocation == LEFT) { 475 | mFloatLogo.setStatus(DotImageView.HIDE_LEFT); 476 | mFloatLogo.setDrawDarkBg(true); 477 | } else { 478 | mFloatLogo.setStatus(DotImageView.HIDE_RIGHT); 479 | mFloatLogo.setDrawDarkBg(true); 480 | } 481 | // mFloatLogo.setOnTouchListener(mDefaultOnTouchListerner);//把onClick事件分发下去,防止onclick无效 482 | } 483 | } 484 | }; 485 | } 486 | 487 | 488 | /** 489 | * 用于 拦截 菜单项的 关闭事件,以方便开始 隐藏定时器 490 | * 491 | * @param mFloatMenuView 492 | */ 493 | private void setMenuClickListener(FloatMenuView mFloatMenuView) { 494 | mFloatMenuView.setOnMenuClickListener(new FloatMenuView.OnMenuClickListener() { 495 | @Override 496 | public void onItemClick(int position, String title) { 497 | mOnMenuClickListener.onItemClick(position, title); 498 | } 499 | 500 | @Override 501 | public void dismiss() { 502 | mFloatLogo.setDrawDarkBg(true); 503 | mOnMenuClickListener.dismiss(); 504 | mHideTimer.start(); 505 | } 506 | }); 507 | 508 | } 509 | 510 | 511 | /** 512 | * 悬浮窗的点击事件和touch事件的切换 513 | */ 514 | private void floatBtnEvent() { 515 | //这里的onClick只有 touchListener = mDefaultOnTouchListener 才会触发 516 | // mFloatLogo.setOnClickListener(new OnClickListener() { 517 | // @Override 518 | // public void onClick(View v) { 519 | // if (!isDrag) { 520 | // if (mFloatLogo.getStatus() != DotImageView.NORMAL) { 521 | // mFloatLogo.setBitmap(mLogoRes); 522 | // mFloatLogo.setStatus(DotImageView.NORMAL); 523 | // if (!mFloatLogo.mDrawDarkBg) { 524 | // mFloatLogo.setDrawDarkBg(true); 525 | // } 526 | // } 527 | // mFloatLogo.setOnTouchListener(touchListener); 528 | // mHideTimer.start(); 529 | // } 530 | // } 531 | // }); 532 | 533 | mFloatLogo.setOnTouchListener(touchListener);//恢复touch事件 534 | } 535 | 536 | /** 537 | * 悬浮窗touch事件的 down 事件 538 | */ 539 | private void floatEventDown(MotionEvent event) { 540 | isDrag = false; 541 | mHideTimer.cancel(); 542 | if (mFloatLogo.getStatus() != DotImageView.NORMAL) { 543 | mFloatLogo.setStatus(DotImageView.NORMAL); 544 | } 545 | if (!mFloatLogo.mDrawDarkBg) { 546 | mFloatLogo.setDrawDarkBg(true); 547 | } 548 | if (mFloatLogo.getStatus() != DotImageView.NORMAL) { 549 | mFloatLogo.setStatus(DotImageView.NORMAL); 550 | } 551 | mXInView = event.getX(); 552 | mYinView = event.getY(); 553 | mXDownInScreen = event.getRawX(); 554 | mYDownInScreen = event.getRawY(); 555 | mXInScreen = event.getRawX(); 556 | mYInScreen = event.getRawY(); 557 | 558 | 559 | } 560 | 561 | /** 562 | * 悬浮窗touch事件的 move 事件 563 | */ 564 | private void floatEventMove(MotionEvent event) { 565 | mXInScreen = event.getRawX(); 566 | mYInScreen = event.getRawY(); 567 | 568 | 569 | //连续移动的距离超过3则更新一次视图位置 570 | if (Math.abs(mXInScreen - mXDownInScreen) > mFloatLogo.getWidth() / 4 || Math.abs(mYInScreen - mYDownInScreen) > mFloatLogo.getWidth() / 4) { 571 | wmParams.x = (int) (mXInScreen - mXInView); 572 | wmParams.y = (int) (mYInScreen - mYinView) - mFloatLogo.getHeight() / 2; 573 | updateViewPosition(); // 手指移动的时候更新小悬浮窗的位置 574 | double a = mScreenWidth / 2; 575 | float offset = (float) ((a - (Math.abs(wmParams.x - a))) / a); 576 | mFloatLogo.setDrag(isDrag, offset, false); 577 | } else { 578 | isDrag = false; 579 | mFloatLogo.setDrag(false, 0, true); 580 | } 581 | } 582 | 583 | /** 584 | * 悬浮窗touch事件的 up 事件 585 | */ 586 | private void floatEventUp() { 587 | if (mXInScreen < mScreenWidth / 2) { //在左边 588 | mHintLocation = LEFT; 589 | } else { //在右边 590 | mHintLocation = RIGHT; 591 | } 592 | if (valueAnimator == null) { 593 | valueAnimator = ValueAnimator.ofInt(64); 594 | valueAnimator.setInterpolator(mLinearInterpolator); 595 | valueAnimator.setDuration(1000); 596 | valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 597 | @Override 598 | public void onAnimationUpdate(ValueAnimator animation) { 599 | mResetLocationValue = (int) animation.getAnimatedValue(); 600 | mHandler.post(updatePositionRunnable); 601 | } 602 | }); 603 | 604 | valueAnimator.addListener(new Animator.AnimatorListener() { 605 | @Override 606 | public void onAnimationStart(Animator animation) { 607 | 608 | } 609 | 610 | @Override 611 | public void onAnimationEnd(Animator animation) { 612 | if (Math.abs(wmParams.x) < 0) { 613 | wmParams.x = 0; 614 | } else if (Math.abs(wmParams.x) > mScreenWidth) { 615 | wmParams.x = mScreenWidth; 616 | } 617 | updateViewPosition(); 618 | isDrag = false; 619 | mFloatLogo.setDrag(false, 0, true); 620 | mHideTimer.start(); 621 | } 622 | 623 | @Override 624 | public void onAnimationCancel(Animator animation) { 625 | if (Math.abs(wmParams.x) < 0) { 626 | wmParams.x = 0; 627 | } else if (Math.abs(wmParams.x) > mScreenWidth) { 628 | wmParams.x = mScreenWidth; 629 | } 630 | 631 | updateViewPosition(); 632 | isDrag = false; 633 | mFloatLogo.setDrag(false, 0, true); 634 | mHideTimer.start(); 635 | 636 | } 637 | 638 | @Override 639 | public void onAnimationRepeat(Animator animation) { 640 | 641 | } 642 | }); 643 | } 644 | if (!valueAnimator.isRunning()) { 645 | valueAnimator.start(); 646 | } 647 | 648 | // //这里需要判断如果如果手指所在位置和logo所在位置在一个宽度内则不移动, 649 | if (Math.abs(mXInScreen - mXDownInScreen) > mFloatLogo.getWidth() / 5 || Math.abs(mYInScreen - mYDownInScreen) > mFloatLogo.getHeight() / 5) { 650 | isDrag = false; 651 | } else { 652 | openMenu(); 653 | } 654 | 655 | } 656 | 657 | 658 | /** 659 | * 用于检查并更新悬浮球的位置 660 | */ 661 | private void checkPosition() { 662 | if (wmParams.x > 0 && wmParams.x < mScreenWidth) { 663 | if (mHintLocation == LEFT) { 664 | wmParams.x = wmParams.x - mResetLocationValue; 665 | } else { 666 | wmParams.x = wmParams.x + mResetLocationValue; 667 | } 668 | updateViewPosition(); 669 | double a = mScreenWidth / 2; 670 | float offset = (float) ((a - (Math.abs(wmParams.x - a))) / a); 671 | mFloatLogo.setDrag(isDrag, offset, true); 672 | return; 673 | } 674 | 675 | 676 | if (Math.abs(wmParams.x) < 0) { 677 | wmParams.x = 0; 678 | } else if (Math.abs(wmParams.x) > mScreenWidth) { 679 | wmParams.x = mScreenWidth; 680 | } 681 | if (valueAnimator.isRunning()) { 682 | valueAnimator.cancel(); 683 | } 684 | 685 | 686 | updateViewPosition(); 687 | isDrag = false; 688 | 689 | 690 | } 691 | 692 | 693 | /** 694 | * 打开菜单 695 | */ 696 | private void openMenu() { 697 | if (isDrag) return; 698 | 699 | if (!isExpanded) { 700 | mFloatLogo.setDrawDarkBg(false); 701 | try { 702 | wManager.removeViewImmediate(mFloatLogo); 703 | if (mHintLocation == RIGHT) { 704 | wManager.addView(rootViewRight, wmParams); 705 | } else { 706 | wManager.addView(rootView, wmParams); 707 | } 708 | } catch (Exception e) { 709 | e.printStackTrace(); 710 | } 711 | 712 | isExpanded = true; 713 | mHideTimer.cancel(); 714 | } else { 715 | mFloatLogo.setDrawDarkBg(true); 716 | if (isExpanded) { 717 | try { 718 | wManager.removeViewImmediate(mHintLocation == LEFT ? rootView : rootViewRight); 719 | wManager.addView(mFloatLogo, wmParams); 720 | } catch (Exception e) { 721 | e.printStackTrace(); 722 | } 723 | 724 | isExpanded = false; 725 | } 726 | mHideTimer.start(); 727 | } 728 | 729 | } 730 | 731 | 732 | /** 733 | * 更新悬浮窗在屏幕中的位置。 734 | */ 735 | private void updateViewPosition() { 736 | isDrag = true; 737 | try { 738 | if (!isExpanded) { 739 | if (wmParams.y - mFloatLogo.getHeight() / 2 <= 0) { 740 | wmParams.y = mStatusBarHeight; 741 | isDrag = true; 742 | } 743 | wManager.updateViewLayout(mFloatLogo, wmParams); 744 | } 745 | } catch (Exception e) { 746 | e.printStackTrace(); 747 | } 748 | } 749 | 750 | public void show() { 751 | try { 752 | if (wManager != null && wmParams != null && mFloatLogo != null) { 753 | wManager.addView(mFloatLogo, wmParams); 754 | } 755 | if (mHideTimer != null) { 756 | mHideTimer.start(); 757 | } else { 758 | initTimer(); 759 | mHideTimer.start(); 760 | } 761 | } catch (Exception e) { 762 | e.printStackTrace(); 763 | } 764 | } 765 | 766 | /** 767 | * 关闭菜单 768 | */ 769 | public void hide() { 770 | destroyFloat(); 771 | } 772 | 773 | 774 | /** 775 | * 移除所有悬浮窗 释放资源 776 | */ 777 | public void destroyFloat() { 778 | //记录上次的位置logo的停放位置,以备下次恢复 779 | saveSetting(LOCATION_X, mHintLocation); 780 | saveSetting(LOCATION_Y, wmParams.y); 781 | mFloatLogo.clearAnimation(); 782 | try { 783 | mHideTimer.cancel(); 784 | if (isExpanded) { 785 | wManager.removeViewImmediate(mHintLocation == LEFT ? rootView : rootViewRight); 786 | } else { 787 | wManager.removeViewImmediate(mFloatLogo); 788 | } 789 | isExpanded = false; 790 | isDrag = false; 791 | } catch (Exception e) { 792 | e.printStackTrace(); 793 | } 794 | } 795 | 796 | /** 797 | * 计算总红点数 798 | */ 799 | private void calculateDotNum() { 800 | int dotNum = 0; 801 | for (FloatItem floatItem : mFloatItems) { 802 | if (!TextUtils.isEmpty(floatItem.getDotNum())) { 803 | int num = Integer.parseInt(floatItem.getDotNum()); 804 | dotNum = dotNum + num; 805 | } 806 | } 807 | mFloatLogo.setDrawNum(mDrawRedPointNum); 808 | setDotNum(dotNum); 809 | } 810 | 811 | /** 812 | * 绘制悬浮球的红点 813 | * 814 | * @param dotNum d 815 | */ 816 | private void setDotNum(int dotNum) { 817 | mFloatLogo.setDotNum(dotNum, new Animator.AnimatorListener() { 818 | @Override 819 | public void onAnimationStart(Animator animation) { 820 | 821 | } 822 | 823 | @Override 824 | public void onAnimationEnd(Animator animation) { 825 | if (!isDrag) { 826 | mHideTimer.start(); 827 | } 828 | } 829 | 830 | @Override 831 | public void onAnimationCancel(Animator animation) { 832 | 833 | } 834 | 835 | @Override 836 | public void onAnimationRepeat(Animator animation) { 837 | 838 | } 839 | }); 840 | } 841 | 842 | /** 843 | * 用于暴露给外部判断是否包含某个菜单项 844 | * 845 | * @param menuLabel string 846 | * @return boolean 847 | */ 848 | public boolean hasMenu(String menuLabel) { 849 | for (FloatItem menuItem : mFloatItems) { 850 | if (TextUtils.equals(menuItem.getTitle(), menuLabel)) { 851 | return true; 852 | } 853 | } 854 | return false; 855 | } 856 | 857 | /** 858 | * 用于保存悬浮球的位置记录 859 | * 860 | * @param key String 861 | * @param defaultValue int 862 | * @return int 863 | */ 864 | private int getSetting(String key, int defaultValue) { 865 | try { 866 | SharedPreferences sharedata = mActivity.getSharedPreferences("floatLogo", 0); 867 | return sharedata.getInt(key, defaultValue); 868 | } catch (Exception e) { 869 | e.printStackTrace(); 870 | } 871 | return defaultValue; 872 | } 873 | 874 | /** 875 | * 用于保存悬浮球的位置记录 876 | * 877 | * @param key String 878 | * @param value int 879 | */ 880 | public void saveSetting(String key, int value) { 881 | try { 882 | SharedPreferences.Editor sharedata = mActivity.getSharedPreferences("floatLogo", 0).edit(); 883 | sharedata.putInt(key, value); 884 | sharedata.apply(); 885 | } catch (Exception e) { 886 | e.printStackTrace(); 887 | } 888 | } 889 | 890 | public static int dp2Px(float dp, Context mContext) { 891 | return (int) TypedValue.applyDimension( 892 | TypedValue.COMPLEX_UNIT_DIP, 893 | dp, 894 | mContext.getResources().getDisplayMetrics()); 895 | } 896 | 897 | 898 | public interface OnMenuClickListener { 899 | void onMenuExpended(boolean isExpened); 900 | } 901 | 902 | 903 | public void setValueAnimator() { 904 | 905 | } 906 | 907 | public static final class Builder { 908 | private int mBackMenuColor; 909 | private boolean mDrawRedPointNum; 910 | private boolean mCircleMenuBg; 911 | private Bitmap mLogoRes; 912 | private int mDefaultLocation; 913 | private List mFloatItems = new ArrayList<>(); 914 | private Context mActivity; 915 | private FloatMenuView.OnMenuClickListener mOnMenuClickListener; 916 | private Drawable mDrawable; 917 | 918 | 919 | public Builder setBgDrawable(Drawable drawable) { 920 | mDrawable = drawable; 921 | return this; 922 | } 923 | 924 | public Builder() { 925 | } 926 | 927 | public Builder setFloatItems(List mFloatItems) { 928 | this.mFloatItems = mFloatItems; 929 | return this; 930 | } 931 | 932 | public Builder addFloatItem(FloatItem floatItem) { 933 | this.mFloatItems.add(floatItem); 934 | return this; 935 | } 936 | 937 | public Builder backMenuColor(int val) { 938 | mBackMenuColor = val; 939 | return this; 940 | } 941 | 942 | public Builder drawRedPointNum(boolean val) { 943 | mDrawRedPointNum = val; 944 | return this; 945 | } 946 | 947 | public Builder drawCicleMenuBg(boolean val) { 948 | mCircleMenuBg = val; 949 | return this; 950 | } 951 | 952 | public Builder logo(Bitmap val) { 953 | mLogoRes = val; 954 | return this; 955 | } 956 | 957 | public Builder withActivity(Activity val) { 958 | mActivity = val; 959 | return this; 960 | } 961 | 962 | public Builder withContext(Context val) { 963 | mActivity = val; 964 | return this; 965 | } 966 | 967 | public Builder setOnMenuItemClickListener(FloatMenuView.OnMenuClickListener val) { 968 | mOnMenuClickListener = val; 969 | return this; 970 | } 971 | 972 | public Builder defaultLocation(int val) { 973 | mDefaultLocation = val; 974 | return this; 975 | } 976 | 977 | public FloatLogoMenu showWithListener(FloatMenuView.OnMenuClickListener val) { 978 | mOnMenuClickListener = val; 979 | return new FloatLogoMenu(this); 980 | } 981 | 982 | public FloatLogoMenu showWithLogo(Bitmap val) { 983 | mLogoRes = val; 984 | return new FloatLogoMenu(this); 985 | } 986 | 987 | public FloatLogoMenu show() { 988 | return new FloatLogoMenu(this); 989 | } 990 | } 991 | 992 | 993 | } 994 | -------------------------------------------------------------------------------- /FloatMenu/src/main/java/com/yw/game/floatmenu/FloatMenuView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, Shanghai YUEWEN Information Technology Co., Ltd. 3 | * All rights reserved. 4 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | * 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | * Neither the name of Shanghai YUEWEN Information Technology Co., Ltd. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | * 10 | * THIS SOFTWARE IS PROVIDED BY SHANGHAI YUEWEN INFORMATION TECHNOLOGY CO., LTD. AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | * 12 | */ 13 | package com.yw.game.floatmenu; 14 | 15 | import android.animation.Animator; 16 | import android.animation.ObjectAnimator; 17 | import android.content.Context; 18 | import android.graphics.Canvas; 19 | import android.graphics.Color; 20 | import android.graphics.Paint; 21 | import android.graphics.PointF; 22 | import android.graphics.Rect; 23 | import android.graphics.RectF; 24 | import android.text.TextUtils; 25 | import android.util.AttributeSet; 26 | import android.view.MotionEvent; 27 | import android.view.View; 28 | import android.view.ViewGroup; 29 | 30 | 31 | import java.util.ArrayList; 32 | import java.util.List; 33 | 34 | /** 35 | * Created by wengyiming on 2017/7/21. 36 | */ 37 | 38 | public class FloatMenuView extends View { 39 | public static final int STATUS_LEFT = 3;//展开左边菜单 40 | public static final int STATUS_RIGHT = 4;//展开右边菜单 41 | 42 | private int mStatus = STATUS_RIGHT;//默认右边 43 | 44 | private Paint mPaint;//画笔 45 | private int mBackgroundColor = 0x00FFFFFF;//默认背景颜色 完全透明的白色 46 | 47 | private int mMenuBackgroundColor = -1;//菜单的背景颜色 48 | 49 | private RectF mBgRect;//菜单的背景矩阵 50 | private int mItemWidth = dip2px(50);//菜单项的宽度 51 | private int mItemHeight = dip2px(50);//菜单项的高度 52 | private int mItemLeft = 0;//菜单项左边的默认偏移值,这里是0 53 | private int mCorner = dip2px(2);//菜单背景的圆角多出的宽度 54 | 55 | 56 | private int mRadius = dip2px(4);//红点消息半径 57 | private final int mRedPointRadiuWithNoNum = dip2px(3);//红点圆半径 58 | 59 | private int mFontSizePointNum = sp2px(10);//红点消息数字的文字大小 60 | 61 | private int mFontSizeTitle = sp2px(12);//菜单项的title的文字大小 62 | private float mFirstItemTop;//菜单项的最小y值,上面起始那条线 63 | private boolean mDrawNum = false;//是否绘制数字,false则只绘制红点 64 | private boolean circleBg = false;//菜单项背景是否绘制成圆形,false则绘制矩阵 65 | 66 | private List mItemList = new ArrayList<>();//菜单项的内容 67 | private List mItemRectList = new ArrayList<>();//菜单项所占用位置的记录,用于判断点击事件 68 | 69 | private OnMenuClickListener mOnMenuClickListener;//菜单项的点击事件回调 70 | 71 | private ObjectAnimator mAlphaAnim;//消失关闭动画的透明值 72 | 73 | //设置菜单内容集合 74 | public void setItemList(List itemList) { 75 | mItemList = itemList; 76 | } 77 | 78 | //设置是否绘制红点数字 79 | public void drawNum(boolean drawNum) { 80 | mDrawNum = drawNum; 81 | } 82 | 83 | //设置是否绘制圆形菜单,否则矩阵 84 | public void setCircleBg(boolean circleBg) { 85 | this.circleBg = circleBg; 86 | } 87 | 88 | //用于标记所依赖的view的screen的坐标,实际view的坐标是window坐标,所以这里后面会减去状态栏的高度 89 | 90 | 91 | //设置菜单的背景颜色 92 | public void setMenuBackgroundColor(int mMenuBackgroundColor) { 93 | this.mMenuBackgroundColor = mMenuBackgroundColor; 94 | } 95 | 96 | //设置这个view(整个屏幕)的背景,这里默认透明 97 | public void setBackgroundColor(int BackgroundColor) { 98 | this.mBackgroundColor = BackgroundColor; 99 | } 100 | 101 | 102 | //下面开始的注释我写不动了,看不懂的话请自行领悟吧 103 | public FloatMenuView(Context context) { 104 | super(context); 105 | } 106 | 107 | public FloatMenuView(Context context, AttributeSet attrs) { 108 | super(context, attrs); 109 | } 110 | 111 | public FloatMenuView(Context context, AttributeSet attrs, int defStyleAttr) { 112 | super(context, attrs, defStyleAttr); 113 | } 114 | 115 | public FloatMenuView(Context baseContext, int status) { 116 | super(baseContext); 117 | mStatus = status; 118 | int screenWidth = getResources().getDisplayMetrics().widthPixels; 119 | int screenHeight = getResources().getDisplayMetrics().heightPixels; 120 | mBgRect = new RectF(0, 0, screenWidth, screenHeight); 121 | initView(); 122 | } 123 | 124 | @Override 125 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 126 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 127 | setMeasuredDimension(mItemWidth * mItemList.size(), mItemHeight); 128 | } 129 | 130 | private void initView( ) { 131 | mPaint = new Paint(); 132 | mPaint.setAntiAlias(true); 133 | mPaint.setStyle(Paint.Style.FILL); 134 | mPaint.setTextSize(sp2px(12)); 135 | 136 | mAlphaAnim = ObjectAnimator.ofFloat(this, "alpha", 1.0f, 0f); 137 | mAlphaAnim.setDuration(50); 138 | mAlphaAnim.addListener(new MyAnimListener() { 139 | @Override 140 | public void onAnimationEnd(Animator animation) { 141 | if (mOnMenuClickListener != null) { 142 | removeView(); 143 | mOnMenuClickListener.dismiss(); 144 | } 145 | } 146 | }); 147 | 148 | mFirstItemTop = 0; 149 | if (mStatus == STATUS_LEFT) { 150 | mItemLeft = 0; 151 | } else { 152 | mItemLeft = 0; 153 | } 154 | 155 | } 156 | 157 | @Override 158 | protected void onDraw(Canvas canvas) { 159 | super.onDraw(canvas); 160 | switch (mStatus) { 161 | case STATUS_LEFT: 162 | drawBackground(canvas); 163 | drawFloatLeftItem(canvas); 164 | break; 165 | case STATUS_RIGHT: 166 | drawBackground(canvas); 167 | drawFloatLeftItem(canvas); 168 | break; 169 | } 170 | } 171 | 172 | private void drawBackground(Canvas canvas) { 173 | mPaint.setColor(mBackgroundColor); 174 | canvas.drawRect(mBgRect, mPaint); 175 | 176 | } 177 | 178 | private void drawFloatLeftItem(Canvas canvas) { 179 | mItemRectList.clear(); 180 | for (int i = 0; i < mItemList.size(); i++) { 181 | canvas.save(); 182 | mPaint.setColor(mMenuBackgroundColor); 183 | if (circleBg) { 184 | float cx = (mItemLeft + i * mItemWidth) + mItemWidth / 2;//x中心点 185 | float cy = mFirstItemTop + mItemHeight / 2;//y中心点 186 | float radius = mItemWidth / 2;//半径 187 | canvas.drawCircle(cx, cy, radius, mPaint); 188 | } else { 189 | mPaint.setColor(mItemList.get(i).bgColor); 190 | canvas.drawRect(mItemLeft + i * mItemWidth, mFirstItemTop, mItemLeft + mItemWidth + i * mItemWidth, mFirstItemTop + mItemHeight, mPaint); 191 | } 192 | 193 | mItemRectList.add(new RectF(mItemLeft + i * mItemWidth, mFirstItemTop, mItemLeft + mItemWidth + i * mItemWidth, mFirstItemTop + mItemHeight)); 194 | mPaint.setColor(mItemList.get(i).bgColor); 195 | drawIconTitleDot(canvas, i); 196 | } 197 | canvas.restore(); 198 | } 199 | 200 | 201 | private void drawIconTitleDot(Canvas canvas, int position) { 202 | FloatItem floatItem = mItemList.get(position); 203 | 204 | if (floatItem.icon != null) { 205 | float centerX = mItemLeft + mItemWidth / 2 + (mItemWidth) * position;//计算每一个item的中心点x的坐标值 206 | float centerY = mFirstItemTop + mItemHeight / 2;//计算每一个item的中心点的y坐标值 207 | 208 | float left = centerX - mItemWidth / 4;//计算icon的左坐标值 中心点往左移宽度的四分之一 209 | float right = centerX + mItemWidth / 4; 210 | 211 | float iconH = mItemHeight * 0.5f;//计算出icon的宽度 = icon的高度 212 | 213 | float textH = getTextHeight(floatItem.getTitle(), mPaint); 214 | float paddingH = (mItemHeight - iconH - textH - mRadius) / 2;//总高度减去文字的高度,减去icon高度,再除以2就是上下的间距剩余 215 | 216 | float top = centerY - mItemHeight / 2 + paddingH;//计算icon的上坐标值 217 | float bottom = top + iconH;//剩下的高度空间用于画文字 218 | 219 | //画icon 220 | mPaint.setColor(floatItem.titleColor); 221 | canvas.drawBitmap(floatItem.icon, null, new RectF(left, top, right, bottom), mPaint); 222 | if (!TextUtils.isEmpty(floatItem.dotNum) && !floatItem.dotNum.equals("0")) { 223 | float dotLeft = centerX + mItemWidth / 5; 224 | float cx = dotLeft + mCorner;//x中心点 225 | float cy = top + mCorner;//y中心点 226 | 227 | int radius = mDrawNum ? mRadius : mRedPointRadiuWithNoNum; 228 | //画红点 229 | mPaint.setColor(Color.RED); 230 | canvas.drawCircle(cx, cy, radius, mPaint); 231 | if (mDrawNum) { 232 | mPaint.setColor(Color.WHITE); 233 | mPaint.setTextSize(mFontSizePointNum); 234 | //画红点消息数 235 | canvas.drawText(floatItem.dotNum, cx - getTextWidth(floatItem.getDotNum(), mPaint) / 2, cy + getTextHeight(floatItem.getDotNum(), mPaint) / 2, mPaint); 236 | } 237 | } 238 | mPaint.setColor(floatItem.titleColor); 239 | mPaint.setTextSize(mFontSizeTitle); 240 | //画menu title 241 | canvas.drawText(floatItem.title, centerX - getTextWidth(floatItem.getTitle(), mPaint) / 2, centerY + iconH / 2 + getTextHeight(floatItem.getTitle(), mPaint) / 2, mPaint); 242 | } 243 | } 244 | 245 | 246 | public void startAnim() { 247 | if (mItemList.size() == 0) { 248 | return; 249 | } 250 | invalidate(); 251 | } 252 | 253 | 254 | public void dismiss() { 255 | if (!mAlphaAnim.isRunning()) { 256 | mAlphaAnim.start(); 257 | } 258 | } 259 | 260 | private void removeView() { 261 | ViewGroup vg = (ViewGroup) this.getParent(); 262 | if (vg != null) { 263 | vg.removeView(this); 264 | } 265 | } 266 | 267 | @Override 268 | protected void onWindowVisibilityChanged(int visibility) { 269 | if (visibility == GONE) { 270 | if (mOnMenuClickListener != null) { 271 | mOnMenuClickListener.dismiss(); 272 | } 273 | } 274 | super.onWindowVisibilityChanged(visibility); 275 | 276 | 277 | } 278 | 279 | public void setOnMenuClickListener(OnMenuClickListener onMenuClickListener) { 280 | this.mOnMenuClickListener = onMenuClickListener; 281 | } 282 | 283 | public interface OnMenuClickListener { 284 | void onItemClick(int position, String title); 285 | 286 | void dismiss(); 287 | 288 | } 289 | 290 | private abstract class MyAnimListener implements Animator.AnimatorListener { 291 | @Override 292 | public void onAnimationStart(Animator animation) { 293 | 294 | } 295 | 296 | @Override 297 | public void onAnimationCancel(Animator animation) { 298 | 299 | } 300 | 301 | @Override 302 | public void onAnimationRepeat(Animator animation) { 303 | 304 | } 305 | } 306 | 307 | 308 | @Override 309 | public boolean onTouchEvent(MotionEvent event) { 310 | switch (event.getAction()) { 311 | case MotionEvent.ACTION_DOWN: 312 | for (int i = 0; i < mItemRectList.size(); i++) { 313 | if (mOnMenuClickListener != null && isPointInRect(new PointF(event.getX(), event.getY()), mItemRectList.get(i))) { 314 | mOnMenuClickListener.onItemClick(i, mItemList.get(i).title); 315 | return true; 316 | } 317 | } 318 | dismiss(); 319 | } 320 | return false; 321 | } 322 | 323 | private boolean isPointInRect(PointF pointF, RectF targetRect) { 324 | return pointF.x >= targetRect.left && pointF.x <= targetRect.right && pointF.y >= targetRect.top && pointF.y <= targetRect.bottom; 325 | } 326 | 327 | 328 | public static class Builder { 329 | 330 | private Context mActivity; 331 | private List mFloatItems = new ArrayList<>(); 332 | private int mBgColor = Color.TRANSPARENT; 333 | private int mStatus = STATUS_LEFT; 334 | private boolean cicleBg = false; 335 | private int mMenuBackgroundColor = -1; 336 | private boolean mDrawNum = false; 337 | 338 | 339 | public Builder drawNum(boolean drawNum) { 340 | mDrawNum = drawNum; 341 | return this; 342 | } 343 | 344 | 345 | public Builder setMenuBackgroundColor(int mMenuBackgroundColor) { 346 | this.mMenuBackgroundColor = mMenuBackgroundColor; 347 | return this; 348 | } 349 | 350 | 351 | public Builder setCicleBg(boolean cicleBg) { 352 | this.cicleBg = cicleBg; 353 | return this; 354 | } 355 | 356 | public Builder setStatus(int status) { 357 | mStatus = status; 358 | return this; 359 | } 360 | 361 | public Builder setFloatItems(List floatItems) { 362 | this.mFloatItems = floatItems; 363 | return this; 364 | } 365 | 366 | 367 | public Builder(Context activity ) { 368 | mActivity = activity; 369 | } 370 | 371 | public Builder addItem(FloatItem floatItem) { 372 | mFloatItems.add(floatItem); 373 | return this; 374 | } 375 | 376 | public Builder addItems(List list) { 377 | mFloatItems.addAll(list); 378 | return this; 379 | } 380 | 381 | public Builder setBackgroundColor(int color) { 382 | mBgColor = color; 383 | return this; 384 | } 385 | 386 | public FloatMenuView create() { 387 | FloatMenuView floatMenuView = new FloatMenuView(mActivity, mStatus); 388 | floatMenuView.setItemList(mFloatItems); 389 | floatMenuView.setBackgroundColor(mBgColor); 390 | floatMenuView.setCircleBg(cicleBg); 391 | floatMenuView.startAnim(); 392 | floatMenuView.drawNum(mDrawNum); 393 | floatMenuView.setMenuBackgroundColor(mMenuBackgroundColor); 394 | return floatMenuView; 395 | } 396 | 397 | } 398 | 399 | 400 | private int dip2px(float dipValue) { 401 | final float scale = getContext().getResources().getDisplayMetrics().density; 402 | return (int) (dipValue * scale + 0.5f); 403 | } 404 | 405 | private int sp2px(float spValue) { 406 | final float fontScale = getContext().getResources().getDisplayMetrics().scaledDensity; 407 | return (int) (spValue * fontScale + 0.5f); 408 | } 409 | 410 | private float getTextHeight(String text, Paint paint) { 411 | Rect rect = new Rect(); 412 | paint.getTextBounds(text, 0, text.length(), rect); 413 | return rect.height() / 1.1f; 414 | } 415 | 416 | private float getTextWidth(String text, Paint paint) { 417 | return paint.measureText(text); 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /FloatMenu/src/main/java/com/yw/game/floatmenu/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, Shanghai YUEWEN Information Technology Co., Ltd. 3 | * All rights reserved. 4 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | * 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | * Neither the name of Shanghai YUEWEN Information Technology Co., Ltd. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | * 10 | * THIS SOFTWARE IS PROVIDED BY SHANGHAI YUEWEN INFORMATION TECHNOLOGY CO., LTD. AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | * 12 | */ 13 | 14 | package com.yw.game.floatmenu; 15 | 16 | 17 | import android.content.Context; 18 | import android.util.TypedValue; 19 | import android.view.ViewGroup; 20 | import android.view.WindowManager; 21 | import android.view.animation.AccelerateInterpolator; 22 | import android.view.animation.AlphaAnimation; 23 | import android.view.animation.Animation; 24 | import android.view.animation.AnimationSet; 25 | import android.view.animation.LinearInterpolator; 26 | import android.view.animation.ScaleAnimation; 27 | import android.widget.FrameLayout; 28 | 29 | import java.lang.reflect.Field; 30 | 31 | public class Utils { 32 | 33 | private static int statusBarHeight; 34 | 35 | /** 36 | * 用于获取状态栏的高度。 37 | * 38 | * @return 返回状态栏高度的像素值。 39 | */ 40 | public static int getStatusBarHeight(Context context) { 41 | if (statusBarHeight == 0) { 42 | try { 43 | Class c = Class.forName("com.android.internal.R$dimen"); 44 | Object o = c.newInstance(); 45 | Field field = c.getField("status_bar_height"); 46 | int x = (Integer) field.get(o); 47 | statusBarHeight = context.getResources().getDimensionPixelSize(x); 48 | } catch (Exception e) { 49 | e.printStackTrace(); 50 | } 51 | } 52 | return statusBarHeight; 53 | } 54 | 55 | 56 | 57 | 58 | 59 | } 60 | -------------------------------------------------------------------------------- /FloatMenu/src/main/java/com/yw/game/floatmenu/customfloat/BaseFloatDialog.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, Shanghai YUEWEN Information Technology Co., Ltd. 3 | * All rights reserved. 4 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | * 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | * Neither the name of Shanghai YUEWEN Information Technology Co., Ltd. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | * 10 | * THIS SOFTWARE IS PROVIDED BY SHANGHAI YUEWEN INFORMATION TECHNOLOGY CO., LTD. AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | * 12 | */ 13 | package com.yw.game.floatmenu.customfloat; 14 | 15 | import android.animation.Animator; 16 | import android.animation.ValueAnimator; 17 | import android.app.Activity; 18 | import android.content.Context; 19 | import android.content.SharedPreferences; 20 | import android.graphics.PixelFormat; 21 | import android.os.Build; 22 | import android.os.CountDownTimer; 23 | import android.os.Handler; 24 | import android.os.Looper; 25 | import android.util.TypedValue; 26 | import android.view.Gravity; 27 | import android.view.LayoutInflater; 28 | import android.view.MotionEvent; 29 | import android.view.View; 30 | import android.view.WindowManager; 31 | import android.view.animation.Interpolator; 32 | import android.view.animation.LinearInterpolator; 33 | 34 | 35 | /** 36 | * Created by wengyiming on 2017/8/25. 37 | */ 38 | 39 | public abstract class BaseFloatDialog { 40 | 41 | /** 42 | * 悬浮球 坐落 左 右 标记 43 | */ 44 | public static final int LEFT = 0; 45 | public static final int RIGHT = 1; 46 | 47 | /** 48 | * 记录 logo 停放的位置,以备下次恢复 49 | */ 50 | private static final String LOCATION_X = "hintLocation"; 51 | private static final String LOCATION_Y = "locationY"; 52 | 53 | 54 | /** 55 | * 停靠默认位置 56 | */ 57 | private int mDefaultLocation = RIGHT; 58 | 59 | 60 | /** 61 | * 悬浮窗 坐落 位置 62 | */ 63 | private int mHintLocation = mDefaultLocation; 64 | 65 | 66 | /** 67 | * 记录当前手指位置在屏幕上的横坐标值 68 | */ 69 | private float mXInScreen; 70 | 71 | /** 72 | * 记录当前手指位置在屏幕上的纵坐标值 73 | */ 74 | private float mYInScreen; 75 | 76 | /** 77 | * 记录手指按下时在屏幕上的横坐标的值 78 | */ 79 | private float mXDownInScreen; 80 | 81 | /** 82 | * 记录手指按下时在屏幕上的纵坐标的值 83 | */ 84 | private float mYDownInScreen; 85 | 86 | /** 87 | * 记录手指按下时在小悬浮窗的View上的横坐标的值 88 | */ 89 | private float mXInView; 90 | 91 | /** 92 | * 记录手指按下时在小悬浮窗的View上的纵坐标的值 93 | */ 94 | private float mYinView; 95 | 96 | /** 97 | * 记录屏幕的宽度 98 | */ 99 | private int mScreenWidth; 100 | 101 | /** 102 | * 来自 activity 的 wManager 103 | */ 104 | private WindowManager wManager; 105 | 106 | 107 | /** 108 | * 为 wManager 设置 LayoutParams 109 | */ 110 | private WindowManager.LayoutParams wmParams; 111 | 112 | 113 | /** 114 | * 用于显示在 mActivity 上的 mActivity 115 | */ 116 | private Context mActivity; 117 | 118 | 119 | /** 120 | * 用于 定时 隐藏 logo的定时器 121 | */ 122 | private CountDownTimer mHideTimer; 123 | 124 | 125 | /** 126 | * float menu的高度 127 | */ 128 | private Handler mHandler = new Handler(Looper.getMainLooper()); 129 | 130 | 131 | /** 132 | * 悬浮窗左右移动到默认位置 动画的 加速器 133 | */ 134 | private Interpolator mLinearInterpolator = new LinearInterpolator(); 135 | 136 | /** 137 | * 标记是否拖动中 138 | */ 139 | private boolean isDrag = false; 140 | 141 | /** 142 | * 用于恢复悬浮球的location的属性动画值 143 | */ 144 | private int mResetLocationValue; 145 | 146 | public boolean isApplicationDialog() { 147 | return isApplicationDialog; 148 | } 149 | 150 | private boolean isApplicationDialog = false; 151 | 152 | /** 153 | * 这个事件用于处理移动、自定义点击或者其它事情,return true可以保证onclick事件失效 154 | */ 155 | private View.OnTouchListener touchListener = new View.OnTouchListener() { 156 | @Override 157 | public boolean onTouch(View v, MotionEvent event) { 158 | switch (event.getAction()) { 159 | case MotionEvent.ACTION_DOWN: 160 | floatEventDown(event); 161 | break; 162 | case MotionEvent.ACTION_MOVE: 163 | floatEventMove(event); 164 | break; 165 | case MotionEvent.ACTION_UP: 166 | case MotionEvent.ACTION_CANCEL: 167 | floatEventUp(); 168 | break; 169 | } 170 | return true; 171 | } 172 | }; 173 | 174 | ValueAnimator valueAnimator = null; 175 | private boolean isExpanded = false; 176 | 177 | private View logoView; 178 | private View rightView; 179 | private View leftView; 180 | 181 | private GetViewCallback mGetViewCallback; 182 | 183 | 184 | public Context getContext() { 185 | return mActivity; 186 | } 187 | 188 | public static class FloatDialogImp extends BaseFloatDialog { 189 | 190 | 191 | public FloatDialogImp(Context context, GetViewCallback getViewCallback) { 192 | super(context, getViewCallback); 193 | } 194 | 195 | public FloatDialogImp(Context context) { 196 | super(context); 197 | } 198 | 199 | @Override 200 | protected View getLeftView(LayoutInflater inflater, View.OnTouchListener touchListener) { 201 | return null; 202 | } 203 | 204 | @Override 205 | protected View getRightView(LayoutInflater inflater, View.OnTouchListener touchListener) { 206 | return null; 207 | } 208 | 209 | @Override 210 | protected View getLogoView(LayoutInflater inflater) { 211 | return null; 212 | } 213 | 214 | @Override 215 | protected void resetLogoViewSize(int hintLocation, View logoView) { 216 | 217 | } 218 | 219 | @Override 220 | protected void dragLogoViewOffset(View logoView, boolean isDraging, boolean isResetPosition, float offset) { 221 | 222 | } 223 | 224 | @Override 225 | protected void shrinkLeftLogoView(View logoView) { 226 | 227 | } 228 | 229 | @Override 230 | protected void shrinkRightLogoView(View logoView) { 231 | 232 | } 233 | 234 | @Override 235 | protected void leftViewOpened(View leftView) { 236 | 237 | } 238 | 239 | @Override 240 | protected void rightViewOpened(View rightView) { 241 | 242 | } 243 | 244 | @Override 245 | protected void leftOrRightViewClosed(View logoView) { 246 | 247 | } 248 | 249 | @Override 250 | protected void onDestroyed() { 251 | 252 | } 253 | } 254 | 255 | protected BaseFloatDialog(Context context, GetViewCallback getViewCallback) { 256 | this(context); 257 | this.mGetViewCallback = getViewCallback; 258 | if (mGetViewCallback == null) { 259 | throw new IllegalArgumentException("GetViewCallback cound not be null!"); 260 | } 261 | 262 | } 263 | 264 | protected BaseFloatDialog(Context context) { 265 | this.mActivity = context; 266 | initFloatWindow(); 267 | initTimer(); 268 | initFloatView(); 269 | 270 | } 271 | 272 | 273 | private void initFloatView() { 274 | LayoutInflater inflater = LayoutInflater.from(mActivity); 275 | logoView = mGetViewCallback == null ? getLogoView(inflater) : mGetViewCallback.getLogoView(inflater); 276 | leftView = mGetViewCallback == null ? getLeftView(inflater, touchListener) : mGetViewCallback.getLeftView(inflater, touchListener); 277 | rightView = mGetViewCallback == null ? getRightView(inflater, touchListener) : mGetViewCallback.getRightView(inflater, touchListener); 278 | 279 | if (logoView == null) { 280 | throw new IllegalArgumentException("Must impl GetViewCallback or impl " + this.getClass().getSimpleName() + "and make getLogoView() not return null !"); 281 | } 282 | 283 | logoView.setOnTouchListener(touchListener);//恢复touch事件 284 | 285 | } 286 | 287 | /** 288 | * 初始化 隐藏悬浮球的定时器 289 | */ 290 | private void initTimer() { 291 | mHideTimer = new CountDownTimer(2000, 10) { //悬浮窗超过5秒没有操作的话会自动隐藏 292 | @Override 293 | public void onTick(long millisUntilFinished) { 294 | if (isExpanded) { 295 | mHideTimer.cancel(); 296 | } 297 | } 298 | 299 | @Override 300 | public void onFinish() { 301 | if (isExpanded) { 302 | mHideTimer.cancel(); 303 | return; 304 | } 305 | if (!isDrag) { 306 | if (mHintLocation == LEFT) { 307 | if (mGetViewCallback == null) { 308 | shrinkLeftLogoView(logoView); 309 | } else { 310 | mGetViewCallback.shrinkLeftLogoView(logoView); 311 | } 312 | } else { 313 | if (mGetViewCallback == null) { 314 | shrinkRightLogoView(logoView); 315 | } else { 316 | mGetViewCallback.shrinkRightLogoView(logoView); 317 | } 318 | } 319 | } 320 | } 321 | }; 322 | } 323 | 324 | 325 | /** 326 | * 初始化悬浮球 window 327 | */ 328 | private void initFloatWindow() { 329 | wmParams = new WindowManager.LayoutParams(); 330 | if (mActivity instanceof Activity) { 331 | Activity activity = (Activity) mActivity; 332 | wManager = activity.getWindowManager(); 333 | //类似dialog,寄托在activity的windows上,activity关闭时需要关闭当前float 334 | wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION; 335 | isApplicationDialog = true; 336 | } else { 337 | wManager = (WindowManager) mActivity.getSystemService(Context.WINDOW_SERVICE); 338 | //判断状态栏是否显示 如果不显示则statusBarHeight为0 339 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 340 | //在android7.1以上系统需要使用TYPE_PHONE类型 配合运行时权限 341 | wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 342 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 343 | //在android7.1以上系统需要使用TYPE_PHONE类型 配合运行时权限 344 | wmParams.type = WindowManager.LayoutParams.TYPE_PHONE; 345 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 346 | wmParams.type = WindowManager.LayoutParams.TYPE_TOAST; 347 | } else { 348 | wmParams.type = WindowManager.LayoutParams.TYPE_PHONE; 349 | } 350 | isApplicationDialog = false; 351 | } 352 | mScreenWidth = wManager.getDefaultDisplay().getWidth(); 353 | int screenHeight = wManager.getDefaultDisplay().getHeight(); 354 | wmParams.format = PixelFormat.RGBA_8888; 355 | wmParams.gravity = Gravity.LEFT | Gravity.TOP; 356 | wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; 357 | mHintLocation = getSetting(LOCATION_X, mDefaultLocation); 358 | int defaultY = ((screenHeight) / 2) / 3; 359 | int y = getSetting(LOCATION_Y, defaultY); 360 | if (mHintLocation == LEFT) { 361 | wmParams.x = 0; 362 | } else { 363 | wmParams.x = mScreenWidth; 364 | } 365 | if (y != 0 && y != defaultY) { 366 | wmParams.y = y; 367 | } else { 368 | wmParams.y = defaultY; 369 | } 370 | wmParams.alpha = 1; 371 | wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT; 372 | wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT; 373 | } 374 | 375 | /** 376 | * 悬浮窗touch事件的 down 事件 377 | */ 378 | private void floatEventDown(MotionEvent event) { 379 | isDrag = false; 380 | mHideTimer.cancel(); 381 | 382 | if (mGetViewCallback == null) { 383 | resetLogoViewSize(mHintLocation, logoView); 384 | } else { 385 | mGetViewCallback.resetLogoViewSize(mHintLocation, logoView); 386 | } 387 | 388 | mXInView = event.getX(); 389 | mYinView = event.getY(); 390 | mXDownInScreen = event.getRawX(); 391 | mYDownInScreen = event.getRawY(); 392 | mXInScreen = event.getRawX(); 393 | mYInScreen = event.getRawY(); 394 | 395 | 396 | } 397 | 398 | 399 | /** 400 | * 悬浮窗touch事件的 move 事件 401 | */ 402 | private void floatEventMove(MotionEvent event) { 403 | mXInScreen = event.getRawX(); 404 | mYInScreen = event.getRawY(); 405 | 406 | 407 | //连续移动的距离超过3则更新一次视图位置 408 | if (Math.abs(mXInScreen - mXDownInScreen) > logoView.getWidth() / 4 || Math.abs(mYInScreen - mYDownInScreen) > logoView.getWidth() / 4) { 409 | wmParams.x = (int) (mXInScreen - mXInView); 410 | wmParams.y = (int) (mYInScreen - mYinView) - logoView.getHeight() / 2; 411 | updateViewPosition(); // 手指移动的时候更新小悬浮窗的位置 412 | double a = mScreenWidth / 2; 413 | float offset = (float) ((a - (Math.abs(wmParams.x - a))) / a); 414 | if (mGetViewCallback == null) { 415 | dragLogoViewOffset(logoView, isDrag, false, offset); 416 | } else { 417 | mGetViewCallback.dragingLogoViewOffset(logoView, isDrag, false, offset); 418 | } 419 | 420 | } else { 421 | isDrag = false; 422 | // logoView.setDrag(false, 0, true); 423 | if (mGetViewCallback == null) { 424 | dragLogoViewOffset(logoView, false, true, 0); 425 | } else { 426 | mGetViewCallback.dragingLogoViewOffset(logoView, false, true, 0); 427 | } 428 | 429 | } 430 | } 431 | 432 | /** 433 | * 悬浮窗touch事件的 up 事件 434 | */ 435 | private void floatEventUp() { 436 | if (mXInScreen < mScreenWidth / 2) { //在左边 437 | mHintLocation = LEFT; 438 | } else { //在右边 439 | mHintLocation = RIGHT; 440 | } 441 | 442 | 443 | valueAnimator = ValueAnimator.ofInt(64); 444 | valueAnimator.setInterpolator(mLinearInterpolator); 445 | valueAnimator.setDuration(1000); 446 | valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 447 | @Override 448 | public void onAnimationUpdate(ValueAnimator animation) { 449 | mResetLocationValue = (int) animation.getAnimatedValue(); 450 | mHandler.post(updatePositionRunnable); 451 | } 452 | }); 453 | 454 | valueAnimator.addListener(new Animator.AnimatorListener() { 455 | @Override 456 | public void onAnimationStart(Animator animation) { 457 | 458 | } 459 | 460 | @Override 461 | public void onAnimationEnd(Animator animation) { 462 | if (Math.abs(wmParams.x) < 0) { 463 | wmParams.x = 0; 464 | } else if (Math.abs(wmParams.x) > mScreenWidth) { 465 | wmParams.x = mScreenWidth; 466 | } 467 | updateViewPosition(); 468 | isDrag = false; 469 | if (mGetViewCallback == null) { 470 | dragLogoViewOffset(logoView, false, true, 0); 471 | } else { 472 | mGetViewCallback.dragingLogoViewOffset(logoView, false, true, 0); 473 | } 474 | mHideTimer.start(); 475 | } 476 | 477 | @Override 478 | public void onAnimationCancel(Animator animation) { 479 | if (Math.abs(wmParams.x) < 0) { 480 | wmParams.x = 0; 481 | } else if (Math.abs(wmParams.x) > mScreenWidth) { 482 | wmParams.x = mScreenWidth; 483 | } 484 | 485 | updateViewPosition(); 486 | isDrag = false; 487 | if (mGetViewCallback == null) { 488 | dragLogoViewOffset(logoView, false, true, 0); 489 | } else { 490 | mGetViewCallback.dragingLogoViewOffset(logoView, false, true, 0); 491 | } 492 | mHideTimer.start(); 493 | 494 | } 495 | 496 | @Override 497 | public void onAnimationRepeat(Animator animation) { 498 | 499 | } 500 | }); 501 | if (!valueAnimator.isRunning()) { 502 | valueAnimator.start(); 503 | } 504 | 505 | // //这里需要判断如果如果手指所在位置和logo所在位置在一个宽度内则不移动, 506 | if (Math.abs(mXInScreen - mXDownInScreen) > logoView.getWidth() / 5 || Math.abs(mYInScreen - mYDownInScreen) > logoView.getHeight() / 5) { 507 | isDrag = false; 508 | } else { 509 | openMenu(); 510 | } 511 | 512 | } 513 | 514 | /** 515 | * 手指离开屏幕后 用于恢复 悬浮球的 logo 的左右位置 516 | */ 517 | private Runnable updatePositionRunnable = new Runnable() { 518 | @Override 519 | public void run() { 520 | isDrag = true; 521 | checkPosition(); 522 | } 523 | }; 524 | 525 | 526 | /** 527 | * 用于检查并更新悬浮球的位置 528 | */ 529 | private void checkPosition() { 530 | if (wmParams.x > 0 && wmParams.x < mScreenWidth) { 531 | if (mHintLocation == LEFT) { 532 | wmParams.x = wmParams.x - mResetLocationValue; 533 | } else { 534 | wmParams.x = wmParams.x + mResetLocationValue; 535 | } 536 | updateViewPosition(); 537 | double a = mScreenWidth / 2; 538 | float offset = (float) ((a - (Math.abs(wmParams.x - a))) / a); 539 | // logoView.setDrag(isDrag, offset, true); 540 | if (mGetViewCallback == null) { 541 | dragLogoViewOffset(logoView, false, true, 0); 542 | } else { 543 | mGetViewCallback.dragingLogoViewOffset(logoView, isDrag, true, offset); 544 | } 545 | 546 | return; 547 | } 548 | 549 | 550 | if (Math.abs(wmParams.x) < 0) { 551 | wmParams.x = 0; 552 | } else if (Math.abs(wmParams.x) > mScreenWidth) { 553 | wmParams.x = mScreenWidth; 554 | } 555 | if (valueAnimator.isRunning()) { 556 | valueAnimator.cancel(); 557 | } 558 | 559 | 560 | updateViewPosition(); 561 | isDrag = false; 562 | 563 | 564 | } 565 | 566 | public void show() { 567 | try { 568 | if (wManager != null && wmParams != null && logoView != null) { 569 | wManager.addView(logoView, wmParams); 570 | } 571 | if (mHideTimer != null) { 572 | mHideTimer.start(); 573 | } else { 574 | initTimer(); 575 | mHideTimer.start(); 576 | } 577 | } catch (Exception e) { 578 | e.printStackTrace(); 579 | } 580 | } 581 | 582 | /** 583 | * 打开菜单 584 | */ 585 | private void openMenu() { 586 | if (isDrag) return; 587 | 588 | if (!isExpanded) { 589 | // logoView.setDrawDarkBg(false); 590 | try { 591 | wManager.removeViewImmediate(logoView); 592 | if (mHintLocation == RIGHT) { 593 | wManager.addView(rightView, wmParams); 594 | if (mGetViewCallback == null) { 595 | rightViewOpened(rightView); 596 | } else { 597 | mGetViewCallback.rightViewOpened(rightView); 598 | } 599 | } else { 600 | wManager.addView(leftView, wmParams); 601 | if (mGetViewCallback == null) { 602 | leftViewOpened(leftView); 603 | } else { 604 | mGetViewCallback.leftViewOpened(leftView); 605 | } 606 | 607 | 608 | } 609 | } catch (Exception e) { 610 | e.printStackTrace(); 611 | } 612 | 613 | isExpanded = true; 614 | mHideTimer.cancel(); 615 | } else { 616 | // logoView.setDrawDarkBg(true); 617 | try { 618 | wManager.removeViewImmediate(mHintLocation == LEFT ? leftView : rightView); 619 | wManager.addView(logoView, wmParams); 620 | if (mGetViewCallback == null) { 621 | leftOrRightViewClosed(logoView); 622 | } else { 623 | mGetViewCallback.leftOrRightViewClosed(logoView); 624 | } 625 | 626 | } catch (Exception e) { 627 | e.printStackTrace(); 628 | } 629 | 630 | isExpanded = false; 631 | mHideTimer.start(); 632 | } 633 | 634 | } 635 | 636 | 637 | /** 638 | * 更新悬浮窗在屏幕中的位置。 639 | */ 640 | private void updateViewPosition() { 641 | isDrag = true; 642 | try { 643 | if (!isExpanded) { 644 | if (wmParams.y - logoView.getHeight() / 2 <= 0) { 645 | wmParams.y = 25; 646 | isDrag = true; 647 | } 648 | wManager.updateViewLayout(logoView, wmParams); 649 | } 650 | } catch (Exception e) { 651 | e.printStackTrace(); 652 | } 653 | } 654 | 655 | /** 656 | * 移除所有悬浮窗 释放资源 657 | */ 658 | public void dismiss() { 659 | //记录上次的位置logo的停放位置,以备下次恢复 660 | saveSetting(LOCATION_X, mHintLocation); 661 | saveSetting(LOCATION_Y, wmParams.y); 662 | logoView.clearAnimation(); 663 | try { 664 | mHideTimer.cancel(); 665 | if (isExpanded) { 666 | wManager.removeViewImmediate(mHintLocation == LEFT ? leftView : rightView); 667 | } else { 668 | wManager.removeViewImmediate(logoView); 669 | } 670 | isExpanded = false; 671 | isDrag = false; 672 | if (mGetViewCallback == null) { 673 | onDestroyed(); 674 | } else { 675 | mGetViewCallback.onDestoryed(); 676 | } 677 | } catch (Exception e) { 678 | e.printStackTrace(); 679 | } 680 | } 681 | 682 | protected abstract View getLeftView(LayoutInflater inflater, View.OnTouchListener touchListener); 683 | 684 | protected abstract View getRightView(LayoutInflater inflater, View.OnTouchListener touchListener); 685 | 686 | protected abstract View getLogoView(LayoutInflater inflater); 687 | 688 | protected abstract void resetLogoViewSize(int hintLocation, View logoView);//logo恢复原始大小 689 | 690 | protected abstract void dragLogoViewOffset(View logoView, boolean isDraging, boolean isResetPosition, float offset); 691 | 692 | protected abstract void shrinkLeftLogoView(View logoView);//logo左边收缩 693 | 694 | protected abstract void shrinkRightLogoView(View logoView);//logo右边收缩 695 | 696 | protected abstract void leftViewOpened(View leftView);//左菜单打开 697 | 698 | protected abstract void rightViewOpened(View rightView);//右菜单打开 699 | 700 | protected abstract void leftOrRightViewClosed(View logoView); 701 | 702 | protected abstract void onDestroyed(); 703 | 704 | public interface GetViewCallback { 705 | View getLeftView(LayoutInflater inflater, View.OnTouchListener touchListener); 706 | 707 | View getRightView(LayoutInflater inflater, View.OnTouchListener touchListener); 708 | 709 | View getLogoView(LayoutInflater inflater); 710 | 711 | 712 | void resetLogoViewSize(int hintLocation, View logoView);//logo恢复原始大小 713 | 714 | void dragingLogoViewOffset(View logoView, boolean isDraging, boolean isResetPosition, float offset);//logo正被拖动,或真在恢复原位 715 | 716 | void shrinkLeftLogoView(View logoView);//logo左边收缩 717 | 718 | void shrinkRightLogoView(View logoView);//logo右边收缩 719 | 720 | void leftViewOpened(View leftView);//左菜单打开 721 | 722 | void rightViewOpened(View rightView);//右菜单打开 723 | 724 | void leftOrRightViewClosed(View logoView); 725 | 726 | void onDestoryed(); 727 | 728 | } 729 | 730 | /** 731 | * 用于保存悬浮球的位置记录 732 | * 733 | * @param key String 734 | * @param defaultValue int 735 | * @return int 736 | */ 737 | private int getSetting(String key, int defaultValue) { 738 | try { 739 | SharedPreferences sharedata = mActivity.getSharedPreferences("floatLogo", 0); 740 | return sharedata.getInt(key, defaultValue); 741 | } catch (Exception e) { 742 | e.printStackTrace(); 743 | } 744 | return defaultValue; 745 | } 746 | 747 | /** 748 | * 用于保存悬浮球的位置记录 749 | * 750 | * @param key String 751 | * @param value int 752 | */ 753 | public void saveSetting(String key, int value) { 754 | try { 755 | SharedPreferences.Editor sharedata = mActivity.getSharedPreferences("floatLogo", 0).edit(); 756 | sharedata.putInt(key, value); 757 | sharedata.apply(); 758 | } catch (Exception e) { 759 | e.printStackTrace(); 760 | } 761 | } 762 | 763 | public static int dp2Px(float dp, Context mContext) { 764 | return (int) TypedValue.applyDimension( 765 | TypedValue.COMPLEX_UNIT_DIP, 766 | dp, 767 | mContext.getResources().getDisplayMetrics()); 768 | } 769 | } 770 | -------------------------------------------------------------------------------- /FloatMenu/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | FloatMenu 16 | 17 | -------------------------------------------------------------------------------- /FloatMenuDemo/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /FloatMenuDemo/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | 6 | defaultConfig { 7 | applicationId "com.yw.game.floatmenu.demo" 8 | minSdkVersion 14 9 | targetSdkVersion 28 10 | 11 | versionCode 1 12 | versionName "1.1" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation fileTree(include: ['*.jar'], dir: 'libs') 24 | testImplementation 'junit:junit:4.12' 25 | implementation 'com.android.support:appcompat-v7:28.0.0' 26 | implementation 'com.yw.game.sclib:shortCutLib:0.0.3' 27 | implementation 'com.android.support:support-v4:28.0.0' 28 | implementation project(':FloatMenu') 29 | // implementation 'com.yw.game.floatmenu:FloatMenu:2.1.0' 30 | } 31 | -------------------------------------------------------------------------------- /FloatMenuDemo/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 /Users/wengyiming/android/adt-bundle-mac-x86_64-20140702/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 | -------------------------------------------------------------------------------- /FloatMenuDemo/src/androidTest/java/com/yw/game/floatmenu/demo/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.yw.game.floatmenu.demo; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/java/com/yw/game/floatmenu/demo/MainActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, Shanghai YUEWEN Information Technology Co., Ltd. 3 | * All rights reserved. 4 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | * 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | * Neither the name of Shanghai YUEWEN Information Technology Co., Ltd. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | * 10 | * THIS SOFTWARE IS PROVIDED BY SHANGHAI YUEWEN INFORMATION TECHNOLOGY CO., LTD. AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | * 12 | */ 13 | 14 | package com.yw.game.floatmenu.demo; 15 | 16 | import android.animation.ValueAnimator; 17 | import android.app.Activity; 18 | import android.graphics.BitmapFactory; 19 | import android.os.Build; 20 | import android.os.Bundle; 21 | import android.os.Handler; 22 | import android.os.Looper; 23 | import android.text.TextUtils; 24 | import android.view.Gravity; 25 | import android.view.LayoutInflater; 26 | import android.view.View; 27 | import android.view.animation.LinearInterpolator; 28 | import android.widget.ImageView; 29 | import android.widget.LinearLayout; 30 | import android.widget.TextView; 31 | import android.widget.Toast; 32 | 33 | import com.yw.game.floatmenu.FloatItem; 34 | import com.yw.game.floatmenu.FloatLogoMenu; 35 | import com.yw.game.floatmenu.FloatMenuView; 36 | import com.yw.game.floatmenu.customfloat.BaseFloatDialog; 37 | 38 | import java.util.ArrayList; 39 | 40 | public class MainActivity extends Activity { 41 | 42 | FloatLogoMenu mFloatMenu; 43 | FloatLogoMenu mFloatMenu1; 44 | ArrayList itemList = new ArrayList<>(); 45 | private Activity mActivity; 46 | 47 | String HOME = "首页"; 48 | String FEEDBACK = "客服"; 49 | String MESSAGE = "消息"; 50 | 51 | String[] MENU_ITEMS = {HOME, FEEDBACK, MESSAGE}; 52 | 53 | private int[] menuIcons = new int[]{R.drawable.yw_menu_account, R.drawable.yw_menu_fb, R.drawable.yw_menu_msg}; 54 | 55 | BaseFloatDialog mBaseFloatDialog; 56 | 57 | @Override 58 | protected void onCreate(Bundle savedInstanceState) { 59 | super.onCreate(savedInstanceState); 60 | setContentView(R.layout.activity_main); 61 | 62 | 63 | mActivity = this; 64 | for (int i = 0; i < menuIcons.length; i++) { 65 | itemList.add(new FloatItem(MENU_ITEMS[i], 0x99000000, 0x99000000, BitmapFactory.decodeResource(this.getResources(), menuIcons[i]), String.valueOf(i + 1))); 66 | } 67 | } 68 | 69 | 70 | @Override 71 | protected void onResume() { 72 | super.onResume(); 73 | if (mFloatMenu == null) { 74 | mFloatMenu = new FloatLogoMenu.Builder() 75 | .withActivity(mActivity) 76 | // .withContext(mActivity.getApplication())//这个在7.0(包括7.0)以上以及大部分7.0以下的国产手机上需要用户授权,需要搭配 77 | .logo(BitmapFactory.decodeResource(getResources(), R.drawable.yw_game_logo)) 78 | .drawCicleMenuBg(true) 79 | .backMenuColor(0xffe4e3e1) 80 | .setBgDrawable(this.getResources().getDrawable(R.drawable.yw_game_float_menu_bg)) 81 | //这个背景色需要和logo的背景色一致 82 | .setFloatItems(itemList) 83 | .defaultLocation(FloatLogoMenu.RIGHT) 84 | .drawRedPointNum(false) 85 | .showWithListener(new FloatMenuView.OnMenuClickListener() { 86 | @Override 87 | public void onItemClick(int position, String title) { 88 | Toast.makeText(MainActivity.this, "position " + position + " title:" + title + " is clicked.", Toast.LENGTH_SHORT).show(); 89 | } 90 | 91 | @Override 92 | public void dismiss() { 93 | 94 | } 95 | }); 96 | 97 | new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { 98 | @Override 99 | public void run() { 100 | refreshDot(); 101 | } 102 | }, 5000); 103 | 104 | //同时只能new一个 105 | } 106 | 107 | 108 | if (mBaseFloatDialog != null) return; 109 | 110 | mBaseFloatDialog = new MyFloatDialog(this); 111 | // mBaseFloatDialog.show(); 112 | 113 | } 114 | 115 | private void showWithCallback() { 116 | mBaseFloatDialog = new BaseFloatDialog.FloatDialogImp(this, new BaseFloatDialog.GetViewCallback() { 117 | @Override 118 | public View getLeftView(LayoutInflater inflater, View.OnTouchListener touchListener) { 119 | LinearLayout linearLayout = new LinearLayout(mActivity); 120 | linearLayout.setOrientation(LinearLayout.HORIZONTAL); 121 | linearLayout.setGravity(Gravity.CENTER); 122 | 123 | TextView textView = new TextView(mActivity); 124 | textView.setText("左边"); 125 | 126 | textView.setOnClickListener(new View.OnClickListener() { 127 | @Override 128 | public void onClick(View v) { 129 | Toast.makeText(mActivity, "左边的菜单被点击了", Toast.LENGTH_SHORT).show(); 130 | } 131 | }); 132 | ImageView imageView = new ImageView(mActivity); 133 | imageView.setLayoutParams(new LinearLayout.LayoutParams(dip2px(50), dip2px(50))); 134 | imageView.setImageResource(R.drawable.yw_game_logo); 135 | imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 136 | imageView.setOnTouchListener(touchListener); 137 | 138 | linearLayout.setLayoutParams(new LinearLayout.LayoutParams(dip2px(50), dip2px(50))); 139 | linearLayout.setBackgroundResource(R.drawable.yw_game_float_menu_bg); 140 | linearLayout.addView(imageView); 141 | linearLayout.addView(textView); 142 | return linearLayout; 143 | } 144 | 145 | @Override 146 | public View getRightView(LayoutInflater inflater, View.OnTouchListener touchListener) { 147 | LinearLayout linearLayout = new LinearLayout(mActivity); 148 | linearLayout.setOrientation(LinearLayout.HORIZONTAL); 149 | linearLayout.setGravity(Gravity.CENTER); 150 | TextView textView = new TextView(mActivity); 151 | textView.setText("右边"); 152 | textView.setOnClickListener(new View.OnClickListener() { 153 | @Override 154 | public void onClick(View v) { 155 | Toast.makeText(mActivity, "右边的菜单被点击了", Toast.LENGTH_SHORT).show(); 156 | } 157 | }); 158 | 159 | ImageView imageView = new ImageView(mActivity); 160 | imageView.setLayoutParams(new LinearLayout.LayoutParams(dip2px(50), dip2px(50))); 161 | imageView.setImageResource(R.drawable.yw_game_logo); 162 | imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 163 | imageView.setOnTouchListener(touchListener); 164 | 165 | linearLayout.setLayoutParams(new LinearLayout.LayoutParams(dip2px(50), dip2px(50))); 166 | linearLayout.setBackgroundResource(R.drawable.yw_game_float_menu_bg); 167 | linearLayout.addView(textView); 168 | linearLayout.addView(imageView); 169 | return linearLayout; 170 | } 171 | 172 | @Override 173 | public View getLogoView(LayoutInflater inflater) { 174 | 175 | 176 | LinearLayout linearLayout = new LinearLayout(mActivity); 177 | linearLayout.setOrientation(LinearLayout.HORIZONTAL); 178 | linearLayout.setGravity(Gravity.CENTER); 179 | 180 | ImageView imageView = new ImageView(mActivity); 181 | imageView.setLayoutParams(new LinearLayout.LayoutParams(dip2px(50), dip2px(50))); 182 | imageView.setScaleType(ImageView.ScaleType.CENTER); 183 | imageView.setImageResource(R.drawable.yw_game_logo); 184 | 185 | linearLayout.setLayoutParams(new LinearLayout.LayoutParams(dip2px(50), dip2px(50))); 186 | linearLayout.setBackgroundResource(R.drawable.yw_game_float_menu_bg); 187 | 188 | linearLayout.addView(imageView); 189 | return linearLayout; 190 | } 191 | 192 | @Override 193 | public void resetLogoViewSize(int hintLocation, View logoView) { 194 | logoView.clearAnimation(); 195 | logoView.setTranslationX(0); 196 | logoView.setScaleX(1); 197 | logoView.setScaleY(1); 198 | } 199 | 200 | @Override 201 | public void dragingLogoViewOffset(final View smallView, boolean isDraging, boolean isResetPosition, float offset) { 202 | if (isDraging && offset > 0) { 203 | smallView.setBackgroundDrawable(null); 204 | smallView.setScaleX(1 + offset); 205 | smallView.setScaleY(1 + offset); 206 | } else { 207 | smallView.setBackgroundResource(R.drawable.yw_game_float_menu_bg); 208 | smallView.setTranslationX(0); 209 | smallView.setScaleX(1); 210 | smallView.setScaleY(1); 211 | } 212 | 213 | 214 | if (isResetPosition) { 215 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 216 | smallView.setRotation(offset * 360); 217 | } 218 | } else { 219 | ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 360 * 4); 220 | valueAnimator.setInterpolator(new LinearInterpolator()); 221 | valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 222 | @Override 223 | public void onAnimationUpdate(ValueAnimator animation) { 224 | int v = (int) animation.getAnimatedValue(); 225 | smallView.setRotation(v); 226 | } 227 | }); 228 | valueAnimator.setDuration(800); 229 | valueAnimator.start(); 230 | } 231 | } 232 | 233 | @Override 234 | public void shrinkLeftLogoView(View smallView) { 235 | smallView.setTranslationX(-smallView.getWidth() / 3); 236 | } 237 | 238 | @Override 239 | public void shrinkRightLogoView(View smallView) { 240 | smallView.setTranslationX(smallView.getWidth() / 3); 241 | } 242 | 243 | @Override 244 | public void leftViewOpened(View leftView) { 245 | Toast.makeText(mActivity, "左边的菜单被打开了", Toast.LENGTH_SHORT).show(); 246 | } 247 | 248 | @Override 249 | public void rightViewOpened(View rightView) { 250 | Toast.makeText(mActivity, "右边的菜单被打开了", Toast.LENGTH_SHORT).show(); 251 | } 252 | 253 | @Override 254 | public void leftOrRightViewClosed(View smallView) { 255 | Toast.makeText(mActivity, "菜单被关闭了", Toast.LENGTH_SHORT).show(); 256 | } 257 | 258 | @Override 259 | public void onDestoryed() { 260 | 261 | } 262 | }); 263 | mBaseFloatDialog.show(); 264 | } 265 | 266 | 267 | private int dip2px(float dipValue) { 268 | final float scale = getResources().getDisplayMetrics().density; 269 | return (int) (dipValue * scale + 0.5f); 270 | } 271 | 272 | public void refreshDot() { 273 | for (FloatItem menuItem : itemList) { 274 | if (TextUtils.equals(menuItem.getTitle(), "我的")) { 275 | menuItem.dotNum = String.valueOf(8); 276 | } 277 | } 278 | mFloatMenu.setFloatItemList(itemList); 279 | } 280 | 281 | 282 | @Override 283 | protected void onPause() { 284 | super.onPause(); 285 | hideFloat(); 286 | } 287 | 288 | @Override 289 | protected void onDestroy() { 290 | destroyFloat(); 291 | super.onDestroy(); 292 | 293 | if (mBaseFloatDialog != null) mBaseFloatDialog.dismiss(); 294 | 295 | } 296 | 297 | 298 | public void hideFloat() { 299 | if (mFloatMenu != null) { 300 | mFloatMenu.hide(); 301 | } 302 | } 303 | 304 | public void destroyFloat() { 305 | if (mFloatMenu != null) { 306 | mFloatMenu.destroyFloat(); 307 | } 308 | mFloatMenu = null; 309 | mActivity = null; 310 | } 311 | 312 | 313 | } 314 | -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/java/com/yw/game/floatmenu/demo/MyFloatDialog.java: -------------------------------------------------------------------------------- 1 | package com.yw.game.floatmenu.demo; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.widget.ImageView; 9 | import android.widget.Toast; 10 | 11 | import com.yw.game.floatmenu.customfloat.BaseFloatDialog; 12 | 13 | /** 14 | * Created by wengyiming on 2017/9/13. 15 | */ 16 | 17 | public class MyFloatDialog extends BaseFloatDialog { 18 | 19 | 20 | public MyFloatDialog(Context context) { 21 | super(context); 22 | } 23 | 24 | @Override 25 | protected View getLeftView(LayoutInflater inflater, View.OnTouchListener touchListener) { 26 | if (getContext() != null) { 27 | Toast.makeText(getContext(), "haha", Toast.LENGTH_SHORT).show(); 28 | } 29 | View view = inflater.inflate(R.layout.layout_menu_left, null); 30 | ImageView leftLogo = (ImageView) view.findViewById(R.id.logo); 31 | leftLogo.setOnTouchListener(touchListener); 32 | return view; 33 | } 34 | 35 | @Override 36 | protected View getRightView(LayoutInflater inflater, View.OnTouchListener touchListener) { 37 | View view = inflater.inflate(R.layout.layout_menu_right, null); 38 | ImageView rightLogo = (ImageView) view.findViewById(R.id.logo); 39 | rightLogo.setOnTouchListener(touchListener); 40 | return view; 41 | } 42 | 43 | @Override 44 | protected View getLogoView(LayoutInflater inflater) { 45 | return inflater.inflate(R.layout.layout_menu_logo, null); 46 | } 47 | 48 | @Override 49 | protected void resetLogoViewSize(int hintLocation, View logoView) { 50 | logoView.clearAnimation(); 51 | logoView.setTranslationX(0); 52 | logoView.setScaleX(1); 53 | logoView.setScaleY(1); 54 | } 55 | 56 | @Override 57 | protected void dragLogoViewOffset(View logoView, boolean isDraging, boolean isResetPosition, float offset) { 58 | if (isDraging && offset > 0) { 59 | logoView.setBackgroundDrawable(null); 60 | logoView.setScaleX(1 + offset); 61 | logoView.setScaleY(1 + offset); 62 | } else { 63 | logoView.setBackgroundResource(R.drawable.yw_game_float_menu_bg); 64 | logoView.setTranslationX(0); 65 | logoView.setScaleX(1); 66 | logoView.setScaleY(1); 67 | } 68 | 69 | 70 | if (isResetPosition) { 71 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 72 | logoView.setRotation(offset * 360); 73 | } 74 | } else { 75 | logoView.setRotation(offset * 360); 76 | } 77 | } 78 | 79 | @Override 80 | public void shrinkLeftLogoView(View smallView) { 81 | smallView.setTranslationX(-smallView.getWidth() / 3); 82 | } 83 | 84 | @Override 85 | public void shrinkRightLogoView(View smallView) { 86 | smallView.setTranslationX(smallView.getWidth() / 3); 87 | } 88 | 89 | @Override 90 | public void leftViewOpened(View leftView) { 91 | Toast.makeText(getContext(), "左边的菜单被打开了", Toast.LENGTH_SHORT).show(); 92 | } 93 | 94 | @Override 95 | public void rightViewOpened(View rightView) { 96 | Toast.makeText(getContext(), "右边的菜单被打开了", Toast.LENGTH_SHORT).show(); 97 | } 98 | 99 | @Override 100 | public void leftOrRightViewClosed(View smallView) { 101 | Toast.makeText(getContext(), "菜单被关闭了", Toast.LENGTH_SHORT).show(); 102 | } 103 | 104 | @Override 105 | protected void onDestroyed() { 106 | if(isApplicationDialog()){ 107 | if(getContext() instanceof Activity){ 108 | dismiss(); 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/drawable-xxxhdpi/yw_game_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/FloatMenuDemo/src/main/res/drawable-xxxhdpi/yw_game_logo.png -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/drawable-xxxhdpi/yw_image_float_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/FloatMenuDemo/src/main/res/drawable-xxxhdpi/yw_image_float_logo.png -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/drawable-xxxhdpi/yw_menu_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/FloatMenuDemo/src/main/res/drawable-xxxhdpi/yw_menu_account.png -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/drawable-xxxhdpi/yw_menu_fb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/FloatMenuDemo/src/main/res/drawable-xxxhdpi/yw_menu_fb.png -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/drawable-xxxhdpi/yw_menu_msg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/FloatMenuDemo/src/main/res/drawable-xxxhdpi/yw_menu_msg.png -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/drawable-xxxhdpi/ywgame_floatmenu_gift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/FloatMenuDemo/src/main/res/drawable-xxxhdpi/ywgame_floatmenu_gift.png -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/drawable-xxxhdpi/ywgame_floatmenu_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/FloatMenuDemo/src/main/res/drawable-xxxhdpi/ywgame_floatmenu_user.png -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/drawable/yw_game_float_menu_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 12 | -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/drawable/yw_game_menu_black_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 12 | -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 23 | 24 | 31 | 32 | -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/layout/layout_menu_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 18 | 25 | 26 | 35 | 36 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/layout/layout_menu_logo.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/layout/layout_menu_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 26 | 27 | 36 | 37 | 38 | 39 | 40 | 47 | 48 | -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/FloatMenuDemo/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/FloatMenuDemo/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/FloatMenuDemo/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/FloatMenuDemo/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/FloatMenuDemo/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | #4DBFF1 17 | #4DBFF1 18 | #FF4081 19 | 20 | -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 16dp 17 | 16dp 18 | 19 | -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | FloatMenuSample 16 | 17 | -------------------------------------------------------------------------------- /FloatMenuDemo/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /FloatMenuDemo/src/test/java/com/yw/game/floatmenu/demo/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.yw.game.floatmenu.demo; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * To work on unit tests, switch the Test Artifact in the Build Variants view. 9 | */ 10 | public class ExampleUnitTest { 11 | @Test 12 | public void addition_isCorrect() throws Exception { 13 | assertEquals(4, 2 + 2); 14 | } 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, 阅文集团 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FloatMenuSample 2 | 3 | [ ![Download](https://api.bintray.com/packages/fanofdemo/maven/FloatMenu/images/download.svg) ] 4 | 5 | [crosg/FloatMenuSample](https://github.com/crosg/FloatMenuSample) 6 | transfer from [yiming/FloatMenuSample](https://github.com/fanOfDemo/FloatMenuSample) 7 | 8 | 9 | 10 | ## GIF 11 | 12 | 13 | 14 | ## GRADLE: 15 | 16 | compile 'com.yw.game.floatmenu:FloatMenu:2.0.1' 17 | 18 | 19 | android float menu in app 20 | 21 | ## 权限 compatibility & permissions 22 | 23 | 无权限需求,支持 api 11 ++ 24 | 25 | 26 | for use: 27 | 28 | 29 | 30 | 使用示例 31 | see sample 32 | 33 | [MainActivity](https://github.com/fanOfDemo/FloatMenuSample/blob/master/FloatMenuDemo/src/main/java/com/yw/game/floatmenu/demo/MainActivity.java) 34 | 35 | 36 | 1: 37 | 38 | mFloatMenu = new FloatLogoMenu.Builder() 39 | .withActivity(mActivity) 40 | // .withContext(mActivity.getApplication())//这个在7.0(包括7.0)以上以及大部分7.0以下的国产手机上需要用户授权,需要搭配 41 | .logo(BitmapFactory.decodeResource(getResources(),R.drawable.yw_game_logo)) 42 | .drawCicleMenuBg(true) 43 | .backMenuColor(0xffe4e3e1) 44 | .setBgDrawable(this.getResources().getDrawable(R.drawable.yw_game_float_menu_bg)) 45 | //这个背景色需要和logo的背景色一致 46 | .setFloatItems(itemList) 47 | .defaultLocation(FloatLogoMenu.RIGHT) 48 | .drawRedPointNum(false) 49 | .showWithListener(new FloatMenuView.OnMenuClickListener() { 50 | @Override 51 | public void onItemClick(int position, String title) { 52 | Toast.makeText(MainActivity.this, "position " + position + " title:" + title + " is clicked.", Toast.LENGTH_SHORT).show(); 53 | } 54 | 55 | @Override 56 | public void dismiss() { 57 | 58 | } 59 | }); 60 | 61 | 62 | 2: 63 | 64 | mFloatMenu = new FloatLogoMenu.Builder() 65 | .withActivity(mActivity) 66 | .logo(R.drawable.yw_image_float_logo) 67 | .backMenuColor(0xffe4e3e1) 68 | .drawCicleMenuBg(true) 69 | .setFloatItems(itemList) 70 | .defaultLocation(FloatLogoMenu.RIGHT) 71 | .drawRedPointNum(false) 72 | .showWithListener(new FloatMenuView.OnMenuClickListener() { 73 | @Override 74 | public void onItemClick(int position, String title) { 75 | Toast.makeText(MainActivity.this, "position " + position + " title:" + title + " is clicked.", Toast.LENGTH_SHORT).show(); 76 | } 77 | 78 | @Override 79 | public void dismiss() { 80 | 81 | } 82 | }); 83 | 84 | 85 | 3: 以下方法供自定义: 86 | 87 | 可将 [BaseFloatDailog](https://github.com/crosg/FloatMenuSample/blob/master/FloatMenu/src/main/java/com/yw/game/floatmenu/customfloat/BaseFloatDailog.java) 文件拷贝出来直接使用 88 | 89 | 90 | 91 | 92 | 通过实现接口的形式自定义 93 | BaseFloatDailog mBaseFloatDailog = new BaseFloatDailog.FloatDialogImp(this, new BaseFloatDailog.GetViewCallback() {}); 94 | 95 | 或通过继承的形式自定义: 96 | 97 | public class MyFloatDialog extends BaseFloatDailog { 98 | .... 99 | } 100 | 101 | ## 更新日志 102 | UPDATE LOG: 103 | 104 | * 1.+ 桌面或应用内悬浮窗 (removed) 105 | 106 | compile 'com.yw.game.floatmenu:FloatMenu:1.0.0' 107 | 108 | 109 | * 2.0.0 重构版,应用内悬浮窗 110 | 111 | compile 'com.yw.game.floatmenu:FloatMenu:2.0.0' 112 | 113 | //2.0.0版使用windows.addContentView接口,在unity3D游戏引擎需要开启该选项 114 | 115 | 116 | 117 | * 2.0.1 可选择支持出现在桌面(需权限),应用内无权限需要。 118 | 119 | 移除windows.addContentView接口 120 | 出现在桌面的话需要: 121 | 使用全局上下文 .withContext(mService.getApplication()) 122 | 并配合权限 123 | 124 | 125 | 126 | 127 | ## License 128 | 129 | 130 | Copyright (c) 2016, Shanghai YUEWEN Information Technology Co., Ltd. 131 | All rights reserved. 132 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 133 | 134 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 135 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 136 | * Neither the name of Shanghai YUEWEN Information Technology Co., Ltd. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 137 | 138 | THIS SOFTWARE IS PROVIDED BY SHANGHAI YUEWEN INFORMATION TECHNOLOGY CO., LTD. AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | google() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.6.1' 10 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' 11 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | jcenter() 20 | maven { 21 | url 'https://dl.bintray.com/fanofdemo/maven/' 22 | } 23 | google() 24 | } 25 | } 26 | 27 | tasks.getByPath(":FloatMenu:javadoc").enabled = false 28 | task clean(type: Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | systemProp.http.proxyHost=127.0.0.1 2 | org.gradle.parallel=true 3 | systemProp.http.proxyPort=1080 4 | 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Apr 07 17:13:25 CST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /picture/20160503125603.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/picture/20160503125603.png -------------------------------------------------------------------------------- /picture/201605031543.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/picture/201605031543.gif -------------------------------------------------------------------------------- /picture/201605041543.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/picture/201605041543.gif -------------------------------------------------------------------------------- /picture/201606161036.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/picture/201606161036.gif -------------------------------------------------------------------------------- /picture/2016112315.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/picture/2016112315.gif -------------------------------------------------------------------------------- /picture/chinareadlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/picture/chinareadlogo.png -------------------------------------------------------------------------------- /picture/floatmen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/picture/floatmen.png -------------------------------------------------------------------------------- /picture/floatmenu2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crosg/FloatMenuSample/ad85459edaf567627d7a9aa8f0cd7b04714c2d85/picture/floatmenu2.gif -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':FloatMenuDemo' 2 | include ':FloatMenu' --------------------------------------------------------------------------------