桌面widget详解(四)——桌面音乐播放器(实战)

先看看本篇的最终效果:

一、Service控制播放部分(MusicManageService.java)

首先由于我们要与按钮相交互,所以在Service中的交互一般是通过BroadcastReceiver来实现的,所以在MusicManageService的OnCreate函数中(Service起来的时候调用OnCreate)应该包括下面几个步骤:注册Receiver,初始化歌曲播放列表,开始播放默认歌曲;


所以首先是注册Receiver:

IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION);
registerReceiver(receiver, intentFilter);

对应的BroadcastReceiver receiver主要是根据接收来的消息来上一首,下一首,暂停、播放歌曲:

public static String ACTION = "to_service";
public static String KEY_USR_ACTION = "key_usr_action";
public static final int ACTION_PRE = 0, ACTION_PLAY_PAUSE = 1, ACTION_NEXT = 2;
private boolean mPlayState = false;
 
private BroadcastReceiver receiver = new BroadcastReceiver() {
	@Override
	public void onReceive(Context context, Intent intent) {
		String action  = intent.getAction();
		if (ACTION.equals(action)) {
			int widget_action = intent.getIntExtra(KEY_USR_ACTION, -1);
			
			switch (widget_action) {
			case ACTION_PRE:
				playPrev(context);
				Log.d("harvic","action_prev");
				break;
			case ACTION_PLAY_PAUSE:
				if (mPlayState) {
					pause(context);
					Log.d("harvic","action_pause");
				}else{
					play(context);
					Log.d("harvic","action_play");
				}	
				break;
			case ACTION_NEXT:
				playNext(context);
				Log.d("harvic","action_next");
				break;
			default:
				break;
			}
		}
	}
};

然后是初始化播放列表:

private int[] mArrayList = new int[9];
private void initList() {
	mArrayList[0] = R.raw.dui_ni_ai_bu_wan;
	mArrayList[1] = R.raw.fei_yu;
	mArrayList[2] = R.raw.gu_xiang_de_yun;
	mArrayList[3] = R.raw.hen_ai_hen_ai_ni;
	mArrayList[4] = R.raw.new_day;
	mArrayList[5] = R.raw.shi_jian_li_de_hua;
	mArrayList[6] = R.raw.ye_gui_ren;
	mArrayList[7] = R.raw.yesterday_once_more;
	mArrayList[8] = R.raw.zai_lu_shang;
}

最后在Service起来时就应该让它播放歌曲:

private void mediaPlayerStart(){
	mPlayer = new MediaPlayer();
	mPlayer = MediaPlayer.create(getApplicationContext(), mArrayList[mIndex]);
	mPlayer.start();
	mPlayState = true;
}

上面就基本上就是MusicManageService的骨架了,其它就是上一首,下一首,播放、暂停,这些难度都不大,就不细讲了,完整的MusicManageService.java代码如下:

public class MusicManageService extends Service {
 
	private MediaPlayer mPlayer;
	private int mIndex = 4;// 从中间开始放
	private int[] mArrayList = new int[9];
	public static String ACTION = "to_service";
	public static String KEY_USR_ACTION = "key_usr_action";
	public static final int ACTION_PRE = 0, ACTION_PLAY_PAUSE = 1, ACTION_NEXT = 2;
	private boolean mPlayState = false;
 
	private BroadcastReceiver receiver = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
			String action  = intent.getAction();
			if (ACTION.equals(action)) {
				int widget_action = intent.getIntExtra(KEY_USR_ACTION, -1);
				
				switch (widget_action) {
				case ACTION_PRE:
					playPrev(context);
					Log.d("harvic","action_prev");
					break;
				case ACTION_PLAY_PAUSE:
					if (mPlayState) {
						pause(context);
						Log.d("harvic","action_pause");
					}else{
						play(context);
						Log.d("harvic","action_play");
					}	
					break;
				case ACTION_NEXT:
					playNext(context);
					Log.d("harvic","action_next");
					break;
				default:
					break;
				}
			}
		}
	};
 
	@Override
	public IBinder onBind(Intent intent) {
		// TODO Auto-generated method stub
		return null;
	}
 
	@Override
	public void onCreate() {
		// TODO Auto-generated method stub
		super.onCreate();
 
		IntentFilter intentFilter = new IntentFilter();
		intentFilter.addAction(ACTION);
		registerReceiver(receiver, intentFilter);
 
		initList();
 
		mediaPlayerStart();		
	}
	private void mediaPlayerStart(){
		mPlayer = new MediaPlayer();
		mPlayer = MediaPlayer.create(getApplicationContext(), mArrayList[mIndex]);
		mPlayer.start();
		mPlayState = true;
	}
 
	private void initList() {
		mArrayList[0] = R.raw.dui_ni_ai_bu_wan;
		mArrayList[1] = R.raw.fei_yu;
		mArrayList[2] = R.raw.gu_xiang_de_yun;
		mArrayList[3] = R.raw.hen_ai_hen_ai_ni;
		mArrayList[4] = R.raw.new_day;
		mArrayList[5] = R.raw.shi_jian_li_de_hua;
		mArrayList[6] = R.raw.ye_gui_ren;
		mArrayList[7] = R.raw.yesterday_once_more;
		mArrayList[8] = R.raw.zai_lu_shang;
	}
 
	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
 
		return super.onStartCommand(intent, flags, startId);
	}
 
	@Override
	public void onDestroy() {
 
		super.onDestroy();
		mPlayer.stop();
	}
 
	/**
	 * 播放下一首
	 * 
	 * @param context
	 */
	public void playNext(Context context) {
		if (++mIndex > 8) {
			mIndex = 0;
		}
		mPlayState = true;
		playSong(context, mArrayList[mIndex]);
	}
 
	/**
	 * 播放上一首
	 * 
	 * @param context
	 */
	public void playPrev(Context context) {
		if (--mIndex < 0) {
			mIndex = 8;
		}
		mPlayState = true;
		playSong(context, mArrayList[mIndex]);
	}
 
	/*
	 * 继续播放
	 */
	public void play(Context context) {
		mPlayState = true;
		mPlayer.start();
	}
 
	/**
	 * 暂停播放
	 * 
	 * @param context
	 */
	public void pause(Context context) {
		mPlayState = false;
		mPlayer.pause();		
	}
 
	/**
	 * 播放指定的歌曲
	 * 
	 * @param context
	 * @param resid
	 */
	private void playSong(Context context, int resid) {
		AssetFileDescriptor afd = context.getResources().openRawResourceFd(
				mArrayList[mIndex]);
		try {
			mPlayer.reset();
			mPlayer.setDataSource(afd.getFileDescriptor(),
					afd.getStartOffset(), afd.getDeclaredLength());
			mPlayer.prepare();
			mPlayer.start();
			afd.close();
		} catch (Exception e) {
			Log.e("harvic","Unable to play audio queue do to exception: "+ e.getMessage(), e);
		}
 
	}
}

二、widget发送广播部分(ExampleAppWidgetProvider.java)

首先,在用户添加widget时,会调用OnUpdate()函数,所在我们在OnUpdate()中要实现绑定RemoteView和更新Widget的操作。

private void pushUpdate(Context context,AppWidgetManager appWidgetManager) {
	
	RemoteViews remoteView = new RemoteViews(context.getPackageName(),R.layout.example_appwidget);
	//将按钮与点击事件绑定
	remoteView.setOnClickPendingIntent(R.id.play_pause,getPendingIntent(context, R.id.play_pause));
	remoteView.setOnClickPendingIntent(R.id.prev_song, getPendingIntent(context, R.id.prev_song));
	remoteView.setOnClickPendingIntent(R.id.next_song, getPendingIntent(context, R.id.next_song));
 
	// 相当于获得所有本程序创建的appwidget
	ComponentName componentName = new ComponentName(context,ExampleAppWidgetProvider.class);
	appWidgetManager.updateAppWidget(componentName, remoteView);
}
 
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
		int[] appWidgetIds) {
	
	pushUpdate(context,appWidgetManager);
}

首先是新建RemoteView,并将它与那三个按钮相绑定,其中的GetPendingIntent的实现与上一篇一样,也是把按钮ID传进去,通过Uri来传送,这里为了接收到以后方便识别是按钮点击传过去的消息,我们随便加一个Category字段,所以在接收方只需要通过intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)来判断是不是我们这里传过去的Intent即可,这里又比上篇高级了一点点有没有,哈哈。

private PendingIntent getPendingIntent(Context context, int buttonId) {
	Intent intent = new Intent();
	intent.setClass(context, ExampleAppWidgetProvider.class);
	intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
	intent.setData(Uri.parse("harvic:" + buttonId));
	PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, 0);
	return pi;
}

然后是接收部分,在接收时,首先根据当前用户点击的哪个按钮,然后给MusicManageService发送不同的广播,让MusicManageService做出不同的响应,接收代码如下 :

public void onReceive(Context context, Intent intent) {
	String action = intent.getAction();
	Log.d("harvic", "action:"+action);
	
	if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) {
		Uri data = intent.getData();
        int buttonId = Integer.parseInt(data.getSchemeSpecificPart());
        switch (buttonId) {
        case R.id.play_pause:
        	pushAction(context,MusicManageService.ACTION_PLAY_PAUSE);
        	if(mStop){
        		Intent startIntent = new Intent(context,MusicManageService.class);
				context.startService(startIntent);
        		mStop = false;
        	}
        	break;
        case R.id.prev_song:
        	pushAction(context, MusicManageService.ACTION_PRE);
        	break;
        case R.id.next_song:
        	pushAction(context, MusicManageService.ACTION_NEXT);
        	break;
        }
 
	}
	super.onReceive(context, intent);
}

在这里首先根据是不是包含intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)这个category来判断是不是点击按钮发出来的消息,然后提取出按钮ID,最后根据不同的按钮ID发送出不同的消息:

private void pushAction(Context context, int ACTION) {
    Intent actionIntent = new Intent(MusicManageService.ACTION);
    actionIntent.putExtra(MusicManageService.KEY_USR_ACTION, ACTION);
    context.sendBroadcast(actionIntent);
}

这里发送消息与MusicManageService的消息接收方式是统一的,发送和接收都是通过Action来匹配,携带的值是当前的Action,下面就是MusicManageService接收时的部分代码,我再摘一遍,方便大家理解:

private BroadcastReceiver receiver = new BroadcastReceiver() {
	@Override
	public void onReceive(Context context, Intent intent) {
		String action  = intent.getAction();
		if (ACTION.equals(action)) {
			int widget_action = intent.getIntExtra(KEY_USR_ACTION, -1);
			
			switch (widget_action) {
			case ACTION_PRE:
				break;
			case ACTION_PLAY_PAUSE:
				break;
			case ACTION_NEXT:
				break;
			}
		}
	}
};

所以完整的ExampleAppWidgetProvider代码是这样的:

public class ExampleAppWidgetProvider extends AppWidgetProvider {
 
	private ExampleAppWidgetProvider mProvider = null;
	private boolean mStop = true;
	
	private PendingIntent getPendingIntent(Context context, int buttonId) {
		Intent intent = new Intent();
		intent.setClass(context, ExampleAppWidgetProvider.class);
		intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
		intent.setData(Uri.parse("harvic:" + buttonId));
		PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, 0);
		return pi;
	}
	
	// 更新所有的 widget
	private void pushUpdate(Context context,AppWidgetManager appWidgetManager) {
		
		RemoteViews remoteView = new RemoteViews(context.getPackageName(),R.layout.example_appwidget);
		//将按钮与点击事件绑定
		remoteView.setOnClickPendingIntent(R.id.play_pause,getPendingIntent(context, R.id.play_pause));
		remoteView.setOnClickPendingIntent(R.id.prev_song, getPendingIntent(context, R.id.prev_song));
		remoteView.setOnClickPendingIntent(R.id.next_song, getPendingIntent(context, R.id.next_song));
 
		// 相当于获得所有本程序创建的appwidget
		ComponentName componentName = new ComponentName(context,ExampleAppWidgetProvider.class);
		appWidgetManager.updateAppWidget(componentName, remoteView);
	}
 
	@Override
	public void onUpdate(Context context, AppWidgetManager appWidgetManager,
			int[] appWidgetIds) {
		
		pushUpdate(context,appWidgetManager);
	}
	
	// 接收广播的回调函数
	@Override
	public void onReceive(Context context, Intent intent) {
		String action = intent.getAction();
		Log.d("harvic", "action:"+action);
		
		if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) {
			Uri data = intent.getData();
	        int buttonId = Integer.parseInt(data.getSchemeSpecificPart());
	        switch (buttonId) {
	        case R.id.play_pause:
	        	pushAction(context,MusicManageService.ACTION_PLAY_PAUSE);
	        	if(mStop){
	        		Intent startIntent = new Intent(context,MusicManageService.class);
					context.startService(startIntent);
	        		mStop = false;
	        	}
	        	break;
	        case R.id.prev_song:
	        	pushAction(context, MusicManageService.ACTION_PRE);
	        	break;
	        case R.id.next_song:
	        	pushAction(context, MusicManageService.ACTION_NEXT);
	        	break;
	        }
	
		}
		super.onReceive(context, intent);
	}
	
	private void pushAction(Context context, int ACTION) {
        Intent actionIntent = new Intent(MusicManageService.ACTION);
        actionIntent.putExtra(MusicManageService.KEY_USR_ACTION, ACTION);
        context.sendBroadcast(actionIntent);
    }
}

到这里,效果是这样的:(在模拟器上点击看起来没有任何反应,其实已经在播放歌曲了,上一首,下一首,播放、暂停功能都是可用的)

三、Service反向通知Widget更新当前状态

上面我们已经实现了widget按钮向Service发广播来播放歌曲的播放、暂停,上一首,下一首,但是我们的widget状态确没有改变,这节我们就需要根据当前歌曲的状态来更新widget控件的状态。

1、发送当前状态广播

首先,我们要在MusicManageService中根据当前的播放状态往ExampleAppWidgetProvider发送广播,广播的目的主要是改变当前播放按钮的状态(播放、暂停)还有更新TextView,让它显示当前播放歌曲的ID值。

所以我们在发送广播时,需要定义Intent的Action,和存放播放状态、歌曲Id的putExtra(key,value)中的key值:

public static String MAIN_UPDATE_UI = "main_activity_update_ui";  //Action
public static String KEY_MAIN_ACTIVITY_UI_BTN = "main_activity_ui_btn_key"; //putExtra中传送当前播放状态的key
public static String KEY_MAIN_ACTIVITY_UI_TEXT = "main_activity_ui_text_key"; //putextra中传送TextView的key
public static final int  VAL_UPDATE_UI_PLAY = 1,VAL_UPDATE_UI_PAUSE =2;//当前歌曲的播放状态

发送时:

private void postState(Context context, int state,int songid) {
	Intent actionIntent = new Intent(ExampleAppWidgetProvider.MAIN_UPDATE_UI);
	actionIntent.putExtra(ExampleAppWidgetProvider.KEY_MAIN_ACTIVITY_UI_BTN,state);
	actionIntent.putExtra(ExampleAppWidgetProvider.KEY_MAIN_ACTIVITY_UI_TEXT, songid);
	context.sendBroadcast(actionIntent);
}

其中:

state就是上面VAL_UPDATE_UI_PLAY = 1,VAL_UPDATE_UI_PAUSE =2其中一个状态;

songid:表示当前播放歌曲的id值

所以在播放歌曲状态和歌曲id值改变时,就应该发送广播:

首先,在Service起来时,我们开始播放歌曲:
 

private void mediaPlayerStart(){
	mPlayer = new MediaPlayer();
	mPlayer = MediaPlayer.create(getApplicationContext(), mArrayList[mIndex]);
	mPlayer.start();
	mPlayState = true;
	postState(getApplicationContext(), ExampleAppWidgetProvider.VAL_UPDATE_UI_PLAY,mIndex);
}

同样,在上一首,下一首,播放,暂停时都要发送广播:

播放时:(改变了播放状态)

public void play(Context context) {
	……
	postState(context, ExampleAppWidgetProvider.VAL_UPDATE_UI_PLAY,mIndex);
}

暂停时:(改变了播放状态)

public void pause(Context context) {
	……		
	postState(context, ExampleAppWidgetProvider.VAL_UPDATE_UI_PAUSE,mIndex);
}

上一首:(改变了歌曲ID)

public void playPrev(Context context) {
	……
	postState(context, ExampleAppWidgetProvider.VAL_UPDATE_UI_PLAY,mIndex);
}

下一首:(改变了歌曲ID)

public void playNext(Context context) {
	……
	postState(context, ExampleAppWidgetProvider.VAL_UPDATE_UI_PLAY,mIndex);
}

OK啦,到这里发送就结束了,下面就是接收了。

2、接收广播

首先在接收广播之前要注册,ExampleAppWidgetProvider以前说过是直接派生自 BroadcastReceiver的,所有我们只能采用静态注册的方式:注意的action要与发送的一致,即:main_activity_update_ui

        <receiver android:name=".ExampleAppWidgetProvider" >
            <intent-filter>
				<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
				<action android:name="main_activity_update_ui" />
			</intent-filter>
			<meta-data android:name="android.appwidget.provider"
				android:resource="@xml/example_appwidget_info" />
        </receiver>

在上面,我们在ExampleAppWidgetProvider中更新RemoteView的pushUpdate() 的代码是这样的:

private void pushUpdate(Context context,AppWidgetManager appWidgetManager) {
	
	RemoteViews remoteView = new RemoteViews(context.getPackageName(),R.layout.example_appwidget);
	//将按钮与点击事件绑定
	remoteView.setOnClickPendingIntent(R.id.play_pause,getPendingIntent(context, R.id.play_pause));
	remoteView.setOnClickPendingIntent(R.id.prev_song, getPendingIntent(context, R.id.prev_song));
	remoteView.setOnClickPendingIntent(R.id.next_song, getPendingIntent(context, R.id.next_song));
 
	// 相当于获得所有本程序创建的appwidget
	ComponentName componentName = new ComponentName(context,ExampleAppWidgetProvider.class);
	appWidgetManager.updateAppWidget(componentName, remoteView);
}

在这里,我们只是绑定了三个按钮控件,但并没有更新当前播放按钮状态和TextView上的字体,所以我们对它加以更改,在绑定按钮以后,根据当前接收到的状态,更新RemoteView

private void pushUpdate(Context context,AppWidgetManager appWidgetManager,String songName,Boolean play_pause) {
	
	RemoteViews remoteView = new RemoteViews(context.getPackageName(),R.layout.example_appwidget);
	//将按钮与点击事件绑定
	remoteView.setOnClickPendingIntent(R.id.play_pause,getPendingIntent(context, R.id.play_pause));
	remoteView.setOnClickPendingIntent(R.id.prev_song, getPendingIntent(context, R.id.prev_song));
	remoteView.setOnClickPendingIntent(R.id.next_song, getPendingIntent(context, R.id.next_song));
	
	//设置内容
	if (!songName.equals("")) {
		remoteView.setTextViewText(R.id.song_name, songName);
	}
	//设定按钮图片
	if (play_pause) {
		remoteView.setImageViewResource(R.id.play_pause, R.drawable.car_musiccard_pause);
	}else {
		remoteView.setImageViewResource(R.id.play_pause, R.drawable.car_musiccard_play);
	}
	// 相当于获得所有本程序创建的appwidget
	ComponentName componentName = new ComponentName(context,ExampleAppWidgetProvider.class);
	appWidgetManager.updateAppWidget(componentName, remoteView);
}

其中songName存储接收过来的歌曲id值,play_pause表示当前歌曲的播放状态,根据当前的播放状态加载不同的播放状态图片;

在理解了上面的更新RemoteView的部分以后,下面看看接收广播的代码:

public void onReceive(Context context, Intent intent) {
	String action = intent.getAction();
	Log.d("harvic", "action:"+action);
	
	if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) {
		…… //接收到的按钮点击广播的处理部分
	}else if (MAIN_UPDATE_UI.equals(action)){
			int play_pause =  intent.getIntExtra(KEY_MAIN_ACTIVITY_UI_BTN, -1);
			int songid = intent.getIntExtra(KEY_MAIN_ACTIVITY_UI_TEXT, -1);
			switch (play_pause) {
			case VAL_UPDATE_UI_PLAY:
				pushUpdate(context, AppWidgetManager.getInstance(context), "current sond id:"+songid,true);
				break;
			case VAL_UPDATE_UI_PAUSE:
				pushUpdate(context, AppWidgetManager.getInstance(context), "current sond id:"+songid,false);
				break;
			default:
				break;
			}
			
		}
 
	super.onReceive(context, intent);
}

首先,我们通过获取action值来判断当前是不是MusicManageService传过来的消息,然后得到传过来当前的播放状态和歌曲ID值,然后利用pushUpdate更新RemoteView;

四、附:相关问题

下面记录一下,我在实际开发中遇到的问题,分享给大家

1、有关RemoteViews实例复用(绝对要每次新建RemoteView实例)

在实际项目中,大家可能会想到复用remoteView,即如果已经创建了就不再重新加载layout,而是重新绑定控件及数据


 !!!!!!!千万不要这样!!!!!!!一定要每次都要新建remoteView!!!!!这是血和泪的教训!!!!!


 因为:如果你在绑定数据时涉及图片等大数据,remoteView不会每次清理,所以如果每次都使用同一个remoteView进行传输会因为溢出而绐终无响应!!!! 你看着每次动作都通过updateAppWidget传送出去了,但界面死活就是没响应;而且重装应用程序也不会有任何反应,只能重启手机才会重新有反应,也是醉了。
 主要原因在于:Binder data size limit is 512k,由于传输到appWidget进程中的Binder最大数据量是512K,并且RemoteView也不会每次清理, 所以如果每次都使用同一个RemoteView进行传输会因为溢出而报错.所以必须每次重新建一个RemoteView来传输!!!!!!

2、操作RemoteView中控件的方法

在RemoteView中的操作控件的方法非常有限,但我们的需求确是非常多样的,所以怎样才能像操作平常控件一样多样性的操作RemoteView呢,

举例:
如果我们需要把widget中的一个view临时隐藏,我们可以这样调用:remoteviews.setInt(textviewid,"setVisibility",VIEW.INVISIBLE);
 

OK啦,终于写完了,有点要累尿了,这部分涉及到的代码量太大,我上面讲的也不太详细,大家匹配代码再仔细琢磨琢磨一下吧。

参考文章:

1、《Android 之窗口小部件详解--App Widget》  (初步入门级,写的很好)

2、《Android桌面组件AppWidget讲解》

3、《app widget 进入主客户端代码》  (讲述了,点击桌面widget如何进入主app的代码)

4、《AppWidget基础小结》

5、《Android 桌面组件【app widget】 进阶项目--心情记录器》

6、《Android Widget开发的相关技术点记录》 (其中有:存储widgetID,以防app崩溃后,无法更新widget的问题)

7、《android widget开发点滴》

8、《Android Appwidget 之按钮事件》

9、《android 转载 widget点击事件》

10、《 Android基础之AppWidgetProvider》

11、《android之widget详解》

 微信公众号【码农园区】,技术分享,值得关注

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梦之归途

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值