Android音乐浮窗播放器

您所在的位置:网站首页 安桌悬浮窗 Android音乐浮窗播放器

Android音乐浮窗播放器

2023-08-01 07:56| 来源: 网络整理| 查看: 265

这是来到公司的第一个小项目。做一个音乐播放器,内容如下

Float Music(浮窗音乐播放器) 考点: 服务, 音乐播放, 浮窗界面, 列表界面, 自定义view, 手势操作 功能描述: 1.音乐列表(主界面) (1)音乐列表从系统媒体库中查询, 按音频媒体库中的添加时间排列 (2)音乐列表的每一项显示歌曲名及播放总时长 (3)点击列表中的一项即开始播放点击的歌曲, 歌曲使用服务在后台播放, 在播放时显示浮动播放控制器(见功能点2)

这里写图片描述

图1.1

2.浮动音乐播放控制器 (1)音乐播放控制器以浮窗的形式显示, 音乐播放时一直显示 (2)以自定义View的方式实现一个圈形进度显示, 根据播放的当前进度显示相应的弧度 (3)控制器中心显示应用的启动图标, 音乐播放时慢慢旋转 (4)点击控制器时切换播放与暂停, 歌曲单曲循环形式播放 (5)浮动控制器可以随手势拖动到屏幕任意位置, 但必须在屏幕内完整显示 (6)长按(>=3s)播放控制器停止播放, 关闭播放服务且关闭浮窗 这里写图片描述

最终,实现效果如下: 这里写图片描述

先附上项目的流程图,便于大家理解。 这里写图片描述

接着附上项目的UML图。 这里写图片描述

这里写图片描述

这里写图片描述

为了实现以上效果,首先,我们肯定是要先获取手机中的所有音乐。那么,我们可以通过Content Provider获取。 首先我们获取手机的跟路径,然后查找该路径下的音乐。

public class MusicUtil { public static String getBaseDir(){ String dir = null; if(!Environment.getExternalStorageState().equals(Environment.MEDIA_UNKNOWN)){ dir=Environment.getExternalStorageDirectory() + File.separator; }else { dir = App.sContext.getFilesDir() + File.separator; } return dir; } }

然后,创建一个music的bean类,参数如下,省略get、set方法

public class Music { private int id; // 音乐id private String title; // 音乐标题 private String album;//专辑名称 private String uri; // 音乐路径 private int duration; // 时长 private int size;//大小 private String year;//发行时间 private String image; // icon private String artist; // 艺术家 }

接着通过ContentProvider获取音乐,并按添加时间排序。

public static ArrayList queryMusic(String dirName){ //select * from xx where data like dirName order by modify_time ArrayList musicList = new ArrayList(); Cursor cursor = App.sContext.getContentResolver().query( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,null, MediaStore.Audio.Media.DATA + " like ?", new String[]{dirName + "%"}, "date_modified"+" desc"); if (cursor == null) return musicList; Music music; for (cursor.moveToFirst();!cursor.isAfterLast();cursor.moveToNext()){ // 如果不是音乐 String isMusic = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.IS_MUSIC)); if (isMusic != null && isMusic.equals("")) continue; String title = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)); String artist = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)); if(isRepeat(title, artist)) continue; music = new Music(); music.setId(cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID))); music.setTitle(title); music.setArtist(artist); music.setUri(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA))); music.setDuration(cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION))); music.setYear(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.YEAR))); music.setSize(cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE))); music.setAlbum(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM))); music.setImage(getAlbumImage(cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)))); music.setDuration(music.getDuration()==0?1:music.getDuration()); musicList.add(music); } cursor.close(); return musicList; }

通过上述代码,我们可以获取的到手机上的音乐列表。 那么,接下去,我们便要创建一个播放的service了。主要功能如下: 1.初始化音乐列表,通过MusicUtil.initMusiclist();获取音乐列表 2.创建电源锁,防止锁屏服务被关闭 3.创建MediaPlayer对象,并添加相关监听事件(播放进度,换曲监听,上下曲点击、暂停、播放) 4.创建notification,创建广播对对应按钮进行事件监听 5.创建线程mPublishProgressRunnable,对播放进度进行广播

public class PlayService extends Service implements MediaPlayer.OnCompletionListener{ ............ @Override public void onCreate() { super.onCreate(); acquireWakeLock(); MusicUtil.initMusicList(); mPlayingPosition = (int) SpUtils.get(this, Constants.PLAY_POS,0); try { Uri uri = Uri.parse(MusicUtil.sMusicList.get( mPlayingPosition).getUri()); mMediaPlayer = MediaPlayer.create(PlayService.this, uri); mMediaPlayer.setOnCompletionListener(this); mProgressUpdatedListener.execute(mPublishProgressRunnable); PendingIntent pendingIntent = PendingIntent .getActivity(PlayService.this, 0, new Intent(PlayService.this, PlayActivity.class), 0); remoteViews = new RemoteViews(getPackageName(), R.layout.play_notification); notification = new Notification(R.drawable.ic_audiotrack_red_300_48dp, R.string.palying_music + "", System.currentTimeMillis()); notification.contentIntent = pendingIntent; notification.contentView = remoteViews; //一直存在 notification.flags = Notification.FLAG_ONGOING_EVENT; Intent intent = new Intent(PlayService.class.getSimpleName()); intent.putExtra(Constants.BUTTON_NOTI, PRE); PendingIntent preIntent = PendingIntent.getBroadcast( PlayService.this, PRE, intent, PendingIntent.FLAG_UPDATE_CURRENT); remoteViews.setOnClickPendingIntent( R.id.music_play_pre, preIntent ); intent.putExtra(Constants.BUTTON_NOTI, PAUSE); PendingIntent pauseIntent = PendingIntent.getBroadcast( PlayService.this, PAUSE, intent, PendingIntent.FLAG_UPDATE_CURRENT); remoteViews.setOnClickPendingIntent( R.id.music_play_pause, pauseIntent); intent.putExtra(Constants.BUTTON_NOTI, NEXT); PendingIntent nextIntent = PendingIntent.getBroadcast( PlayService.this, NEXT, intent, PendingIntent.FLAG_UPDATE_CURRENT); remoteViews.setOnClickPendingIntent( R.id.music_play_next, nextIntent); intent.putExtra(Constants.BUTTON_NOTI, EXIT); PendingIntent exit = PendingIntent.getBroadcast( PlayService.this, EXIT, intent, PendingIntent.FLAG_UPDATE_CURRENT); remoteViews.setOnClickPendingIntent( R.id.music_play_notifi_exit, exit); notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); setRemoteViews(); /** * 注册广播接收者 * 功能: * 监听通知栏按钮点击事件 */ IntentFilter filter = new IntentFilter( PlayService.class.getSimpleName()); PlayBroadCastReceiver receiver = new PlayBroadCastReceiver(); registerReceiver(receiver, filter); }catch (Exception e){ e.printStackTrace(); } } .......... }

那么,大概的播放服务类已经完成了。接下去就是浮窗的创建了。 由于浮窗的特殊性,我们需要创建第二个service类。

public class FxService extends Service { LinearLayout mFloatLayout; WindowManager.LayoutParams mWparams; WindowManager mWindowManager; RotateView mRotateView; private FloatingViewClickListener mListener; private int pointDownX; private int pointDownY; private int pointUpX; private int pointUpY; private long pointDownTime; private long lastDownTime; public final static int DISTANCE = 15; public final static int LONG_CLICK_TIME = 3000; private ProgressRecevier mReceiver; private void createFloatView() { mWparams = new WindowManager.LayoutParams(); mWindowManager = (WindowManager) getApplication().getSystemService(getApplication().WINDOW_SERVICE); mWparams.type = WindowManager.LayoutParams.TYPE_PHONE; mWparams.format = PixelFormat.RGBA_8888; mWparams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; mWparams.gravity = Gravity.LEFT|Gravity.TOP; mWparams.x = 0; mWparams.y = 0; mWparams.width = WindowManager.LayoutParams.WRAP_CONTENT; mWparams.height = WindowManager.LayoutParams.WRAP_CONTENT; LayoutInflater inflater = LayoutInflater.from(getApplication()); mFloatLayout = (LinearLayout) inflater.inflate(R.layout.activity_play,null,false); mWindowManager.addView(mFloatLayout,mWparams); mRotateView = (RotateView) mFloatLayout.findViewById(R.id.cdView); Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher); mRotateView.setCdImage(bmp,0.3); //mRotateView.setRingWidth((float) (App.sScreenWidth*0.3-15)); mRotateView.startRoll(); //mRotateView.setOnTouchListener(this); mRotateView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { //getRawX是触摸位置相对于屏幕的坐标,getX是相对于按钮的坐标 mWparams.x = (int) event.getRawX() - mRotateView.getMeasuredWidth()/2; mWparams.y = (int) event.getRawY() - mRotateView.getMeasuredHeight()/2 - 25; //刷新 mWindowManager.updateViewLayout(mFloatLayout, mWparams); int x = (int) event.getRawX(); int y = (int) event.getRawY(); switch (event.getAction()){ case MotionEvent.ACTION_DOWN: pointDownX = x; pointDownY = y; pointDownTime = System.currentTimeMillis(); break; case MotionEvent.ACTION_MOVE: lastDownTime = System.currentTimeMillis(); //Log.w("haha",pointDownX+"!!"+pointDownY+"!!!x="+x+"!!!y="+y); if (isLongPressed(pointDownX,pointDownY,x,y,pointDownTime,lastDownTime)) { LongClick(); return true; } break; case MotionEvent.ACTION_UP: pointUpX = x; pointUpY = y; if (Math.abs(pointDownX-pointUpX) void OnClick(); void onLongClick(); } @Override public void onCreate() { super.onCreate(); createFloatView(); } public class ProgressRecevier extends BroadcastReceiver{ @Override public void onReceive(Context context, Intent intent) { int progress = intent.getIntExtra("progress",0); mRotateView.rotate(progress); } } }

通过上述代码,我们可以发现,创建浮窗的代码也很简单。RotateView是我自定义的一个view,是一个旋转安卓机器人和圆形进度图的view,稍后我会进行代码介绍。 1.对显示的view进行边界检测,通过重写setOnTouch事件,对位移进行判断,如果超出边界,则不移动,否则,移动响应的位移。 2.对单击和长按进行区分,通过接口进行实现。 3.接受来自playservice的播放进度的广播,对自定义view进行进度控制,因为当app退出的时候,两个service无法通过activity进行回调,所以我通过广播进行进度的监听。(不给用第三方,所以不能用rxbus了。。。)

现在,核心的两个service类都已经完成了。接下来,看看主页面的代码吧。 让我们看看我们的BaseActivity类,关键代码如下

public abstract class BaseActivity extends AppCompatActivity { protected PlayService mPlayService; protected FxService mFxService; private boolean isBound = false; public boolean isFirst = true; public void allowBindService(){ getApplicationContext().bindService(new Intent(this,PlayService.class), mPlayServiceConnection, Context.BIND_AUTO_CREATE); } public void allowUnBindService(){ getApplicationContext().unbindService(mPlayServiceConnection); } public void BindFxService(){ isBound = getApplicationContext().bindService(new Intent(this,FxService.class), mFxServiceConnection, Context.BIND_AUTO_CREATE); } public void unBindFxService(){ if (isBound) { isBound = false; isFirst = true; getApplicationContext().unbindService(mFxServiceConnection); stopService(new Intent(this,FxService.class)); } } public PlayService getPlayService(){ return mPlayService; } private ServiceConnection mPlayServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { mPlayService = ((PlayService.PlayBinder) iBinder).getService(); mPlayService.setOnMusicEventListener(mMusicEventListener); } @Override public void onServiceDisconnected(ComponentName componentName) { mPlayService = null; } }; private ServiceConnection mFxServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { mFxService = ((FxService.FxBinder) iBinder).getService(); } @Override public void onServiceDisconnected(ComponentName componentName) { mFxService = null; } }; public FxService getFxService(){ return mFxService; } private PlayService.OnMusicEventListener mMusicEventListener = new PlayService.OnMusicEventListener() { @Override public void onPublish(int percent) { BaseActivity.this.onPublish(percent*100/ MusicUtil.sMusicList.get(mPlayService.getPlayingPosition()).getDuration()); } @Override public void onChange(int position) { BaseActivity.this.onChange(position); } }; public abstract void onPublish(int percent); public abstract void onChange(int position); }

BaseActivity的主要功能有: 1.建立Activity和service之间的连接 2.提供接触绑定和绑定服务的方法 3.播放进度的回调监听

接下来,就是主页面的Activity了。关键代码如下:

public class MainActivity extends BaseActivity { @Override public void initUiAndListener() { mMusicAdapter = new MusicAdapter(); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setItemAnimator(new DefaultItemAnimator()); recyclerView.setAdapter(mMusicAdapter); recyclerView.addItemDecoration(new RecyclerViewDivider( this, RecyclerViewDivider.VERTICAL_LIST)); mMusicAdapter.setOnItemClickLisetener( new MusicAdapter.OnItemClickLisetener() { @Override public void onItemClick(int position) { mPlayService.play(position); startService(new Intent(App.sContext, FxService.class)); BindFxService(); playProgress.setMax(MusicUtil.sMusicList.get(position).getDuration()); } }); playProgress.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int i, boolean b) { } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { int progress = seekBar.getProgress(); mPlayService.seek(progress); } }); } }

主页面主要功能是: 1.通过recyclerview对音乐列表进行显示,通过MusicAdapter对对应的item进行点击监听 2.页面底部有播放进度的seekbar,还有上下曲的按钮,通过拉动seekbar可对音乐进度进行调整 3.让fxservice和Activity进行绑定,让悬浮窗显示

接下来,我们说说刚才说到到RotateView

public class RotateView extends View{ private static final int MSG_RUN = 0x00000100; private static final int TIME_UPDATE = 16; private Bitmap mClipBitmap;//cd图片 private Matrix mMatrix; private float mRotation = 0.0f; private volatile boolean isRunning; private Paint paint; private int ringColor; private int ringProgressColor; private float ringWidth; private double scale; public RotateView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mMatrix = new Matrix(); paint = new Paint(); TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.RoundProgressBar); ringColor = mTypedArray.getColor(R.styleable.RoundProgressBar_ringColor,0xff50c0e9); ringProgressColor = mTypedArray.getColor(R.styleable.RoundProgressBar_ringProgressColor, 0xffffc641); ringWidth = mTypedArray.getDimension(R.styleable.RoundProgressBar_ringWidth, 20); mTypedArray.recycle(); } public RotateView(Context context, AttributeSet attrs) { this(context, attrs ,0); } public RotateView(Context context) { this(context, null); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mClipBitmap == null){ super.onMeasure(widthMeasureSpec, heightMeasureSpec); return; } int width = 0; int height = 0; int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (widthMode == MeasureSpec.EXACTLY){ width = widthSize; }else { width = mClipBitmap.getWidth(); //子view不能大于父类 if (widthMode == MeasureSpec.AT_MOST){ width = Math.min(width,widthSize); } } if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else { height = mClipBitmap.getHeight(); if (heightMode == MeasureSpec.AT_MOST) { height = Math.min(height, heightSize); } } setMeasuredDimension(width, height); } @Override protected void onDraw(Canvas canvas) { if (mClipBitmap == null) return; canvas.save(); mMatrix.setRotate(mRotation, (float) (App.sScreenWidth/10*1.5), (float) (App.sScreenWidth/10*1.5)); canvas.drawBitmap(mClipBitmap,mMatrix,null); canvas.restore(); int center = (int)(App.sScreenWidth/2*scale);//圆心的x坐标 int radius = (int)(center-ringWidth/2); /** * 画最外层的大圆环 */ paint.setColor(ringColor);//设置圆环的颜色 paint.setStyle(Paint.Style.STROKE);//设置空心 paint.setStrokeWidth(ringWidth); //设置圆环的宽度 paint.setAntiAlias(true); //消除锯齿 canvas.drawCircle(center, center, radius, paint); //画出圆环 paint.setStrokeWidth(ringWidth); paint.setColor(ringProgressColor); RectF oval = new RectF(center - radius, center - radius, center + radius, center + radius); paint.setStyle(Paint.Style.STROKE); canvas.drawArc(oval, 0, mRotation, false, paint); //根据进度画圆弧 } private Bitmap cretaeCircleBitmap(Bitmap src){ Paint paint = new Paint(); paint.setAntiAlias(true); paint.setARGB(255,241,239,229); Bitmap target = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(target); canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredWidth() / 2, (float) getMeasuredWidth()/2-ringWidth, paint); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); canvas.drawBitmap(src, 0, 0, paint); return target; } public void setCdImage(Bitmap bitmap,double scale){ this.scale = scale; bitmap = ImageTools.scaleBitmap(bitmap, (int) (App.sScreenWidth * scale)); int widthSize = bitmap.getWidth(); int heightSize = bitmap.getHeight(); int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST); int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.AT_MOST); measure(widthSpec, heightSpec); mClipBitmap = cretaeCircleBitmap(bitmap); requestLayout(); invalidate(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); isRunning = false; } public void startRoll(){ if (isRunning) return; isRunning = true; mHandler.sendEmptyMessageDelayed(MSG_RUN, TIME_UPDATE); } //暂停旋转 public void pause() { if (!isRunning) return; isRunning = false; } private Handler mHandler = new Handler() { public void handleMessage(Message msg) { if (msg.what == MSG_RUN) { if (isRunning) { if (mRotation >= 360) mRotation = 0; invalidate(); sendEmptyMessageDelayed(MSG_RUN, TIME_UPDATE); } } } }; public void rotate(float angle){ this.mRotation = (float) (angle*3.6); } }

首先,通过自定义参数,我们可以画出一个圆形进度条,然后获取对应的机器人bitmap和宽高大小。 再通过rotate设置旋转的角度并通过Matrix对view进行旋转。

到这里,我们的应用基本就完成了。把应用发给组长,组长后来跟我说运行不了,直接奔溃。。。然后说可能是权限问题,他的手机是6.0的。所以,后来我就添加了一下动态权限申请。

public class SplashActivity extends AppCompatActivity { public static final int REQUEST_CODE = 100; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // no title requestWindowFeature(Window.FEATURE_NO_TITLE); // 全屏 getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); setContentView(R.layout.splash_layout); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_CODE); return; }else { new Handler().postDelayed(new Runnable() { @Override public void run() { startActivity(new Intent(SplashActivity.this, MainActivity.class)); finish(); } }, 2000); } } @Override public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_CODE) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // Permission granted. requestDrawOverLays(); } else { // User refused to grant permission. Toast.makeText(this,"请先给予读写权限,否则app没法用啊",Toast.LENGTH_LONG).show(); } } } public static int OVERLAY_PERMISSION_REQ_CODE = 1234; @TargetApi(Build.VERSION_CODES.M) public void requestDrawOverLays() { if (!Settings.canDrawOverlays(SplashActivity.this)) { Toast.makeText(this, "请手动给予特殊权限", Toast.LENGTH_SHORT).show(); Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + SplashActivity.this.getPackageName())); startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE); } else { startService(new Intent(this,PlayService.class)); startActivity(new Intent(this, MainActivity.class)); finish(); // Already hold the SYSTEM_ALERT_WINDOW permission, do addview or something. } } @TargetApi(Build.VERSION_CODES.M) @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == OVERLAY_PERMISSION_REQ_CODE) { if (!Settings.canDrawOverlays(this)) { // SYSTEM_ALERT_WINDOW permission not granted... Toast.makeText(this, "给权限啊,大佬。。。", Toast.LENGTH_SHORT).show(); } else { //Toast.makeText(this, "Permission Allowed", Toast.LENGTH_SHORT).show(); startService(new Intent(this,PlayService.class)); startActivity(new Intent(this, MainActivity.class)); finish(); // Already hold the SYSTEM_ALERT_WINDOW permission, do addview or something. } } } }

这里我要说明一下,首先呢,读写的权限肯定是必须的。ALERT_WINDOW用于弹窗的显示。

读写权限可以通过动态申请获取,但是SYSTEM_ALERT_WINDOW 这个权限比较特殊,官方的解释是在goole play下载的时候,会提示用户是否给予权限,同意了才能下载。下载完应用会即刻拥有SYSTEM_ALERT_WINDOW的权限,即无需手动授权。 然而,对于自己写的应用,只能通过动态权限申请,上述代码在原生的机器上,不管是自带的模拟器还是Genymotion,都可以显示权限申请界面,但是在我们国产的手机(乱改系统)的情况下,很多手机都把SYSTEM_ALERT_WINDOW这个权限默认直接禁止了,所以说我们只能手动赋予权限。(即设置->安装应用->FloatMusic->权限管理->浮窗权限)否则,浮窗默认是不显示的。

最后,附上代码链接:https://github.com/RuijiePan/FinalMusic.git



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3