├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── assets
│ │ └── 99nights.mp3
│ ├── java
│ │ └── com
│ │ │ └── cpacm
│ │ │ └── musicbtn
│ │ │ ├── FmmActivity.java
│ │ │ ├── MusicPlayerActivity.java
│ │ │ └── MusicUtils.java
│ └── res
│ │ ├── drawable-xxhdpi
│ │ ├── author.jpg
│ │ ├── cover.jpg
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_round.png
│ │ └── moefou.jpg
│ │ ├── drawable
│ │ ├── ic_add.xml
│ │ ├── ic_pause.xml
│ │ ├── ic_play.xml
│ │ ├── ic_play_detail.xml
│ │ └── ic_remove.xml
│ │ ├── layout
│ │ ├── activity_fmm.xml
│ │ └── activity_main.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── cpacm
│ └── musicbtn
│ └── ExampleUnitTest.java
├── bintray.gradle
├── build.gradle
├── floatingmusicmenu
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── cpacm
│ │ │ ├── FloatingMusicButton.java
│ │ │ ├── FloatingMusicMenu.java
│ │ │ └── RotatingProgressDrawable.java
│ └── res
│ │ └── values
│ │ ├── attrs.xml
│ │ └── strings.xml
│ └── test
│ └── java
│ └── com
│ └── cpacm
│ └── floatingmusicbutton
│ └── ExampleUnitTest.java
├── gradle.properties
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 | .externalNativeBuild
10 | .idea
11 | gradle
12 | /gradlew
13 | /gradlew.bat
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FloatingMusicMenu
2 |
3 | 一款可用于音乐播放器的悬浮菜单按钮,它是基于 `FloatingActionButton` 上完成,能够联动音乐播放器显示歌曲的进度,设置歌曲的封面和通过封面的旋转来展示播放的状态(停止或者播放)。
4 | 除此之外,它可以设置一组按钮作为菜单展示,支持上下左右四个方位显示,更方便的是可以在代码中动态的添加按钮或者移除按钮。
5 |
6 | 
7 | 
8 |
9 | ## 引入
10 | ```groovy
11 | dependencies {
12 | compile 'com.cpacm:floatingmusicmenu:1.1.0'
13 | }
14 | ```
15 | ### 具体使用
16 | 可以直接在你的layout布局文件中直接定义
17 | ```xml
18 |
19 | * 装载旋转进度按钮位图的按钮,继承自{@link FloatingActionButton} 20 | *
21 | *22 | * 23 | * @author cpacm 24 | *
25 | */ 26 | public class FloatingMusicButton extends FloatingActionButton { 27 | 28 | private RotatingProgressDrawable coverDrawable; 29 | private int percent, color; 30 | private ColorStateList backgroundHint; 31 | private float progress = 0f; 32 | private boolean isRotation = false; 33 | 34 | public FloatingMusicButton(Context context) { 35 | super(context); 36 | setMaxImageSize(); 37 | } 38 | 39 | public FloatingMusicButton(Context context, AttributeSet attrs) { 40 | super(context, attrs); 41 | setMaxImageSize(); 42 | } 43 | 44 | public FloatingMusicButton(Context context, AttributeSet attrs, int defStyleAttr) { 45 | super(context, attrs, defStyleAttr); 46 | setMaxImageSize(); 47 | } 48 | 49 | @Override 50 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 51 | super.onLayout(changed, left, top, right, bottom); 52 | } 53 | 54 | /** 55 | * 利用反射重新定义fab图片的大小,默认充满整个fab 56 | */ 57 | public void setMaxImageSize() { 58 | try { 59 | Class clazz = getClass().getSuperclass(); 60 | Method sizeMethod = clazz.getDeclaredMethod("getSizeDimension"); 61 | sizeMethod.setAccessible(true); 62 | int size = (Integer) sizeMethod.invoke(this); 63 | //set fab maxsize 64 | Field field = clazz.getDeclaredField("maxImageSize"); 65 | field.setAccessible(true); 66 | field.setInt(this,size); 67 | //get fab impl 68 | Field field2 = clazz.getDeclaredField("impl"); 69 | field2.setAccessible(true); 70 | Object o = field2.get(this); 71 | //set fabimpl maxsize 72 | Method maxMethod = o.getClass().getSuperclass().getDeclaredMethod("setMaxImageSize", int.class); 73 | maxMethod.setAccessible(true); 74 | maxMethod.invoke(o, size); 75 | 76 | } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException | NoSuchFieldException e) { 77 | e.printStackTrace(); 78 | } 79 | //postInvalidate(); 80 | } 81 | 82 | /** 83 | * 对fmb进行配置 84 | * 85 | * @param percent 进度条宽度百分比 86 | * @param color 进度条颜色 87 | * @param backgroundHint fmb背景颜色 88 | */ 89 | public void config(int percent, int color, ColorStateList backgroundHint) { 90 | this.percent = percent; 91 | this.color = color; 92 | this.backgroundHint = backgroundHint; 93 | config(); 94 | } 95 | 96 | public void config() { 97 | if (coverDrawable != null) { 98 | coverDrawable.setProgressWidthPercent(percent); 99 | coverDrawable.setProgressColor(color); 100 | if (backgroundHint != null) { 101 | setBackgroundTintList(backgroundHint); 102 | } 103 | coverDrawable.setProgress(progress); 104 | coverDrawable.rotate(isRotation); 105 | //setMaxImageSize(); 106 | } 107 | } 108 | 109 | /** 110 | * 设置进度 111 | * 112 | * @param progress 113 | */ 114 | public void setProgress(float progress) { 115 | this.progress = progress; 116 | if (coverDrawable != null) { 117 | coverDrawable.setProgress(progress); 118 | } 119 | } 120 | 121 | /** 122 | * 设置按钮背景 123 | * 124 | * @param drawable 125 | */ 126 | public void setCoverDrawable(Drawable drawable) { 127 | this.coverDrawable = new RotatingProgressDrawable(drawable); 128 | config(); 129 | setImageDrawable(this.coverDrawable); 130 | postInvalidate(); 131 | } 132 | 133 | public void setCover(Bitmap bitmap) { 134 | coverDrawable = new RotatingProgressDrawable(getResources(),bitmap); 135 | config(); 136 | setImageDrawable(this.coverDrawable); 137 | postInvalidate(); 138 | } 139 | 140 | public void rotate(boolean rotate) { 141 | coverDrawable.rotate(rotate); 142 | isRotation = rotate; 143 | } 144 | 145 | @Override 146 | protected Parcelable onSaveInstanceState() { 147 | super.onSaveInstanceState(); 148 | Bundle bundle = new Bundle(); 149 | bundle.putBoolean("rotation", isRotation); 150 | bundle.putFloat("progress", progress); 151 | if (coverDrawable != null) { 152 | bundle.putFloat("rotation_angle", coverDrawable.getRotation()); 153 | } 154 | return bundle; 155 | } 156 | 157 | @Override 158 | protected void onRestoreInstanceState(Parcelable state) { 159 | super.onRestoreInstanceState(state); 160 | Bundle bundle = (Bundle) state; 161 | isRotation = bundle.getBoolean("rotation"); 162 | progress = bundle.getFloat("progress"); 163 | requestLayout(); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /floatingmusicmenu/src/main/java/com/cpacm/FloatingMusicMenu.java: -------------------------------------------------------------------------------- 1 | package com.cpacm; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.AnimatorSet; 6 | import android.animation.ObjectAnimator; 7 | import android.annotation.TargetApi; 8 | import android.content.Context; 9 | import android.content.res.ColorStateList; 10 | import android.content.res.TypedArray; 11 | import android.graphics.Bitmap; 12 | import android.graphics.drawable.Drawable; 13 | import android.os.Build; 14 | 15 | import androidx.coordinatorlayout.widget.CoordinatorLayout; 16 | import androidx.core.view.ViewCompat; 17 | import android.util.AttributeSet; 18 | import android.util.TypedValue; 19 | import android.view.MotionEvent; 20 | import android.view.View; 21 | import android.view.ViewGroup; 22 | import android.view.animation.DecelerateInterpolator; 23 | import android.view.animation.Interpolator; 24 | import android.view.animation.OvershootInterpolator; 25 | 26 | import com.cpacm.floatingmusicbutton.R; 27 | import com.google.android.material.floatingactionbutton.FloatingActionButton; 28 | 29 | /** 30 | *31 | * 浮动音乐菜单,可以显示歌曲封面和旋转动画并随着音乐显示进度。 32 | *
52 | * 可以通过调用 {@link #addButton(FloatingActionButton)} 和 {@link #removeButton(FloatingActionButton)} 来动态增减按钮数量。 53 | * 54 | * @author cpacm 55 | *
56 | */ 57 | @CoordinatorLayout.DefaultBehavior(FloatingMusicMenu.Behavior.class) 58 | public class FloatingMusicMenu extends ViewGroup { 59 | 60 | public final static int FLOATING_DIRECTION_UP = 0; 61 | public final static int FLOATING_DIRECTION_LEFT = 1; 62 | public final static int FLOATING_DIRECTION_DOWN = 2; 63 | public final static int FLOATING_DIRECTION_RIGHT = 3; 64 | 65 | private static final int SHADOW_OFFSET = 20; 66 | 67 | private FloatingMusicButton floatingMusicButton; 68 | private AnimatorSet showAnimation; 69 | private AnimatorSet hideAnimation; 70 | 71 | private int progressWidthPercent; 72 | private int progressColor; 73 | private float progress; 74 | private float buttonInterval; 75 | private ColorStateList backgroundTint; 76 | private Drawable cover; 77 | private boolean isExpanded; 78 | private boolean isHided; 79 | private int floatingDirection; 80 | 81 | public FloatingMusicMenu(Context context) { 82 | this(context, null); 83 | } 84 | 85 | public FloatingMusicMenu(Context context, AttributeSet attrs) { 86 | super(context, attrs); 87 | initMenu(context, attrs); 88 | } 89 | 90 | public FloatingMusicMenu(Context context, AttributeSet attrs, int defStyleAttr) { 91 | super(context, attrs, defStyleAttr); 92 | initMenu(context, attrs); 93 | } 94 | 95 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 96 | public FloatingMusicMenu(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 97 | super(context, attrs, defStyleAttr, defStyleRes); 98 | initMenu(context, attrs); 99 | } 100 | 101 | private void initMenu(Context context, AttributeSet attrs) { 102 | TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.FloatingMusicMenu, 0, 0); 103 | progressWidthPercent = attr.getInteger(R.styleable.FloatingMusicMenu_fmm_progress_percent, 3); 104 | progressColor = attr.getColor(R.styleable.FloatingMusicMenu_fmm_progress_color, getResources().getColor(android.R.color.holo_blue_dark)); 105 | progress = attr.getFloat(R.styleable.FloatingMusicMenu_fmm_progress, 0); 106 | buttonInterval = attr.getDimension(R.styleable.FloatingMusicMenu_fmm_button_interval, 4); 107 | buttonInterval = dp2px(buttonInterval); 108 | /* if (Build.VERSION.SDK_INT < 21) { 109 | // 版本兼容 110 | buttonInterval = -BitmapUtils.dp2px(16); 111 | }*/ 112 | cover = attr.getDrawable(R.styleable.FloatingMusicMenu_fmm_cover); 113 | backgroundTint = attr.getColorStateList(R.styleable.FloatingMusicMenu_fmm_backgroundTint); 114 | floatingDirection = attr.getInteger(R.styleable.FloatingMusicMenu_fmm_floating_direction, 0); 115 | attr.recycle(); 116 | createRootButton(context); 117 | addScrollAnimation(); 118 | } 119 | 120 | private void addScrollAnimation() { 121 | showAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION); 122 | showAnimation.play(ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f)); 123 | showAnimation.setInterpolator(alphaExpandInterpolator); 124 | showAnimation.addListener(new AnimatorListenerAdapter() { 125 | @Override 126 | public void onAnimationEnd(Animator animation) { 127 | super.onAnimationEnd(animation); 128 | setVisibility(VISIBLE); 129 | } 130 | 131 | @Override 132 | public void onAnimationStart(Animator animation) { 133 | super.onAnimationStart(animation); 134 | setVisibility(VISIBLE); 135 | } 136 | }); 137 | 138 | hideAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION); 139 | hideAnimation.play(ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f)); 140 | hideAnimation.setInterpolator(alphaExpandInterpolator); 141 | hideAnimation.addListener(new AnimatorListenerAdapter() { 142 | @Override 143 | public void onAnimationEnd(Animator animation) { 144 | super.onAnimationEnd(animation); 145 | setVisibility(INVISIBLE); 146 | } 147 | }); 148 | } 149 | 150 | private void createRootButton(Context context) { 151 | floatingMusicButton = new FloatingMusicButton(context); 152 | floatingMusicButton.setOnClickListener(new OnClickListener() { 153 | @Override 154 | public void onClick(View v) { 155 | toggle(); 156 | } 157 | }); 158 | floatingMusicButton.config(progressWidthPercent, progressColor, backgroundTint); 159 | floatingMusicButton.setProgress(progress); 160 | if (cover != null) { 161 | floatingMusicButton.setCoverDrawable(cover); 162 | } 163 | } 164 | 165 | @Override 166 | protected void onFinishInflate() { 167 | super.onFinishInflate(); 168 | addView(floatingMusicButton, super.generateDefaultLayoutParams()); 169 | } 170 | 171 | @Override 172 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 173 | measureChildren(widthMeasureSpec, heightMeasureSpec); 174 | switch (floatingDirection) { 175 | case FLOATING_DIRECTION_UP: 176 | case FLOATING_DIRECTION_DOWN: 177 | onMeasureVerticalDirection(); 178 | break; 179 | case FLOATING_DIRECTION_LEFT: 180 | case FLOATING_DIRECTION_RIGHT: 181 | onMeasureHorizontalDirection(); 182 | break; 183 | } 184 | } 185 | 186 | /** 187 | * 计算竖向排列时需要的大小 188 | */ 189 | private void onMeasureVerticalDirection() { 190 | int width = 0; 191 | int height = 0; 192 | for (int i = 0; i < getChildCount(); i++) { 193 | View child = getChildAt(i); 194 | if (child.getVisibility() == GONE) 195 | continue; 196 | width = Math.max(child.getMeasuredWidth(), width); 197 | height += child.getMeasuredHeight(); 198 | } 199 | width += SHADOW_OFFSET * 2; 200 | height += SHADOW_OFFSET * 2; 201 | height += buttonInterval * (getChildCount() - 1); 202 | height = adjustShootLength(height); 203 | setMeasuredDimension(width, height); 204 | } 205 | 206 | /** 207 | * 计算横向排列时需要的大小 208 | */ 209 | private void onMeasureHorizontalDirection() { 210 | int width = 0; 211 | int height = 0; 212 | for (int i = 0; i < getChildCount(); i++) { 213 | View child = getChildAt(i); 214 | if (child.getVisibility() == GONE) 215 | continue; 216 | height = Math.max(child.getMeasuredHeight(), height); 217 | width += child.getMeasuredWidth(); 218 | } 219 | width += SHADOW_OFFSET * 2; 220 | height += SHADOW_OFFSET * 2; 221 | width += buttonInterval * (getChildCount() - 1); 222 | width = adjustShootLength(width); 223 | setMeasuredDimension(width, height); 224 | } 225 | 226 | private int adjustShootLength(int length) { 227 | return length * 12 / 10; 228 | } 229 | 230 | @Override 231 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 232 | switch (floatingDirection) { 233 | case FLOATING_DIRECTION_UP: 234 | onUpDirectionLayout(l, t, r, b); 235 | break; 236 | case FLOATING_DIRECTION_DOWN: 237 | onDownDirectionLayout(l, t, r, b); 238 | break; 239 | case FLOATING_DIRECTION_LEFT: 240 | onLeftDirectionLayout(l, t, r, b); 241 | break; 242 | case FLOATING_DIRECTION_RIGHT: 243 | onRightDirectionLayout(l, t, r, b); 244 | break; 245 | } 246 | } 247 | 248 | /** 249 | * 摆放朝上展开方向的子控件位置 250 | */ 251 | private void onUpDirectionLayout(int l, int t, int r, int b) { 252 | int centerX = (r - l) / 2; 253 | int offsetY = b - t - SHADOW_OFFSET; 254 | 255 | for (int i = getChildCount() - 1; i >= 0; i--) { 256 | View child = getChildAt(i); 257 | if (child.getVisibility() == GONE) 258 | continue; 259 | int width = child.getMeasuredWidth(); 260 | int height = child.getMeasuredHeight(); 261 | child.layout(centerX - width / 2, offsetY - height, centerX + width / 2, offsetY); 262 | 263 | //排除根按钮,添加动画 264 | if (i != getChildCount() - 1) { 265 | float collapsedTranslation = b - t - SHADOW_OFFSET - offsetY; 266 | float expandedTranslation = 0f; 267 | child.setTranslationY(isExpanded ? expandedTranslation : collapsedTranslation); 268 | child.setAlpha(isExpanded ? 1f : 0f); 269 | 270 | MenuLayoutParams params = (MenuLayoutParams) child.getLayoutParams(); 271 | params.collapseDirAnim.setFloatValues(expandedTranslation, collapsedTranslation); 272 | params.expandDirAnim.setFloatValues(collapsedTranslation, expandedTranslation); 273 | params.collapseDirAnim.setProperty(View.TRANSLATION_Y); 274 | params.expandDirAnim.setProperty(View.TRANSLATION_Y); 275 | params.setAnimationsTarget(child); 276 | } 277 | offsetY -= height + buttonInterval; 278 | } 279 | } 280 | 281 | /** 282 | * 摆放朝下展开方向的子控件位置 283 | */ 284 | private void onDownDirectionLayout(int l, int t, int r, int b) { 285 | int centerX = (r - l) / 2; 286 | int offsetY = SHADOW_OFFSET; 287 | View rootView = getChildAt(getChildCount() - 1); 288 | rootView.layout(centerX - rootView.getMeasuredWidth() / 2, offsetY, centerX + rootView.getMeasuredWidth() / 2, offsetY + rootView.getMeasuredHeight()); 289 | offsetY += rootView.getMeasuredHeight() + buttonInterval; 290 | 291 | for (int i = 0; i < getChildCount() - 1; i++) { 292 | View child = getChildAt(i); 293 | if (child.getVisibility() == GONE) 294 | continue; 295 | int width = child.getMeasuredWidth(); 296 | int height = child.getMeasuredHeight(); 297 | child.layout(centerX - width / 2, offsetY, centerX + width / 2, offsetY + height); 298 | 299 | float collapsedTranslation = -offsetY; 300 | float expandedTranslation = 0f; 301 | child.setTranslationY(isExpanded ? expandedTranslation : collapsedTranslation); 302 | child.setAlpha(isExpanded ? 1f : 0f); 303 | 304 | MenuLayoutParams params = (MenuLayoutParams) child.getLayoutParams(); 305 | params.collapseDirAnim.setFloatValues(expandedTranslation, collapsedTranslation); 306 | params.expandDirAnim.setFloatValues(collapsedTranslation, expandedTranslation); 307 | params.collapseDirAnim.setProperty(View.TRANSLATION_Y); 308 | params.expandDirAnim.setProperty(View.TRANSLATION_Y); 309 | params.setAnimationsTarget(child); 310 | 311 | offsetY += height + buttonInterval; 312 | } 313 | } 314 | 315 | /** 316 | * 摆放朝左展开方向的子控件位置 317 | */ 318 | private void onLeftDirectionLayout(int l, int t, int r, int b) { 319 | int centerY = (b - t) / 2; 320 | int offsetX = r - l - SHADOW_OFFSET; 321 | 322 | for (int i = getChildCount() - 1; i >= 0; i--) { 323 | View child = getChildAt(i); 324 | if (child.getVisibility() == GONE) 325 | continue; 326 | int width = child.getMeasuredWidth(); 327 | int height = child.getMeasuredHeight(); 328 | child.layout(offsetX - width, centerY - height / 2, offsetX, centerY + height / 2); 329 | 330 | //排除根按钮,添加动画 331 | if (i != getChildCount() - 1) { 332 | float collapsedTranslation = r - l - SHADOW_OFFSET - offsetX; 333 | float expandedTranslation = 0f; 334 | child.setTranslationX(isExpanded ? expandedTranslation : collapsedTranslation); 335 | child.setAlpha(isExpanded ? 1f : 0f); 336 | 337 | MenuLayoutParams params = (MenuLayoutParams) child.getLayoutParams(); 338 | params.collapseDirAnim.setFloatValues(expandedTranslation, collapsedTranslation); 339 | params.expandDirAnim.setFloatValues(collapsedTranslation, expandedTranslation); 340 | params.collapseDirAnim.setProperty(View.TRANSLATION_X); 341 | params.expandDirAnim.setProperty(View.TRANSLATION_X); 342 | params.setAnimationsTarget(child); 343 | } 344 | offsetX -= width + buttonInterval; 345 | } 346 | } 347 | 348 | /** 349 | * 摆放朝右展开方向的子控件位置 350 | */ 351 | private void onRightDirectionLayout(int l, int t, int r, int b) { 352 | int centerY = (b - t) / 2; 353 | int offsetX = SHADOW_OFFSET; 354 | View rootView = getChildAt(getChildCount() - 1); 355 | rootView.layout(offsetX, centerY - rootView.getMeasuredHeight() / 2, offsetX + rootView.getMeasuredWidth(), centerY + rootView.getMeasuredHeight() / 2); 356 | offsetX += rootView.getMeasuredWidth() + buttonInterval; 357 | 358 | for (int i = 0; i < getChildCount() - 1; i++) { 359 | View child = getChildAt(i); 360 | if (child.getVisibility() == GONE) 361 | continue; 362 | int width = child.getMeasuredWidth(); 363 | int height = child.getMeasuredHeight(); 364 | child.layout(offsetX, centerY - height / 2, offsetX + width, centerY + height / 2); 365 | 366 | float collapsedTranslation = -offsetX; 367 | float expandedTranslation = 0f; 368 | child.setTranslationX(isExpanded ? expandedTranslation : collapsedTranslation); 369 | child.setAlpha(isExpanded ? 1f : 0f); 370 | 371 | MenuLayoutParams params = (MenuLayoutParams) child.getLayoutParams(); 372 | params.collapseDirAnim.setFloatValues(expandedTranslation, collapsedTranslation); 373 | params.expandDirAnim.setFloatValues(collapsedTranslation, expandedTranslation); 374 | params.collapseDirAnim.setProperty(View.TRANSLATION_X); 375 | params.expandDirAnim.setProperty(View.TRANSLATION_X); 376 | params.setAnimationsTarget(child); 377 | 378 | offsetX += width + buttonInterval; 379 | } 380 | } 381 | 382 | public void setButtonInterval(float buttonInterval) { 383 | this.buttonInterval = buttonInterval; 384 | requestLayout(); 385 | } 386 | 387 | public void addButton(FloatingActionButton button) { 388 | addView(button, 0); 389 | requestLayout(); 390 | } 391 | 392 | public void addButtonAtLast(FloatingActionButton button) { 393 | addView(button, getChildCount() - 1); 394 | requestLayout(); 395 | } 396 | 397 | public void removeButton(FloatingActionButton button) { 398 | removeView(button); 399 | requestLayout(); 400 | } 401 | 402 | public void setMusicCover(Drawable drawable) { 403 | floatingMusicButton.setCoverDrawable(drawable); 404 | } 405 | 406 | public void setMusicCover(Bitmap bitmap) { 407 | floatingMusicButton.setCover(bitmap); 408 | } 409 | 410 | public void setProgress(float progress) { 411 | if (floatingMusicButton != null) { 412 | floatingMusicButton.setProgress(progress); 413 | } 414 | } 415 | 416 | public void start() { 417 | floatingMusicButton.rotate(true); 418 | } 419 | 420 | public void stop() { 421 | floatingMusicButton.rotate(false); 422 | } 423 | 424 | public void setFloatingDirection(int floatingDirection) { 425 | this.floatingDirection = floatingDirection; 426 | postInvalidate(); 427 | } 428 | 429 | @Override 430 | protected LayoutParams generateDefaultLayoutParams() { 431 | return new MenuLayoutParams(super.generateDefaultLayoutParams()); 432 | } 433 | 434 | @Override 435 | public LayoutParams generateLayoutParams(AttributeSet attrs) { 436 | return new MenuLayoutParams(super.generateLayoutParams(attrs)); 437 | } 438 | 439 | @Override 440 | protected LayoutParams generateLayoutParams(LayoutParams p) { 441 | return new MenuLayoutParams(super.generateLayoutParams(p)); 442 | } 443 | 444 | private static final int ANIMATION_DURATION = 300; 445 | private static final float COLLAPSED_PLUS_ROTATION = 0f; 446 | private static final float EXPANDED_PLUS_ROTATION = 90f + 45f; 447 | 448 | private AnimatorSet mExpandAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION); 449 | private AnimatorSet mCollapseAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION); 450 | 451 | private static Interpolator expandInterpolator = new OvershootInterpolator(); 452 | private static Interpolator collapseInterpolator = new DecelerateInterpolator(3f); 453 | private static Interpolator alphaExpandInterpolator = new DecelerateInterpolator(); 454 | 455 | private class MenuLayoutParams extends LayoutParams { 456 | 457 | private ObjectAnimator expandDirAnim = new ObjectAnimator(); 458 | private ObjectAnimator expandAlphaAnim = new ObjectAnimator(); 459 | private ObjectAnimator collapseDirAnim = new ObjectAnimator(); 460 | private ObjectAnimator collapseAlphaAnim = new ObjectAnimator(); 461 | 462 | private boolean animationsSetToPlay; 463 | 464 | public MenuLayoutParams(LayoutParams source) { 465 | super(source); 466 | 467 | expandDirAnim.setInterpolator(expandInterpolator); 468 | expandAlphaAnim.setInterpolator(alphaExpandInterpolator); 469 | collapseDirAnim.setInterpolator(collapseInterpolator); 470 | collapseAlphaAnim.setInterpolator(collapseInterpolator); 471 | 472 | collapseAlphaAnim.setProperty(View.ALPHA); 473 | collapseAlphaAnim.setFloatValues(1f, 0f); 474 | 475 | expandAlphaAnim.setProperty(View.ALPHA); 476 | expandAlphaAnim.setFloatValues(0f, 1f); 477 | } 478 | 479 | public void setAnimationsTarget(View view) { 480 | collapseAlphaAnim.setTarget(view); 481 | collapseDirAnim.setTarget(view); 482 | expandDirAnim.setTarget(view); 483 | expandAlphaAnim.setTarget(view); 484 | 485 | // Now that the animations have targets, set them to be played 486 | if (!animationsSetToPlay) { 487 | addLayerTypeListener(expandDirAnim, view); 488 | addLayerTypeListener(collapseDirAnim, view); 489 | 490 | mCollapseAnimation.play(collapseAlphaAnim); 491 | mCollapseAnimation.play(collapseDirAnim); 492 | mExpandAnimation.play(expandAlphaAnim); 493 | mExpandAnimation.play(expandDirAnim); 494 | animationsSetToPlay = true; 495 | } 496 | } 497 | 498 | private void addLayerTypeListener(Animator animator, final View view) { 499 | animator.addListener(new AnimatorListenerAdapter() { 500 | @Override 501 | public void onAnimationEnd(Animator animation) { 502 | view.setLayerType(LAYER_TYPE_NONE, null); 503 | } 504 | 505 | @Override 506 | public void onAnimationStart(Animator animation) { 507 | view.setLayerType(LAYER_TYPE_HARDWARE, null); 508 | } 509 | }); 510 | } 511 | } 512 | 513 | public void collapse() { 514 | collapse(false); 515 | } 516 | 517 | public void collapseImmediately() { 518 | collapse(true); 519 | } 520 | 521 | private void collapse(boolean immediately) { 522 | if (isExpanded) { 523 | isExpanded = false; 524 | mCollapseAnimation.setDuration(immediately ? 0 : ANIMATION_DURATION); 525 | mCollapseAnimation.start(); 526 | mExpandAnimation.cancel(); 527 | } 528 | } 529 | 530 | public void toggle() { 531 | if (isExpanded) { 532 | collapse(); 533 | } else { 534 | expand(); 535 | } 536 | } 537 | 538 | public void expand() { 539 | if (!isExpanded) { 540 | isExpanded = true; 541 | mCollapseAnimation.cancel(); 542 | mExpandAnimation.start(); 543 | } 544 | } 545 | 546 | public boolean isExpanded() { 547 | return isExpanded; 548 | } 549 | 550 | public void hide() { 551 | if (!isHided) { 552 | isHided = true; 553 | hideAnimation.start(); 554 | showAnimation.cancel(); 555 | } 556 | } 557 | 558 | public void show() { 559 | if (isHided) { 560 | isHided = false; 561 | showAnimation.start(); 562 | hideAnimation.cancel(); 563 | } 564 | } 565 | 566 | public float dp2px(float dp) { 567 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, 568 | getResources().getDisplayMetrics()); 569 | } 570 | 571 | /** 572 | *573 | * 上拉隐藏,下拉显示的动作行为,配合 {@link FloatingMusicMenu} 使用更佳 574 | *
575 | */ 576 | public static class Behavior extends CoordinatorLayout.Behavior
24 | * 可旋转的进度条位图,继承自 {@link Drawable}
25 | * 原理:利用 {@link BitmapShader} 绘制出圆形图案,周围留出空白以便绘制进度条。
26 | *
28 | * 29 | * @author cpacm 30 | *
31 | */ 32 | public class RotatingProgressDrawable extends Drawable { 33 | 34 | private static final int COLORDRAWABLE_DIMENSION = 2; 35 | private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_4444; 36 | private static final int ROTATION_DEFAULT_SPEED = 25; 37 | private Paint mPaint, progressPaint; 38 | private Drawable drawable; 39 | private int mWidth; 40 | private float mRotation; 41 | private RectF rectF; 42 | 43 | private float progress;//进度条 44 | private int progressPercent;//进度条宽度 45 | private int progressColor;//进度条颜色 46 | 47 | // 旋转控制 48 | private RotateHandler rotateHandler; 49 | 50 | public RotatingProgressDrawable(Drawable drawable) { 51 | initDrawable(); 52 | this.drawable = drawable; 53 | circleBitmapFromDrawable(this.drawable); 54 | } 55 | 56 | public RotatingProgressDrawable(Resources res, Bitmap bitmap) { 57 | initDrawable(); 58 | drawable = new BitmapDrawable(res, bitmap); 59 | circleBitmapFromDrawable(drawable); 60 | } 61 | 62 | private void initDrawable() { 63 | progressPercent = 3; 64 | progress = 0f; 65 | progressColor = Color.RED; 66 | 67 | rotateHandler = new RotateHandler(Looper.getMainLooper()); 68 | 69 | rectF = new RectF(); 70 | progressPaint = new Paint(); 71 | progressPaint.setColor(progressColor); 72 | progressPaint.setStyle(Paint.Style.STROKE); 73 | progressPaint.setAntiAlias(true); 74 | } 75 | 76 | 77 | @SuppressWarnings("UnusedDeclaration") 78 | public float getRotation() { 79 | return mRotation; 80 | } 81 | 82 | @SuppressWarnings("UnusedDeclaration") 83 | public void setRotation(float rotation) { 84 | mRotation = rotation; 85 | invalidateSelf(); 86 | } 87 | 88 | @Override 89 | public void draw(Canvas canvas) { 90 | float progressWidth = mWidth * progressPercent / 100f; 91 | float halfWidth = progressWidth / 2; 92 | // 画背景图 93 | canvas.save(); 94 | canvas.rotate(mRotation, getBounds().centerX(), getBounds().centerY()); 95 | float scale = 1 - progressWidth * 2.0f / mWidth; 96 | canvas.scale(scale, scale, mWidth / 2.0f, mWidth / 2.0f); 97 | canvas.drawCircle(mWidth / 2, mWidth / 2, mWidth / 2, mPaint); 98 | canvas.restore(); 99 | // 画进度条 100 | rectF.set(halfWidth, halfWidth, mWidth - halfWidth, mWidth - halfWidth); 101 | canvas.drawArc(rectF, -90, progress, false, progressPaint); 102 | } 103 | 104 | /** 105 | * set progress 106 | * 设置进度 107 | * 108 | * @param progress 0-100 109 | */ 110 | public void setProgress(float progress) { 111 | if (progress < 0 || progress > 100) 112 | return; 113 | progress = progress * 360 / 100f; 114 | this.progress = progress; 115 | invalidateSelf(); 116 | } 117 | 118 | /** 119 | * 设置进度条相对于图片的百分比,默认为3% 120 | * 121 | * @param percent 0-100 122 | */ 123 | public void setProgressWidthPercent(int percent) { 124 | this.progressPercent = percent; 125 | if (mWidth > 0) { 126 | float progressWidth = mWidth * percent / 100f; 127 | progressPaint.setStrokeWidth(progressWidth); 128 | } 129 | invalidateSelf(); 130 | } 131 | 132 | /** 133 | * 设置进度条的颜色 134 | * 135 | * @param progressColor 136 | */ 137 | public void setProgressColor(int progressColor) { 138 | this.progressColor = progressColor; 139 | progressPaint.setColor(progressColor); 140 | invalidateSelf(); 141 | } 142 | 143 | /** 144 | * 是否开始旋转 145 | * 146 | * @param rotate 147 | */ 148 | public void rotate(boolean rotate) { 149 | rotateHandler.removeMessages(0); 150 | if (rotate) { 151 | rotateHandler.sendEmptyMessage(0); 152 | } 153 | } 154 | 155 | /** 156 | * 圆形 157 | */ 158 | private void circleBitmap(Bitmap mBitmap) { 159 | BitmapShader bitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, 160 | Shader.TileMode.CLAMP); 161 | mPaint = new Paint(); 162 | mPaint.setAntiAlias(true); 163 | mPaint.setShader(bitmapShader); 164 | mWidth = Math.min(mBitmap.getWidth(), mBitmap.getHeight()); 165 | float progressWidth = mWidth * progressPercent / 100f; 166 | progressPaint.setStrokeWidth(progressWidth); 167 | } 168 | 169 | private void circleBitmapFromDrawable(Drawable drawable) { 170 | Bitmap mBitmap; 171 | if (drawable instanceof ColorDrawable) { 172 | mBitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, 173 | COLORDRAWABLE_DIMENSION, BITMAP_CONFIG); 174 | } else { 175 | mBitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), 176 | drawable.getIntrinsicHeight(), BITMAP_CONFIG); 177 | } 178 | Canvas canvas = new Canvas(mBitmap); 179 | drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 180 | drawable.draw(canvas); 181 | 182 | circleBitmap(mBitmap); 183 | } 184 | 185 | @Override 186 | public int getIntrinsicWidth() { 187 | return mWidth; 188 | } 189 | 190 | @Override 191 | public int getIntrinsicHeight() { 192 | return mWidth; 193 | } 194 | 195 | @Override 196 | public void setAlpha(int alpha) { 197 | mPaint.setAlpha(alpha); 198 | } 199 | 200 | @Override 201 | public void setColorFilter(ColorFilter cf) { 202 | mPaint.setColorFilter(cf); 203 | } 204 | 205 | @Override 206 | public int getOpacity() { 207 | return PixelFormat.TRANSLUCENT; 208 | } 209 | 210 | private class RotateHandler extends Handler { 211 | 212 | RotateHandler(Looper looper) { 213 | super(looper); 214 | } 215 | 216 | @Override 217 | public void handleMessage(Message msg) { 218 | if (msg.what == 0) { 219 | mRotation = mRotation + 1; 220 | if (mRotation > 360) { 221 | mRotation = 0; 222 | } 223 | setRotation(mRotation); 224 | rotateHandler.sendEmptyMessageDelayed(0, ROTATION_DEFAULT_SPEED); 225 | } 226 | super.handleMessage(msg); 227 | } 228 | 229 | } 230 | 231 | } 232 | -------------------------------------------------------------------------------- /floatingmusicmenu/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 |