背景
最近想写个小应用,有个类似抽奖转盘的控件需要实现,因此记录和分享这个实现过程。一开始打算使用自定义view来写的,毕竟之前写过,后来写了一半,发现SurfaceView是一个专门为频繁绘制图形而提供的高性能类,因此决定改为SurfaceView来实现。
使用硬件:Nexus7 2013版,十年前的平板了,性能表现还行。
效果预览
按住中间的按钮开始转动灯盘,停止后加权两个灯盘的数字,获得加权和进行显示,效果如下:
GIF有时加载不出来,csdn的问题, 我贴张图片大家自行想象下,数字区域黄色的灯块会在用户按住中心按钮时进行滚动,松手后取和显示在中央。

不知道为什么,这个GIF在优快云有时候就是显示不出来
实现想法和思路
由于图层的属性约束,因此绘制图形需要先从底层开始绘制,保障绘制顺序,那么按照所设计的转盘,分别从下到上的图层为:
背景圆盘
外层内容 与 内层 数字栅格
按钮图层
数字总和
其中,内层的数字栅格具有演示效果,考虑在灯盘转动过程中,将整个内层栅格全部置灰,以增强视觉对比效果,同时通过控制色块的位置,制造数字灯盘转动的效果,而灯盘转灯的间隔,则根据SurfaceView中的刷新线程间隔来进行控制。
考虑到转盘的整体效果,存在三种状态:初始态 - > 运行态 - > 等待态
初始态表示未发生过启动事件,无选中表现在界面,运行态表示灯盘转动过程,界面需要有对比和动画效果,等待态表示运行结束,选中结果保留在屏幕上。
因此,基于以上考虑,单独实现一个状态机来配合控制状态切换。
代码实现
考虑到代码的灵活统一和可阅读性,我们除了状态机,还将外层内容,内层数字栅格,按钮及数字图层抽象为三个类,方便随时调整内部参数。
代码目录结构如下:

Component,各图层要素抽象类内容包;
fsm,状态机;
LuckyWheel, GUI绘制及逻辑控制。
4.1图层内容抽象
4.1.1外层内容
外圈内容抽象,包含画笔,背景颜色,以及要定义显示的内容元素。
因为格子并非标准的矩形,因此需要使用path类进行绘制,利用弧度绘制显示,因此需要存储内外相切圆的半径。
将UI和文字的path存储至此类中,方便重复利用。
public class PunishmentAndReward {
public int itemsNum;
public float bg_out_radius;
public float bg_in_radius;
public float txt_radius;
public final String[] txt_array= {"文字A","文字A","文字A","文字A",
"文字A","文字A","文字A","文字A"};
public List<Path> layer_list = new ArrayList<>();
public List<Path> text_list = new ArrayList<>();
public Paint mPaintPRLayer,mPaintText;
public final int PRsColorOne = 0xff60c5ba;
public final int PRsColorTwo = 0xffffc952;
public final int PRsColorThree = 0xffa5dff9;
public final int PRsColorFour = 0xffef5285;
public PunishmentAndReward(){
itemsNum = txt_array.length;
mPaintPRLayer = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintPRLayer.setStyle(Paint.Style.FILL);
mPaintText = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintText.setStrokeWidth(2);
mPaintText.setColor(Color.WHITE);
mPaintText.setTextAlign(Paint.Align.CENTER);
mPaintText.setTextSize(45);
mPaintText.setStyle(Paint.Style.FILL);
}
}
4.2.2 内层及数字内容
public class Stride {
public int itemsNum;
public float bg_out_radius;
public float bg_in_radius;
public float txt_radius;
public final String[] txt_array= {"0", "8", "3", "7" ,"4", "9", "2", "1", "5", "10",
"6", "3", "1", "12","11","6", "8", "1", "10", "5",
"12","4","9","1","8","2","7","9","10","6",
"5","7","4","12","11","3","2","6","9","1"};
public List<Path> layer_list = new ArrayList<>();
public List<Path> text_list = new ArrayList<>();
public final int defaultZeroColor = 0xff41D3BD;
public final int defaultColorOne = 0xff090707;
public final int defaultColorTwo = 0xffE53A40;
public final int unselectedColor = 0xff9baec8;
public final int selectedColor = 0xffffc952;
public Paint mPaintStrideLayer, mPaintText;
public Stride(){
itemsNum = txt_array.length;
mPaintStrideLayer = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintStrideLayer.setStyle(Paint.Style.FILL);
mPaintText = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintText.setStrokeWidth(2);
mPaintText.setColor(Color.WHITE);
mPaintText.setTextAlign(Paint.Align.CENTER);
mPaintText.setTextSize(55);
mPaintText.setStyle(Paint.Style.STROKE);
}
}
4.2.3按钮内容
由于按钮有触摸事件,为了限制触发事件只在按钮圆圈内生效,需要使用Region类进行判断。
public class StartBtn {
public float mRadius=0;
public Paint mPaintStartBtn;
public Paint mPaintTextBg;
public Paint mPaintText;
public float offsetText;
public Path mPath = new Path();
public Region mRegion = new Region();
public StartBtn(){
mPaintStartBtn = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintStartBtn.setStyle(Paint.Style.FILL);
mPaintStartBtn.setColor(0xff1ec0ff);
mPaintTextBg = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintTextBg.setStyle(Paint.Style.FILL);
mPaintTextBg.setColor(0xff1ec0ff);
mPaintText = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintText.setStyle(Paint.Style.FILL);
mPaintText.setColor(Color.WHITE);
mPaintText.setTextAlign(Paint.Align.CENTER);
}
public void calc_path(){
mPath.addCircle(0,0,mRadius, Path.Direction.CW);
// Log.i("TAG", "calc_path: "+mRadius);
mRegion.setPath(mPath,new Region(-(int)mRadius,-(int)mRadius,(int)mRadius,(int)mRadius));
mPaintText.setTextSize(mRadius);
offsetText = mRadius/4;
}
4.2状态机
4.2.1接口定义
动画控制原理只需要控制不同时刻的颜色变化即可,因此状态接口只需要定义颜色接口。
public interface State {
int[] pickStridesColors(); //内圈栅格颜色
int[] pickPRsColors(); //外圈栅格颜色
}
4.2.2状态机控制器
使用单例模式做控制器,实现基本的状态切换能力。
public class FSMM implements State{
private final String TAG = "FSMM";
private static FSMM instalce = null;
private final Initial initialState;
private final Running runningState;
private final Waiting waitingState;
private State state;
public final String str_initial = "state_initial";
public final String str_running = "state_running";
public final String str_waiting = "state_waiting";
public final int[] selected_flags = {0,1};
public Stride strideObj = new Stride();
public PunishmentAndReward punRewObj = new PunishmentAndReward();
public StartBtn startBtn = new StartBtn();
public static FSMM getInstance(){
if (null == instalce){
synchronized (FSMM.class){
if (null == instalce){
instalce = new FSMM();
}
}
}
return instalce;
}
private void init(){
Log.i(TAG, "init: FSMM 初始化完成 ...");
}
private FSMM(){
initialState = new Initial(this);
runningState = new Running(this);
waitingState = new Waiting(this);
this.state = initialState;
}
public void setState(State state){
Log.i(TAG, "setState: before --> " + getStateString());
this.state = state;
Log.i(TAG, "setState: after --> " + getStateString());
}
private State getState(){
return this.state;
}
public State getStateByString(String str){
switch (str){
case str_initial:
return initialState;
case str_running:
return runningState;
case str_waiting:
return waitingState;
default:
Log.i(TAG, "getStateByString: err here");
return null;
}
}
public String getStateString(){
if (getState() instanceof Initial){
return str_initial;
}else if (getState() instanceof Running){
return str_running;
}else if (getState() instanceof Waiting){
return str_waiting;
}else {
return "unknown_state";
}
}
@Override
public int[] pickStridesColors() {
return state.pickStridesColors();
}
@Override
public int[] pickPRsColors() {
return state.pickPRsColors();
}
}
4.2.3初始状态定义
初始咋红台下,只要提供固定颜色即可,特殊颜色如内圈的0栅格,额外设置即可。
public class Initial implements State{
private final String TAG = "Initial";
FSMM fsmm ;
public Initial(FSMM fsmm) {
this.fsmm = fsmm;
}
@Override
public int[] pickStridesColors() {
int[] colors = new int[fsmm.strideObj.itemsNum];
colors[0] = fsmm.strideObj.defaultZeroColor;
for (int i = 1; i < fsmm.strideObj.itemsNum; i++){
if (i % 2 == 0){
colors[i] = fsmm.strideObj.defaultColorTwo;
}else {
colors[i] = fsmm.strideObj.defaultColorOne;
}
}
return colors;
}
@Override
public int[] pickPRsColors() {
int[] colors = new int[fsmm.punRewObj.itemsNum];
colors[0] = fsmm.punRewObj.PRsColorOne;
colors[1] = fsmm.punRewObj.PRsColorTwo;
colors[2] = fsmm.punRewObj.PRsColorOne;
colors[3] = fsmm.punRewObj.PRsColorThree;
colors[4] = fsmm.punRewObj.PRsColorFour;
colors[5] = fsmm.punRewObj.PRsColorTwo;
colors[6] = fsmm.punRewObj.PRsColorThree;
colors[7] = fsmm.punRewObj.PRsColorFour;
return colors;
}
}
4.2.4运行状态定义
运行状态中,需要灰化每一个数字灯格,表现为未选中状态,每一次运行,都会对选中数字栅格的颜色进行改变,由于我们设计的是双跑马灯,因此,需要FSMM控制器中定义int[] 来存储位置数字,每调运一次接口,选中位置标记分别++和--,并对栅格位置的颜色进行相应的改变。
public class Running implements State {
private final String TAG = "Running";
FSMM fsmm ;
public Running(FSMM fsmm) {
this.fsmm = fsmm;
}
@Override
public int[] pickStridesColors() {
int[] selected_flags = fsmm.selected_flags;
int[] colors = new int[fsmm.strideObj.itemsNum];
selected_flags[0]++;
selected_flags[1]--;
if (selected_flags[1] == selected_flags[0]){
selected_flags[0]++;
}
if (selected_flags[0] > 39){
selected_flags[0] = 0;
}
if (selected_flags[0] < 0){
selected_flags[0] = 39;
}
if (selected_flags[1] > 39){
selected_flags[1] = 0;
}
if (selected_flags[1] < 0){
selected_flags[1] = 39;
}
// Log.i(TAG, "pickStridesColors: "+ selected_flags[0]+"--"+selected_flags[1]);
for (int i=0;i<fsmm.strideObj.itemsNum;i++){
if (i == selected_flags[0] || i ==selected_flags[1]){
colors[i] = fsmm.strideObj.selectedColor;
}else {
colors[i] = fsmm.strideObj.unselectedColor;
}
}
return colors;
}
@Override
public int[] pickPRsColors() {
int[] colors = new int[fsmm.punRewObj.itemsNum];
colors[0] = fsmm.punRewObj.PRsColorOne;
colors[1] = fsmm.punRewObj.PRsColorTwo;
colors[2] = fsmm.punRewObj.PRsColorOne;
colors[3] = fsmm.punRewObj.PRsColorThree;
colors[4] = fsmm.punRewObj.PRsColorFour;
colors[5] = fsmm.punRewObj.PRsColorTwo;
colors[6] = fsmm.punRewObj.PRsColorThree;
colors[7] = fsmm.punRewObj.PRsColorFour;
return colors;
}
}
4.2.5等待状态定义
等待装态需要做的事情为恢复灯盘的原本颜色,同时保留被选中栅格位置的颜色。
public class Waiting implements State{
private final String TAG = "Waiting";
FSMM fsmm;
public Waiting(FSMM fsmm) {
this.fsmm = fsmm;
}
@Override
public int[] pickStridesColors() {
int[] colors = new int[fsmm.strideObj.itemsNum];
for (int i = 0; i < fsmm.strideObj.itemsNum; i++){
if (i % 2 == 0){
colors[i] = fsmm.strideObj.defaultColorTwo;
}else {
colors[i] = fsmm.strideObj.defaultColorOne;
}
if (i == fsmm.selected_flags[0] || i == fsmm.selected_flags[1]){
colors[i] = fsmm.strideObj.selectedColor;
}
}
if (0 != fsmm.selected_flags[0] && 0 != fsmm.selected_flags[1]){
colors[0] = fsmm.strideObj.defaultZeroColor;
}
return colors;
}
@Override
public int[] pickPRsColors() {
int[] colors = new int[fsmm.punRewObj.itemsNum];
colors[0] = fsmm.punRewObj.PRsColorOne;
colors[1] = fsmm.punRewObj.PRsColorTwo;
colors[2] = fsmm.punRewObj.PRsColorOne;
colors[3] = fsmm.punRewObj.PRsColorThree;
colors[4] = fsmm.punRewObj.PRsColorFour;
colors[5] = fsmm.punRewObj.PRsColorTwo;
colors[6] = fsmm.punRewObj.PRsColorThree;
colors[7] = fsmm.punRewObj.PRsColorFour;
return colors;
}
}
4.3基于SurfaceView的转盘代码实现
代码中使用了initBGCounter等需要注意下,本意是为了避免重复绘制造成性能损耗,因为SurfaceView具有多重缓冲的特性,因此如果想固定某个背景不再重新绘制,需要初始化三次画面,因此代码中使用了个计数器保证缓冲全部被填充。
public class LuckyWheel extends SurfaceView implements SurfaceHolder.Callback ,Runnable{
private final String TAG = "LuckyWheel";
private final Context mContext;
private Paint mPaintBackground;
private SurfaceHolder mSurfaceHolder;
Canvas mCanvas;
private float mWidth, mHeight;
boolean isDrawing= false, isStarting= false;;
float mBackLayerRadius;
private int initBGCounter = 0; //针对三缓冲而设置的计数器
private int initPRLayerCounter =0;
public LuckyWheel(Context context) {
super(context);
this.mContext = context;
}
public LuckyWheel(Context context, AttributeSet attrs) {
super(context, attrs);
this.mContext = context;
}
public LuckyWheel(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = getWidth();
mHeight = getHeight();
calc_paras(); //计算标准参数
init();
calc_punishment_and_reward_paras(FSMM.getInstance().punRewObj.bg_out_radius,
FSMM.getInstance().punRewObj.bg_in_radius,
FSMM.getInstance().punRewObj.txt_radius);
calc_stride_layer_paras(FSMM.getInstance().strideObj.bg_out_radius
,FSMM.getInstance().strideObj.bg_in_radius
,FSMM.getInstance().strideObj.txt_radius);
}
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
Log.i(TAG, "surfaceCreated: ");
isDrawing = true;
new Thread(this).start();
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
Log.i(TAG, "surfaceDestroyed: ");
isDrawing = false;
}
@Override
public void run() {
Log.i(TAG, "run: 运行开始");
long t = 0;
while (isDrawing){
t = System.currentTimeMillis();
try {
mCanvas = mSurfaceHolder.lockCanvas();
axis_init();
draw_background();
draw_punishment_and_reward_layer();
draw_strides_layer();
draw_start_button_layer();
}finally {
if (mCanvas!=null){
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
}
}
// Log.i(TAG, "run: 运行中");
try {
Thread.sleep(Math.max(0, 5-(System.currentTimeMillis()-t)));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void init(){
Log.d(TAG, "init: hehhehe");
mSurfaceHolder = getHolder();
mSurfaceHolder.addCallback(this);
this.setZOrderOnTop(true); //画布透明处理
this.mSurfaceHolder.setFormat(PixelFormat.TRANSLUCENT);
setFocusable(true);
setFocusableInTouchMode(true);
this.setKeepScreenOn(true); //屏幕常亮
mPaintBackground = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintBackground.setColor(0xff52616a);
mPaintBackground.setStyle(Paint.Style.FILL);
FSMM.getInstance().startBtn.mPaintStartBtn.setShadowLayer(FSMM.getInstance().startBtn.mRadius,0,0,0xff0080ff);
FSMM.getInstance().startBtn.calc_path();
}
//坐标原点移动到中间,y轴翻转
private void axis_init(){
mCanvas.translate(mWidth/2, mHeight/2);
// mCanvas.scale(1,-1);
}
private void draw_background(){
if (initBGCounter < 4){
mCanvas.drawCircle(0,0,mBackLayerRadius,mPaintBackground);
mCanvas.drawPath(FSMM.getInstance().startBtn.mPath,FSMM.getInstance().startBtn.mPaintStartBtn);
initBGCounter++;
}
}
private void calc_paras(){
mBackLayerRadius = mWidth/2;
FSMM.getInstance().punRewObj.bg_out_radius = mWidth/30 * 14;
FSMM.getInstance().punRewObj.bg_in_radius = mWidth/30 * 11;
FSMM.getInstance().punRewObj.txt_radius = (float) (mWidth/30 * 12) ;
FSMM.getInstance().strideObj.bg_out_radius = mWidth/30 * 11;
FSMM.getInstance().strideObj.bg_in_radius = mWidth/30 * 9;
FSMM.getInstance().strideObj.txt_radius = (float)(mWidth/30 * 9.5) ;
FSMM.getInstance().startBtn.mRadius = mWidth/30 * 7;
}
private void draw_strides_layer(){
int[] colors = FSMM.getInstance().pickStridesColors();
for (int i = 0; i <FSMM.getInstance().strideObj.itemsNum ;i++){
FSMM.getInstance().strideObj.mPaintStrideLayer.setColor(colors[i]);
mCanvas.drawPath(FSMM.getInstance().strideObj.layer_list.get(i),FSMM.getInstance().strideObj.mPaintStrideLayer);
}
for (int i = 0 ; i< FSMM.getInstance().strideObj.itemsNum;i++){
mCanvas.drawTextOnPath(FSMM.getInstance().strideObj.txt_array[i],FSMM.getInstance().strideObj.text_list.get(i),0,0, FSMM.getInstance().strideObj.mPaintText);
}
}
/*数字层参数计算,包含文字*/
private void calc_stride_layer_paras(float out_rads, float in_rads, float text_rads){
int start_angle = 0;
int offset_angle = 360 / FSMM.getInstance().strideObj.itemsNum; //计算每个item需要旋转的角度
RectF text_recfF = new RectF(-text_rads,-text_rads,text_rads,text_rads);
RectF out_rectF = new RectF(-out_rads, -out_rads, out_rads, out_rads);
RectF in_recfF = new RectF(-in_rads, -in_rads, in_rads, in_rads);
for (int i =0; i <FSMM.getInstance().strideObj.itemsNum; i++){
int out_start_angle = start_angle + i * offset_angle; //每次偏移
int in_start_angle = out_start_angle + offset_angle; //内偏多一个便宜角度反向开始扫弧度
Path path = new Path();
path.arcTo(out_rectF, out_start_angle, offset_angle,false);
path.arcTo(in_recfF,in_start_angle,-offset_angle,false);
path.close();
FSMM.getInstance().strideObj.layer_list.add(path);
Path mStrideTextPath = new Path();
mStrideTextPath.arcTo(text_recfF,out_start_angle,offset_angle);
FSMM.getInstance().strideObj.text_list.add(mStrideTextPath);
}
}
private void draw_start_button_layer(){
mCanvas.drawPath(FSMM.getInstance().startBtn.mPath,FSMM.getInstance().startBtn.mPaintTextBg);
int num1 = Integer.parseInt(FSMM.getInstance().strideObj.txt_array[FSMM.getInstance().selected_flags[0]]);
int num2 = Integer.parseInt(FSMM.getInstance().strideObj.txt_array[FSMM.getInstance().selected_flags[1]]);
String numStr = String.valueOf(num1+num2);
float offset = FSMM.getInstance().startBtn.offsetText;
mCanvas.drawText(numStr,0,offset,FSMM.getInstance().startBtn.mPaintText);
}
private void draw_punishment_and_reward_layer(){
if (initPRLayerCounter < 4){
initPRLayerCounter ++ ;
int[] colors = FSMM.getInstance().pickPRsColors();
for (int i = 0; i <FSMM.getInstance().punRewObj.itemsNum ;i++){
FSMM.getInstance().punRewObj.mPaintPRLayer.setColor(colors[i]);
mCanvas.drawPath(FSMM.getInstance().punRewObj.layer_list.get(i),FSMM.getInstance().punRewObj.mPaintPRLayer);
}
for (int i = 0 ; i< FSMM.getInstance().punRewObj.itemsNum;i++){
mCanvas.drawTextOnPath(FSMM.getInstance().punRewObj.txt_array[i],FSMM.getInstance().punRewObj.text_list.get(i),0,0, FSMM.getInstance().punRewObj.mPaintText);
}
}
}
private void calc_punishment_and_reward_paras(float out_rads, float in_rads, float text_rads){
int start_angle = 0;
int offset_angle = 360 / FSMM.getInstance().punRewObj.itemsNum;
RectF text_recfF = new RectF(-text_rads,-text_rads,text_rads,text_rads);
RectF out_rectF = new RectF(-out_rads, -out_rads, out_rads, out_rads);
RectF in_recfF = new RectF(-in_rads, -in_rads, in_rads, in_rads);
for (int i = 0; i < FSMM.getInstance().punRewObj.itemsNum;i++){
int out_start_angle = start_angle + i * offset_angle; //每次偏移
int in_start_angle = out_start_angle + offset_angle; //内偏多一个便宜角度反向开始扫弧度
Path path = new Path();
path.arcTo(out_rectF, out_start_angle, offset_angle,false);
path.arcTo(in_recfF,in_start_angle,-offset_angle,false);
path.close();
FSMM.getInstance().punRewObj.layer_list.add(path);
Path mTextPath = new Path();
mTextPath.arcTo(text_recfF,out_start_angle,offset_angle);
FSMM.getInstance().punRewObj.text_list.add(mTextPath);
}
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_DOWN:
//判断是否落在按钮区域内,event.getXY获得的是屏幕坐标系,由于画布坐标系做过平移,因此需要做转换
if (FSMM.getInstance().startBtn.mRegion.contains((int)event.getX()-(int)mWidth/2,(int)event.getY()-((int) mHeight/2))){
FSMM.getInstance().selected_flags[0] = new Random().nextInt(FSMM.getInstance().strideObj.itemsNum);
FSMM.getInstance().selected_flags[1] = new Random().nextInt(FSMM.getInstance().strideObj.itemsNum);
FSMM.getInstance().setState(FSMM.getInstance().getStateByString("state_running"));
}else {
Log.i(TAG, "onTouchEvent: 未落在");
}
Log.i(TAG, "ACTION_DOWN: "+x+"---"+y);
break;
case MotionEvent.ACTION_UP:
FSMM.getInstance().setState(FSMM.getInstance().getStateByString("state_waiting"));
break;
}
return true;
}
}
引用参考
5.1Nexus7平板
5.2参考
Android 自定义View —— Path_android path 描边_胡小牧的博客-优快云博客
安卓canvas path addArc()与arcTo()方法的区别_path.addarc_Java_noob1的博客-优快云博客
Android_自定义遥控器按钮_CodeCopyer的博客-优快云博客
android 自定义view 画板改变画笔颜色_android 不断变化paint颜色修改_小鲁班one的博客-优快云博客
LOL Colors - Curated color palette inspiration (webdesignrankings.com)
Android 自定义View-文字绘制_android 自定义view绘制文字_xiangxiongfly915的博客-优快云博客
5.3资源
https://pictogrammers.com/library/mdi/
https://fonts.google.com/icons?selected=Material+Icons&icon.platform=android
https://www.aigei.com/s?tab=file&type=2d&dim=interface_ui-is_vip_false