安卓实现仿网易云音乐播放界面
想要在安卓上实现类似于网易云的播放界面,工作不算复杂,简单分析一下需要完成的工作:
- 上拉全屏弹窗
Activity
- 毛玻璃背景
- 播放暂停旋转动画
其实也就这些内容,所以实现起来也很简单,下面逐一完成。
1. 上拉全屏弹窗Activity
实际上就是改变一下Activity
的入场和出场动画,额外需要增加的是下拉关闭功能
1.1 出入场动画
只需要调用方法overridePendingTransition()
即可:
//入场
startActivity(new Intent(activity, MusicPlayerActivity.class))
overridePendingTransition(R.anim.slide_down_in, R.anim.slide_down_out)
//出场
finish();
overridePendingTransition(R.anim.slide_up_in, R.anim.slide_up_out);
java
其中用到的四个动画代码文件放置在资源文件夹res/anim
下(非必须,能找到就可以),代码如下:
res/anim/slide_down_in.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300">
<translate
android:fromYDelta="100%"
android:interpolator="@android:anim/decelerate_interpolator"
android:toYDelta="0"/>
</set>
xml
res/anim/slide_down_out.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300">
<translate
android:fromYDelta="0"
android:interpolator="@android:anim/decelerate_interpolator"
android:toYDelta="-100%" />
</set>
xml
res/anim/slide_up_in.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="200">
<translate
android:fromYDelta="-100%"
android:interpolator="@android:anim/decelerate_interpolator"
android:toYDelta="0" />
</set>
xml
res/anim/slide_up_out.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="200">
<translate
android:fromYDelta="0"
android:interpolator="@android:anim/accelerate_interpolator"
android:toYDelta="100%" />
</set>
xml
我们定义一个静态方法show()
用于封装startActivity()
:
public static void show(Activity activity) {
if (activity == null) return;
activity.startActivity(new Intent(activity, MusicPlayerActivity.class));
activity.overridePendingTransition(R.anim.slide_down_in, R.anim.slide_down_out);
}
java
同时重写一下finish()
方法:
@Override
public void finish() {
super.finish();
overridePendingTransition(R.anim.slide_up_in, R.anim.slide_up_out);
}
java
这样其它Activity
想要启动该Activity
只需要调用静态方法show()
即可,关闭的时候正常调用finish
.
1.2 下拉关闭
我们需要重写touchEvent
,根据Action
来决定Activity
的行为:
当按下时,记录Y
方向的坐标信息,为下拉事件判定作准备
当移动时,计算Y
方向坐标变化,然后改变Activity
的位置
当松开/取消时,判断Y
方向向下移动距离是否超过临界距离,超过则关闭Activity
,否则Activity
归位
float touchDownY = 0;
float layoutY = 0;
float closeY = (float) (Resources.getSystem().getDisplayMetrics().heightPixels * 0.3);
boolean isTouched = false;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "onTouchEvent: ACTION_DOWN....");
touchDownY = event.getY();
isTouched = true;
layoutY = mLayout.getY();
break;
case MotionEvent.ACTION_MOVE:
if (isTouched) {
float aimY = layoutY + event.getY() - touchDownY;
if (aimY < 0) aimY = 0;
Log.d(TAG, "onTouchEvent: " + aimY);
mLayout.setY(aimY);
}
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "onTouchEvent: ACTION_UP....");
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, "onTouchEvent: ACTION_CANCEL....");
isTouched = false;
if (mLayout.getY() < closeY) {
ObjectAnimator enterAnim = ObjectAnimator.ofFloat(mLayout, "y", mLayout.getY(), 0);
enterAnim.setDuration(300);
enterAnim.start();
} else {
finish();
}
break;
}
return false;
}
java
1.3 透明通知栏
为了更好的显示效果,需要将通知栏透明:
public void setNavAndStatusBarTransparent(Activity activity) {
if (activity == null) {
return;
}
try {
Window window = activity.getWindow();
if (window != null) {
View decorView = window.getDecorView();
//两个 flag 要结合使用,表示让应用的主体内容占用系统状态栏的空间
int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(option);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);
//导航栏颜色也可以正常设置
window.setNavigationBarColor(Color.TRANSPARENT);
}
} catch (Exception e) {
e.printStackTrace();
}
}
java
然后在OnCreate
里调用:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_music_player);
setNavAndStatusBarTransparent(this);
}
private void initView() {
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.width = WindowManager.LayoutParams.MATCH_PARENT;
lp.height = WindowManager.LayoutParams.MATCH_PARENT;
getWindow().setAttributes(lp);
setNavAndStatusBarTransparent(this);
}
java
1.4 全屏显示
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_music_player);
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.width = WindowManager.LayoutParams.MATCH_PARENT;
lp.height = WindowManager.LayoutParams.MATCH_PARENT;
getWindow().setAttributes(lp);
}
java
我们封装好的Activity完整代码如下:
MusicPlayerActivity.class
package cool.hyz.musicplayer;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.RelativeLayout;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
public class MusicPlayerActivity extends AppCompatActivity {
private static final String TAG = "MusicPlayerActivity";
private RelativeLayout mLayout;
public static void show(Activity activity) {
if (activity == null) return;
activity.startActivity(new Intent(activity, MusicPlayerActivity.class));
activity.overridePendingTransition(R.anim.slide_down_in, R.anim.slide_down_out);
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_music_player);
initView();
}
private void initView() {
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.width = WindowManager.LayoutParams.MATCH_PARENT;
lp.height = WindowManager.LayoutParams.MATCH_PARENT;
getWindow().setAttributes(lp);
setNavAndStatusBarTransparent(this);
mLayout = findViewById(R.id.media_player_layout);
}
public void setNavAndStatusBarTransparent(Activity activity) {
if (activity == null) {
return;
}
try {
Window window = activity.getWindow();
if (window != null) {
View decorView = window.getDecorView();
//两个 flag 要结合使用,表示让应用的主体内容占用系统状态栏的空间
int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(option);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);
//导航栏颜色也可以正常设置
window.setNavigationBarColor(Color.TRANSPARENT);
}
} catch (Exception e) {
e.printStackTrace();
}
}
float touchDownY = 0;
float layoutY = 0;
float closeY = (float) (Resources.getSystem().getDisplayMetrics().heightPixels * 0.3);
boolean isTouched = false;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "onTouchEvent: ACTION_DOWN....");
touchDownY = event.getY();
isTouched = true;
layoutY = mLayout.getY();
break;
case MotionEvent.ACTION_MOVE:
if (isTouched) {
float aimY = layoutY + event.getY() - touchDownY;
if (aimY < 0) aimY = 0;
Log.d(TAG, "onTouchEvent: " + aimY);
mLayout.setY(aimY);
}
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "onTouchEvent: ACTION_UP....");
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, "onTouchEvent: ACTION_CANCEL....");
isTouched = false;
if (mLayout.getY() < closeY) {
ObjectAnimator enterAnim = ObjectAnimator.ofFloat(mLayout, "y", mLayout.getY(), 0);
enterAnim.setDuration(300);
enterAnim.start();
} else {
finish();
}
break;
}
return false;
}
@Override
public void finish() {
super.finish();
overridePendingTransition(R.anim.slide_up_in, R.anim.slide_up_out);
}
}
java
1.5 布局文件
首先准备几个图标,这里我们用vector:
- 播放:
drawable/ic_music_play.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<group
android:rotation="180"
android:translateX="1024"
android:translateY="1024">
<path
android:fillColor="#CD1C1C1C"
android:pathData="M511.3 98.3c-228 0-413.5 185.5-413.5 413.5 0 23.8 2.6 48.1 6.5 70.8 0 0.1 0.2 0.1 0.3 0.2 1.6 5.8 6.7 10.1 13 10.1 7.6 0 13.8-6.2 13.8-13.8 0-0.5-0.2-0.9-0.3-1.3l0.2-0.1c-3.7-21.4-6-43.3-6-65.8 0-213.1 172.8-385.9 385.9-385.9s385.9 172.8 385.9 385.9-172.6 385.9-385.8 385.9c-142.4 0-266.5-77.4-333.4-192.2 0-0.1-0.1-0.1-0.1-0.2l-0.6-0.9c-2.3-4.3-6.7-7.4-12-7.4-7.6 0-13.8 6.2-13.8 13.8 0 3 1.2 5.6 2.8 7.8l-0.2 0.1C225.7 842 358.8 925.2 511.3 925.2c228 0 413.5-185.5 413.5-413.5S739.3 98.3 511.3 98.3z" />
</group>
<path
android:fillColor="#CD1C1C1C"
android:pathData="M432.1 672.1c-7.6 0-13.8-6.2-13.8-13.8V365.4c0-7.6 6.2-13.8 13.8-13.8 7.6 0 13.8 6.2 13.8 13.8v292.9c0 7.6-6.2 13.8-13.8 13.8zM590.6 672.1c-7.6 0-13.8-6.2-13.8-13.8V365.4c0-7.6 6.2-13.8 13.8-13.8 7.6 0 13.8 6.2 13.8 13.8v292.9c0 7.6-6.2 13.8-13.8 13.8z" />
<path
android:fillColor="#CD1C1C1C"
android:pathData="m886,387c-7.18232,0 -13,-5.81768 -13,-13c0,-7.18232 5.81768,-13 13,-13c7.18232,0 13,5.81768 13,13c0,7.18232 -5.81768,13 -13,13z" />
</vector>
xml
- 暂停:
drawable/ic_music_pause.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#CD1C1C1C"
android:pathData="M511.3 98.3c-228 0-413.5 185.5-413.5 413.5 0 23.8 2.6 48.1 6.5 70.8 0 0.1 0.2 0.1 0.3 0.2 1.6 5.8 6.7 10.1 13 10.1 7.6 0 13.8-6.2 13.8-13.8 0-0.5-0.2-0.9-0.3-1.3l0.2-0.1c-3.7-21.4-6-43.3-6-65.8 0-213.1 172.8-385.9 385.9-385.9s385.9 172.8 385.9 385.9-172.6 385.9-385.8 385.9c-142.4 0-266.5-77.4-333.4-192.2 0-0.1-0.1-0.1-0.1-0.2l-0.6-0.9c-2.3-4.3-6.7-7.4-12-7.4-7.6 0-13.8 6.2-13.8 13.8 0 3 1.2 5.6 2.8 7.8l-0.2 0.1C225.7 842 358.8 925.2 511.3 925.2c228 0 413.5-185.5 413.5-413.5S739.3 98.3 511.3 98.3z" />
<path
android:fillColor="#CD1C1C1C"
android:pathData="M446.9 659.6c-7 0-14.1-1.8-20.5-5.5-12.8-7.3-20.6-20.5-20.9-35.2L402 406.1c-0.2-14.9 7.3-28.5 20.1-36.2 12.8-7.7 28.3-7.9 41.3-0.7l191.3 106.4c13.2 7.4 21.2 20.8 21.3 36 0.1 15.1-7.8 28.6-21 36.1L467.3 654.2c-6.4 3.6-13.4 5.4-20.4 5.4z m-17.4-254l3.6 212.8c0.1 7.1 4.9 10.6 7 11.7 2 1.2 7.5 3.6 13.6 0l187.7-106.4c6.3-3.6 7-9.6 7-12.1 0-2.4-0.7-8.5-7.1-12L450 393.3c-6.2-3.4-11.7-1-13.8 0.2-2 1.3-6.8 4.9-6.7 12.1z" />
<group
android:rotation="180"
android:translateX="1024"
android:translateY="1024">
<path
android:fillColor="#CD1C1C1C"
android:pathData="m886,387c-7.18232,0 -13,-5.81768 -13,-13c0,-7.18232 5.81768,-13 13,-13c7.18232,0 13,5.81768 13,13c0,7.18232 -5.81768,13 -13,13z" />
</group>
</vector>
xml
- 上一曲:
drawable/ic_music_pre.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#CD1C1C1C"
android:pathData="M356.77 605.22L701.8 849.53c69.8 49.42 166.29-0.49 166.29-86.01V261.25c0-85.52-96.49-135.43-166.29-86.01L356.77 419.55c-64.02 45.34-64.02 140.34 0 185.67zM210.42 154.36c30.38 0 55 24.62 55 55v606.07c0 30.38-24.62 55-55 55s-55-24.62-55-55V209.36c0-30.38 24.62-55 55-55z" />
</vector>
xml
- 下一曲:
drawable/ic_music_next.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#CD1C1C1C"
android:pathData="M665.47 417.65l-345.03-244.3c-69.8-49.42-166.29 0.49-166.29 86.01v502.27c0 85.52 96.49 135.43 166.29 86.01l345.03-244.31c64.02-45.34 64.02-140.34 0-185.68zM811.82 868.52c-30.38 0-55-24.62-55-55V207.46c0-30.38 24.62-55 55-55s55 24.62 55 55v606.07c0 30.37-24.62 54.99-55 54.99z" />
</vector>
xml
- 随机播放:
drawable/ic_music_random.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#CD1C1C1C"
android:pathData="M747.093333 596.48a21.333333 21.333333 0 0 1 12.586667 4.096l133.888 97.792a21.333333 21.333333 0 0 1-0.106667 34.56l-133.909333 96.405333a21.333333 21.333333 0 0 1-33.813333-17.322666v-53.482667c-82.858667-0.789333-133.397333-24.896-178.709334-81.088 16.810667-24.192 32.597333-51.029333 47.808-80.554667 36.032 56.213333 68.885333 75.690667 130.901334 76.288V617.813333a21.333333 21.333333 0 0 1 21.333333-21.333333zM746.88 169.472a21.333333 21.333333 0 0 1 12.458667 4.010667l134.058666 96.298666a21.333333 21.333333 0 0 1 0.149334 34.56l-134.08 98.026667a21.333333 21.333333 0 0 1-33.92-17.237333v-55.296c-86.592 0.789333-118.784 37.824-175.381334 161.365333-6.357333 13.930667-9.749333 21.333333-12.906666 28.16-75.946667 164.16-153.642667 236.608-323.541334 239.232H128v-85.76h85.077333c129.152-1.984 182.784-51.989333 246.4-189.482667 3.072-6.656 6.357333-13.866667 12.757334-27.861333 69.141333-150.912 122.88-210.197333 253.333333-211.413333V190.805333a21.333333 21.333333 0 0 1 21.333333-21.333333zM213.76 244.074667c102.442667 1.578667 171.370667 28.544 226.602667 85.717333a981.12 981.12 0 0 0-42.432 82.133333c-44.693333-57.450667-97.493333-80.746667-184.832-82.112H128v-85.76h85.738667z" />
</vector>
xml
- 循环播放:
drawable/ic_music_loop.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#CD1C1C1C"
android:pathData="M203 768a31.85 31.85 0 0 1-20.11-7.13A320 320 0 0 1 384 192h210.75l-9.38-9.37a32 32 0 0 1 45.26-45.26l64 64a32 32 0 0 1 0 45.26l-64 64a32 32 0 0 1-45.26-45.26l9.38-9.37H384c-141.16 0-256 114.84-256 256a255.07 255.07 0 0 0 95.13 199.15A32 32 0 0 1 203 768z m235.63 118.63a32 32 0 0 0 0-45.26l-9.38-9.37H640a320 320 0 0 0 201.12-568.91 32 32 0 1 0-40.25 49.76A255.07 255.07 0 0 1 896 512c0 141.16-114.84 256-256 256H429.25l9.38-9.37a32 32 0 0 0-45.26-45.26l-64 64a32 32 0 0 0 0 45.26l64 64a32 32 0 0 0 45.26 0zM448 544a32 32 0 1 1-32-32 32 32 0 0 1 32 32z m96-32a32 32 0 1 0 32 32 32 32 0 0 0-32-32z m128 0a32 32 0 1 0 32 32 32 32 0 0 0-32-32z"
/></vector>
xml
- 单曲循环播放:
drawable/ic_music_loop_single.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#CD1C1C1C"
android:pathData=
"M361.5 727.8c-119.1 0-215.9-96.9-215.9-215.9 0-119.1 96.9-215.9 215.9-215.9 2.3 0 4.6-0.2 6.8-0.6v58.3c0 12.3 14 19.4 23.9 12.1l132.6-97.6c8.1-6 8.1-18.2 0-24.2l-132.6-97.6c-9.9-7.3-23.9-0.2-23.9 12.1v58.1c-2.2-0.4-4.5-0.6-6.8-0.6-39.8 0-78.5 7.9-115 23.4-35.2 15-66.8 36.3-94 63.5s-48.6 58.8-63.5 94c-15.5 36.5-23.4 75.2-23.4 115s7.9 78.5 23.4 115c15 35.2 36.3 66.8 63.5 94s58.8 48.6 94 63.5c36.5 15.5 75.2 23.4 115 23.4 22.1 0 40-17.9 40-40s-17.9-40-40-40zM938.2 396.9c-15-35.2-36.3-66.8-63.5-94s-58.8-48.6-94-63.5c-36.5-15.5-75.2-23.4-115-23.4-22.1 0-40 17.9-40 40s17.9 40 40 40c119.1 0 215.9 96.9 215.9 215.9 0 119.1-96.9 215.9-215.9 215.9-4.1 0-8.1 0.6-11.8 1.8v-60.8c0-12.3-14-19.4-23.9-12.1l-132.6 97.6c-8.1 6-8.1 18.2 0 24.2L629.9 876c9.9 7.3 23.9 0.2 23.9-12.1V806c3.7 1.2 7.7 1.8 11.8 1.8 39.8 0 78.5-7.9 115-23.4 35.2-15 66.8-36.3 94-63.5s48.6-58.8 63.5-94c15.5-36.5 23.4-75.2 23.4-115s-7.8-78.5-23.3-115z"
/>
<path
android:fillColor="#CD1C1C1C"
android:pathData=
"M512.8 660.6c22.1-0.1 39.9-18.1 39.8-40.2l-1.2-214.1c-0.1-22-18-39.8-40-39.8h-0.2c-22.1 0.1-39.9 18.1-39.8 40.2l1.2 214.1c0.1 22 18 39.8 40 39.8h0.2z"
/>
</vector>
xml
- 顺序播放:
drawable/ic_music_in_order.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#CD1C1C1C"
android:pathData="M560.70379009 320.16991061H198.25332639c-21.68104205 0-39.27725009-17.59620803-39.27725009-39.27725007s17.59620803-39.27725009 39.27725009-39.27725005H560.70379009c21.68104205 0 39.27725009 17.59620803 39.27725007 39.27725005s-17.59620803 39.27725009-39.27725007 39.27725007zM560.70379009 555.67630209H198.25332639c-21.68104205 0-39.27725009-17.59620803-39.27725009-39.27725008s17.59620803-39.27725009 39.27725009-39.27725008H560.70379009c21.68104205 0 39.27725009 17.59620803 39.27725007 39.27725008-0.15710901 21.68104205-17.59620803 39.27725009-39.27725007 39.27725008zM560.70379009 791.02558454H198.25332639c-21.68104205 0-39.27725009-17.59620803-39.27725009-39.27725007s17.59620803-39.27725009 39.27725009-39.27725006H560.70379009c21.68104205 0 39.27725009 17.59620803 39.27725007 39.27725006s-17.59620803 39.27725009-39.27725007 39.27725007zM721.42629742 826.21800061c-6.441469 0-13.04004703-1.57108999-18.85308003-4.87037899-12.56872002-6.91279601-20.26706103-20.10995203-20.26706105-34.40687108V245.07180847c0-21.68104205 17.59620803-39.27725009 39.27725007-39.27725008s39.27725009 17.59620803 39.27725008 39.27725008v470.69856494l48.54668111-30.63625506c18.22464404-11.46895703 42.57653909-6.127251 54.20260509 12.25450201 11.62606602 18.38175303 6.127251 42.57653909-12.25450202 54.20260511l-108.71942822 68.65663314c-6.59857803 3.92772502-13.98270102 5.97014202-21.20971503 5.970142z"
/></vector>
xml
- 播放列表:
drawable/ic_music_list.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#CD1C1C1C"
android:pathData="M221.58 319.89h-48c-35.9 0-65-29.1-65-65s29.1-65 65-65h48c35.9 0 65 29.1 65 65s-29.1 65-65 65zM718.58 319.89h-322c-35.9 0-65-29.1-65-65s29.1-65 65-65h322c35.9 0 65 29.1 65 65s-29.1 65-65 65zM407.58 579.89h-234c-35.9 0-65-29.1-65-65s29.1-65 65-65h234c35.9 0 65 29.1 65 65s-29.1 65-65 65zM256.58 838.89h-83c-35.9 0-65-29.1-65-65s29.1-65 65-65h83c35.9 0 65 29.1 65 65s-29.1 65-65 65zM429.58 838.89h-1c-35.9 0-65-29.1-65-65s29.1-65 65-65h1c35.9 0 65 29.1 65 65s-29.1 65-65 65zM713.96 396.72c-122.04 0-220.97 98.93-220.97 220.97s98.93 220.97 220.97 220.97 220.97-98.93 220.97-220.97S836 396.72 713.96 396.72zM837.9 561.91L689.88 715.26a30.078 30.078 0 0 1-21.38 9.19h-0.27c-7.97 0-15.63-3.17-21.27-8.81l-56.57-56.57c-11.75-11.75-11.75-30.79 0-42.54s30.79-11.75 42.54 0l34.92 34.91 126.76-131.32c11.54-11.96 30.58-12.29 42.54-0.75 11.95 11.54 12.29 30.58 0.75 42.54z" />
</vector>
xml
以及两个图片文件
-
drawable/icon_music_player_bar.webp
-
drawable/icon_music_player_pan.webp
需要的只需要右键图片然后保存图片复制到资源目录res/drawable
下就可以了
完整的布局文件代码如下:
layout/activity_music_player.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/media_player_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<ImageView
android:id="@+id/song_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:scaleX="1.5"
android:scaleY="1.5" />
<LinearLayout
android:id="@+id/player_tab"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_alignParentTop="true"
android:background="#90707070"
android:paddingTop="40dp">
<View
android:layout_width="0dp"
android:layout_height="2dp"
android:layout_weight="1.5" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="详情"
android:textAlignment="center"
android:textColor="#E8E8E8" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="|"
android:textAlignment="center" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="歌曲"
android:textAlignment="center"
android:textColor="@color/white" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="|"
android:textAlignment="center" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="歌词"
android:textAlignment="center"
android:textColor="#E8E8E8" />
<View
android:layout_width="0dp"
android:layout_height="2dp"
android:layout_weight="1.5" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/player_tab"
android:background="#90707070"
android:orientation="vertical"
android:padding="30dp">
<RelativeLayout
android:id="@+id/player"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="26dp">
<ImageView
android:id="@+id/song_cover"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_centerInParent="true"
android:scaleType="centerCrop"
android:src="@mipmap/ic_launcher" />
<ImageView
android:layout_width="320dp"
android:layout_height="320dp"
android:layout_centerInParent="true"
android:scaleType="centerCrop"
android:src="@drawable/icon_music_player_pan" />
</RelativeLayout>
<TextView
android:id="@+id/song_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="续加仪"
android:textAlignment="center"
android:textColor="@color/white"
android:textSize="25sp" />
<TextView
android:id="@+id/song_singer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="续加仪"
android:textAlignment="center"
android:textColor="#E8E8E8"
android:textSize="15sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center"
android:orientation="vertical"
android:paddingStart="30dp"
android:paddingEnd="30dp">
<SeekBar
android:id="@+id/song_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:progress="0"
android:progressTint="@color/white"
android:thumbTint="@color/white" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingStart="15dp"
android:paddingEnd="15dp"
android:orientation="horizontal">
<TextView
android:id="@+id/song_current"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="00:00"
android:textAlignment="center"
android:textColor="#E8E8E8"
android:textSize="12sp" />
<View
android:layout_width="0dp"
android:layout_height="2dp"
android:layout_weight="1" />
<TextView
android:id="@+id/song_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="05:30"
android:textAlignment="center"
android:textColor="#E8E8E8"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<View
android:layout_width="0dp"
android:layout_height="2dp"
android:layout_weight="1" />
<ImageView
android:id="@+id/song_play_mode"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_music_random"
app:tint="@color/selector_color_music_icon_button_white" />
<View
android:layout_width="0dp"
android:layout_height="2dp"
android:layout_weight="1" />
<ImageView
android:id="@+id/song_pre"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_music_pre"
app:tint="@color/selector_color_music_icon_button_white" />
<View
android:layout_width="0dp"
android:layout_height="2dp"
android:layout_weight="1" />
<ImageView
android:id="@+id/song_play_or_pause"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_music_pause"
app:tint="@color/selector_color_music_icon_button_white" />
<View
android:layout_width="0dp"
android:layout_height="2dp"
android:layout_weight="1" />
<ImageView
android:id="@+id/song_next"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_music_next"
app:tint="@color/selector_color_music_icon_button_white" />
<View
android:layout_width="0dp"
android:layout_height="2dp"
android:layout_weight="1" />
<ImageView
android:id="@+id/song_play_list"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_music_list"
app:tint="@color/selector_color_music_icon_button_white" />
<View
android:layout_width="0dp"
android:layout_height="2dp"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>
<ImageView
android:id="@+id/player_bar"
android:layout_width="180dp"
android:layout_height="wrap_content"
android:layout_below="@id/player_tab"
android:layout_centerHorizontal="true"
android:layout_marginTop="-19.5dp"
android:adjustViewBounds="true"
android:rotation="-30"
android:src="@drawable/icon_music_player_bar"
android:transformPivotX="90dp"
android:transformPivotY="19dp" />
</RelativeLayout>
xml
效果:
1.6 主题设置
该实现效果会出现上拉的Activity把前一个Activity顶上去的问题,需要配置主题:
res/values/themes.xml
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="FullScreenActivity" parent="Theme.AppCompat.Light.NoActionBar">
<!--设置Activity的背景-->
<item name="android:windowBackground">@android:color/transparent</item>
<!--设置Activity的windowFrame框为无-->
<item name="android:windowFrame">@null</item>
<!--设置无标题-->
<item name="android:windowNoTitle">true</item>
<!--是否浮现在activity之上-->
<item name="android:windowIsFloating">true</item>
<!--设置窗口内容不覆盖-->
<item name="android:windowContentOverlay">@null</item>
<!--背景是否模糊显示-->
<item name="android:backgroundDimEnabled">false</item>
</style>
</resources>
xml
并在Manifest中配置:
AndroidManifest.xml
<activity android:name="cool.hyz.musicplayer.MusicPlayerActivity" android:theme="@style/FullScreenActivity"/>
xml
2. 毛玻璃背景
先放一张需要用到的素材
drawable/lover_cover.png
需要的可以右键保存复制到资源目录res/drawable
下
毛玻璃背景很好实现,只需要使用Gilde自带的功能即可。
其中,Glide的依赖为:
//imageLoader
implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation 'jp.wasabeef:glide-transformations:4.3.0'
然后实现方式为:
Glide.with(this).load(R.drawable.lover_cover)
.apply(RequestOptions.bitmapTransform(new BlurTransformation(25, 3)))
.into(mBackground);
java
3.播放暂停旋转动画
播放暂停旋转动画是一种仿留声机的唱片旋转动画,只需要完成两个动作:
- 唱片的旋转
- 唱头的摆动
首先我们先给音乐的基本信息显示出来,包括歌手、歌名、封面,这里需要圆角图片,同样使用Glide完成:
mTitle.setText("恋人");
mSinger.setText("李荣浩");
Glide.with(this).load(R.drawable.lover_cover)
.apply(RequestOptions.circleCropTransform())
.into(mCover);
java
3.1 唱片的旋转
我们需要在播放的时候,唱片顺时针无限循环旋转;暂停的时候停止旋转,但角度保持;继续播放的时候不归位而继续旋转
RelativeLayout mPlayer = findViewById(R.id.player);
MediaPlayer mMediaPlayer;
ObjectAnimator mPlayerRotationAnim;
if (mPlayerRotationAnim != null) {
mPlayerRotationAnim.pause();
}
if (mMediaPlayer.isPlaying()) {
mPlayerBar.clearAnimation();
mPlayerBar.setAnimation(mPlayerBarRotationAnimEnter);
if (mPlayerRotationAnim == null) {
mPlayerRotationAnim = ObjectAnimator.ofFloat(mPlayer, "rotation", mPlayer.getRotation(), 360.0f);
mPlayerRotationAnim.setDuration(5000);
mPlayerRotationAnim.setRepeatCount(Animation.INFINITE);
mPlayerRotationAnim.setInterpolator(new LinearInterpolator());
mPlayerRotationAnim.setRepeatMode(ObjectAnimator.RESTART);// 循环模式
mPlayerRotationAnim.start();
} else
mPlayerRotationAnim.resume();
}
java
3.2 唱头的摆动
我们需要在播放的时候,唱头位于唱片之上;暂停的时候远离唱片;中间过程为摆动的动画
这里使用RotateAnimation
ImageView mPlayerBar = findViewById(R.id.player_bar);
MediaPlayer mMediaPlayer;
RotateAnimation mPlayerBarRotationAnimEnter;
RotateAnimation mPlayerBarRotationAnimExit;
if (mPlayerBarRotationAnimEnter != null) mPlayerBarRotationAnimEnter.reset();
mPlayerBarRotationAnimEnter = new RotateAnimation(0.0f, 25.0f, mPlayerBar.getPivotX(), mPlayerBar.getPivotY());
mPlayerBarRotationAnimEnter.setInterpolator(new LinearInterpolator());
mPlayerBarRotationAnimEnter.setFillEnabled(true);
mPlayerBarRotationAnimEnter.setFillAfter(true);
mPlayerBarRotationAnimEnter.setDuration(300);
if (mPlayerBarRotationAnimExit != null) mPlayerBarRotationAnimExit .reset();
mPlayerBarRotationAnimExit = new RotateAnimation(25.0f, 0.f, mPlayerBar.getPivotX(), mPlayerBar.getPivotY());
mPlayerBarRotationAnimExit.setInterpolator(new LinearInterpolator());
mPlayerBarRotationAnimExit.setFillEnabled(true);
mPlayerBarRotationAnimExit.setFillAfter(true);
mPlayerBarRotationAnimExit.setDuration(300);
if (mMediaPlayer.isPlaying()) {
mPlayerBar.clearAnimation();
mPlayerBar.setAnimation(mPlayerBarRotationAnimEnter);
} else {
mPlayerBar.clearAnimation();
mPlayerBar.setAnimation(mPlayerBarRotationAnimExit);
}
java
将上面的内容封装为一个方法initAnim()
,在播放/暂停时调用即可:
private ObjectAnimator mPlayerRotationAnim;
private RotateAnimation mPlayerBarRotationAnimEnter;
private RotateAnimation mPlayerBarRotationAnimExit;
private void initAnim() {
if (mPlayerRotationAnim != null) {
mPlayerRotationAnim.pause();
}
if (mPlayerBarRotationAnimEnter != null) mPlayerBarRotationAnimEnter.reset();
mPlayerBarRotationAnimEnter = new RotateAnimation(0.0f, 25.0f, mPlayerBar.getPivotX(),
mPlayerBar.getPivotY());
mPlayerBarRotationAnimEnter.setInterpolator(new LinearInterpolator());
mPlayerBarRotationAnimEnter.setFillEnabled(true);
mPlayerBarRotationAnimEnter.setFillAfter(true);
mPlayerBarRotationAnimEnter.setDuration(300);
if (mPlayerBarRotationAnimExit != null) mPlayerBarRotationAnimExit .reset();
mPlayerBarRotationAnimExit = new RotateAnimation(25.0f, 0.f, mPlayerBar.getPivotX(),
mPlayerBar.getPivotY());
mPlayerBarRotationAnimExit.setInterpolator(new LinearInterpolator());
mPlayerBarRotationAnimExit.setFillEnabled(true);
mPlayerBarRotationAnimExit.setFillAfter(true);
mPlayerBarRotationAnimExit.setDuration(300);
if (mMediaPlayer.isPlaying()) {
mPlayerBar.clearAnimation();
mPlayerBar.setAnimation(mPlayerBarRotationAnimEnter);
if (mPlayerRotationAnim == null) {
mPlayerRotationAnim = ObjectAnimator.ofFloat(mPlayer, "rotation", mPlayer.getRotation(), 360.0f);
mPlayerRotationAnim.setDuration(5000);
mPlayerRotationAnim.setRepeatCount(Animation.INFINITE);
mPlayerRotationAnim.setInterpolator(new LinearInterpolator());
mPlayerRotationAnim.setRepeatMode(ObjectAnimator.RESTART);// 循环模式
mPlayerRotationAnim.start();
} else
mPlayerRotationAnim.resume();
} else {
mPlayerBar.clearAnimation();
mPlayerBar.setAnimation(mPlayerBarRotationAnimExit);
}
}
java
4. 音乐播放
4.1 MediaPlayer初始化
使用MediaPlayer
完成播放功能
MediaPlayer mMediaPlayer;
...
if (mMediaPlayer == null) {
mMediaPlayer = MediaPlayer.create(this, R.raw.ronghaoli_lover);
}
java
这里用到的音频文件如下,右键保存复制到资源文件res/raw
下面即可:
恋人-李荣浩
4.2 进度更新
需要用到Timer
定时器:
private Timer timer;
private SeekBar mProgress;//需要初始化
...
private void initTimer() {
boolean playing = mMediaPlayer.isPlaying();
if (timer != null) timer.cancel();
if (!playing) return;
timer = new Timer();
mProgress.setMax(mMediaPlayer.getDuration());
mDuration.setText(DateUtils.formatTime(mMediaPlayer.getDuration()));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mProgress.setMin(0);
}
mProgress.setProgress(mMediaPlayer.getCurrentPosition());
BaseApplication.getHandler().post(() -> {
timer.schedule(new TimerTask() {
@Override
public void run() {
MusicPlayerActivity.this.runOnUiThread(() -> {
long currentPosition = mMediaPlayer.getCurrentPosition();
mProgress.setProgress((int) currentPosition, true);
mCurrent.setText(DateUtils.formatTime(currentPosition));
});
}
}, 0, 1000);
});
}
java
其中用到的事件格式化工具类:
package cool.hyz.musicplayer.utils;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
/**
* @author xujiayi
* @date 2024/7/11
* 我只是个自由的主!
*/
public class DateUtils {
public static String getDateStr(String time) {
Date date = null;
try {
date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse(time);
} catch (Exception ignored) {}
if (date == null) return "最近";
Calendar calendar= Calendar.getInstance();
calendar.setTime(date);
long now = new Date().getTime() / 1000;
long dTime = now - date.getTime() / 1000;
if (dTime < 10) return "刚刚";
else if (dTime < 60) return dTime + "秒前";
else if (dTime < 60 * 60) return (dTime / 60) + "分钟前";
else if (dTime < 60 * 60 * 24) return (dTime / 60 / 60) + "小时前";
else if (dTime < 60 * 60 * 24 * 10) return (dTime / 60 / 60 / 24) + "天前";
else return calendar.get(Calendar.YEAR) + "年" + (calendar.get(Calendar.MONTH) + 1) + "月" +calendar.get(Calendar.DATE) + "日";
}
public static Date getDate(String time) {
Date date = null;
try {
date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse(time);
} catch (Exception ignored) {}
return date;
}
public static String formatTime(double time) {
String min = ((int) time / 1000 / 60) + "";
min = (min.length() == 1 ? "0" : "") + min;
String sec = ((int) time / 1000 % 60) + "";
sec = (sec.length() == 1 ? "0" : "") + sec;
return min + ":" + sec;
}
}
java
以及BaseApplication
类:
package cool.hyz.musicplayer.base;
import android.app.Application;
import android.os.Handler;
/**
* @author xujiayi
* @date 2024/7/11
* 我只是个自由的主!
*/
public class BaseApplication extends Application {
private static BaseApplication sInstance;
public static BaseApplication getInstance() {
return sInstance;
}
private static Handler sHandler;
public static Handler getHandler(){
if (sHandler ==null) {
synchronized (Handler.class){
if(sHandler==null){
sHandler = new Handler();
}
}
}
return sHandler;
}
@Override
public void onCreate() {
super.onCreate();
sInstance = this;
}
}
java
同时在AndroidManifest
中添加:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".base.BaseApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/Theme.Musicplayer"
tools:targetApi="31" >
<activity android:name="cool.hyz.musicplayer.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name="cool.hyz.musicplayer.MusicPlayerActivity" android:theme="@style/FullScreenActivity"/>
</application>
</manifest>
xml
注意需要在Activity
销毁时,结束定时任务:
@Override
protected void onDestroy() {
super.onDestroy();
if (mMediaPlayer != null) {
mMediaPlayer.stop();
mMediaPlayer.release();
if (timer != null) {
timer.cancel();
}
}
}
java
项目地址
Github : Music Player Activity