android输入法02:openwnn源码解析02—Keyboard和KeyboardView

 本文主要介绍openwnn对Keyboard和KeyboardView的处理。

        这一部分主要涉及三个类:InputViewManager.java,DefaultSoftKeyboard.java,DefaultSoftKeyboardJAJP.java。其中InputViewManager是与键盘相关的对外接口,DefaultSoftKeyboard是通用类,DefaultSoftKeyboardJAJP是日文定制类。

1、InputViewManager

       第一步我们先来看看InputViewManager。这个接口类代码很简单:

[java]  view plain copy
  1. /** 
  2.  * The interface of input view manager used by OpenWnn. 
  3.  * 
  4.  * @author Copyright (C) 2009 OMRON SOFTWARE CO., LTD.  All Rights Reserved. 
  5.  */  
  6. public interface InputViewManager {  
  7.     /** 
  8.      * Initialize the input view. 
  9.      * 
  10.      * @param parent    The OpenWnn object 
  11.      * @param width     The width of the display 
  12.      * @param height    The height of the display 
  13.      * 
  14.      * @return      The input view created in the initialize process; {@code null} if cannot create a input view. 
  15.      */  
  16.     public View initView(OpenWnn parent, int width, int height);  
  17.   
  18.     /** 
  19.      * Get the input view being used currently. 
  20.      * 
  21.      * @return  The input view; {@code null} if no input view is used currently. 
  22.      */  
  23.     public View getCurrentView();  
  24.   
  25.     /** 
  26.      * Notification of updating parent's state. 
  27.      * 
  28.      * @param parent    The OpenWnn object using this manager 
  29.      */  
  30.     public void onUpdateState(OpenWnn parent);  
  31.   
  32.     /** 
  33.      * Reflect the preferences in the input view. 
  34.      * 
  35.      * @param pref    The preferences 
  36.      * @param editor  The information about the editor 
  37.      */  
  38.     public void setPreferences(SharedPreferences pref, EditorInfo editor);  
  39.   
  40.     /** 
  41.      * Close the input view. 
  42.      */  
  43.     public void closing();  
  44. }  
        从这个接口文件中,我们可以看出在输入法处理中,对于键盘部分需要涉及的操作并不是很多。

2、配置项

       这里我们先从简单的部分开始研究。第一个是setPreferences,这只配置项。这一步的工作是读取与键盘部分有关的配置项,在生成键盘(改变键盘)时进行设置。代码中设计到的配置项很减少,只有:震动、声音、是否自动切换大写。在DefaultSoftKeyboard.java中只设置了震动和声音,在DefaultSoftKeyboardJAJP.java添加了是否自动切换为大写。具体大家可以看代码,对于是否自动切换大写,从代码上看,我猜测,有些输入框默认是输入大写的。(这一部分我想不到例子,谁有例子可以share一下)

3、KeyboardView

        在InputViewManager中有initView这个函数,实际上使用来生成KeyboardView的。其源码如下:

在DefaultSoftKeyboard.java中:

[java]  view plain copy
  1. /** @see jp.co.omronsoft.openwnn.InputViewManager#initView */  
  2.    public View initView(OpenWnn parent, int width, int height) {  
  3.        mWnn = parent;  
  4.        mDisplayMode = (width == 320)? PORTRAIT : LANDSCAPE;  
  5.   
  6.        /* 
  7.         * create keyboards & the view. 
  8.         * To re-display the input view when the display mode is changed portrait <-> landscape, 
  9.         * create keyboards every time. 
  10.         */  
  11.        createKeyboards(parent);  
  12.   
  13.        SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(parent);  
  14.        String skin = pref.getString("keyboard_skin",  
  15.                                     mWnn.getResources().getString(R.string.keyboard_skin_id_default));  
  16.        Log.d("OpenWnn""keyboard_skin="+skin);  
  17.        int id = parent.getResources().getIdentifier(skin, "layout""jp.co.omronsoft.openwnn");  
  18.   
  19.        mKeyboardView = (KeyboardView) mWnn.getLayoutInflater().inflate(id, null);  
  20.     mKeyboardView.setOnKeyboardActionListener(this);  
  21.        mCurrentKeyboard = null;  
  22.   
  23.        mMainView = (ViewGroup) parent.getLayoutInflater().inflate(R.layout.keyboard_default_main, null);  
  24.        mSubView = (ViewGroup) parent.getLayoutInflater().inflate(R.layout.keyboard_default_sub, null);  
  25.        if (mDisplayMode == LANDSCAPE && !mHardKeyboardHidden) {   
  26.            mMainView.addView(mSubView);  
  27.        }  
  28.        if (mKeyboardView != null) {  
  29.            mMainView.addView(mKeyboardView);  
  30.        }  
  31.          
  32.        return mMainView;  
  33.    }  
  34.      
        其中我们可以看到,程序先通过layout文件创建KeyboardView,然后通过将其封装在一个mMainView的view变量中。这里很重要的一段代码是,他会去配置项中读取”skin“这一配置项,并通过该配置项去读取对应的layout文件。这一步是动态变换皮肤的关键,也就是说你可以在配置项中选择不同的皮肤,程序会根据你的选择来生成不同的皮肤。

       另外,从SoftKeyboard项目中我们知道,KeyboardView实际上是装着一个Keyboard。但在这一段代码中只有生成Keyboard,并未将Keyboard封装到KeyboardView里面,因为当前键盘变量mCurrentKeyboard是空的。这里我们猜测在继承类中会做封装Keyboard的操作。我们看DefaultSoftKeyboardJAJP.java的代码:

[java]  view plain copy
  1. /** @see jp.co.omronsoft.openwnn.DefaultSoftKeyboard#initView */  
  2.      @Override public View initView(OpenWnn parent, int width, int height) {  
  3.   
  4.         View view = super.initView(parent, width, height);  
  5.         changeKeyboard(mKeyboard[mCurrentLanguage][mDisplayMode][mCurrentKeyboardType][mShiftOn][mCurrentKeyMode][0]);  
  6.           
  7.         return view;  
  8.      }  
        这里changeKeyboard是父类DefaultSoftKeyboard中的函数,其代码为:

[java]  view plain copy
  1. /** 
  2.      * Change the keyboard. 
  3.      * 
  4.      * @param keyboard  The new keyboard 
  5.      * @return          {@code true} if the keyboard is changed; {@code false} if not changed. 
  6.      */  
  7.     protected boolean changeKeyboard(Keyboard keyboard) {  
  8.   
  9.         if (keyboard == null) {  
  10.             return false;  
  11.         }  
  12.         if (mCurrentKeyboard != keyboard) {  
  13.             mKeyboardView.setKeyboard(keyboard);  
  14.             mKeyboardView.setShifted((mShiftOn == 0) ? false : true);  
  15.             mCurrentKeyboard = keyboard;  
  16.             return true;  
  17.         } else {  
  18.             mKeyboardView.setShifted((mShiftOn == 0) ? false : true);  
  19.             return false;  
  20.         }  
  21.     }  
       这里我们可以看到mKeyboardView.setKeyboard(keyboard)这一句,这就验证了我们对KeyboardView实际上是装着一个Keyboard的猜测。

4、Keyboard

      这里的Keyboard异常复杂,以至于需要一个5维数组来维护。

[java]  view plain copy
  1. /** 
  2.      * Keyboard surfaces  
  3.      * <br> 
  4.      * Keyboard[language][portrait/landscape][keyboard type][shift off/on][key-mode] 
  5.      */  
  6.     protected Keyboard[][][][][][] mKeyboard;  
       在DefaultSoftKeyboard.java中有许多函数是在做键盘切换的。另外,这个数组中的每个元素都是一个Keyboard,他们在DefaultSoftKeyboardJAJP.java类中的createKeyboards函数中创建,其中又调用了createKeyboardsPortrait和createKeyboardsLandscape两个函数来做具体的创建工作。具体而言是每个键盘对应一个配置文件,这些文件存放在xml文件夹下面。

       键盘的处理还是比复杂,因为根据不同的输入框,需要显示不同的键盘;同时用户可以选择不同的输入法模式,此时又需要显示不同的键盘。这一点从onUpdateState 函数,以及onKey函数等可以看出。

       另外,有些手机不是纯触摸屏的,也就是带有键盘的手机。对于这些手机是不会显示软键盘的(我猜测),对此需要对硬件盘信息进行设置。其中setHardKeyboardHidden函数就是用于做这些事情的。

5、onUpdateState

       从函数名我们可以看出这一函数的目的是用于更新状态,主要是用户更新键盘状态(或者是切换键盘)。从DefaultSoftKeyboard类中代码可以看出:

[java]  view plain copy
  1. /** @see jp.co.omronsoft.openwnn.InputViewManager#onUpdateState */  
  2. public void onUpdateState(OpenWnn parent) {  
  3.     try {  
  4.         if (parent.mComposingText.size(1) == 0) {  
  5.             if (!mNoInput) {  
  6.                 /* when the mode changed to "no input" */  
  7.                 mNoInput = true;  
  8.                 Keyboard newKeyboard = getKeyboardInputed(false);  
  9.                 if (mCurrentKeyboard != newKeyboard) {  
  10.                     changeKeyboard(newKeyboard);  
  11.                 }  
  12.             }  
  13.         } else {  
  14.             if (mNoInput) {  
  15.                 /* when the mode changed to "input some characters" */  
  16.                 mNoInput = false;  
  17.                 Keyboard newKeyboard = getKeyboardInputed(true);  
  18.                 if (mCurrentKeyboard != newKeyboard) {  
  19.                     changeKeyboard(newKeyboard);  
  20.                 }  
  21.             }  
  22.         }  
  23.     } catch (Exception ex) {  
  24.     }  
  25. }  
其中mNoInput定义为:

[java]  view plain copy
  1. /** 
  2.  * Status of the composing text 
  3.  * <br> 
  4.  * {@code true} if there is no composing text. 
  5.  */  
  6. protected boolean mNoInput = true;  

        这一段代码主要是根据当前的输入状态,来更新键盘的。程序判断当前输入串(这里指输入时,带有下划线的输入串)是否为空。若输入串为空,判断mNoInput是否为有输入串(false),若是,则mNoInput改为true,同时键盘改为没有输入串的状态;若输入串不为空,此时若未没有输入串的状态,则要改为有输入串的状态,并相应修改键盘。

        而DefaultSoftKeyboardJAJP中的这段代码则较为简单:

[java]  view plain copy
  1. /** @see jp.co.omronsoft.openwnn.DefaultSoftKeyboard#onUpdateState */  
  2. @Override public void onUpdateState(OpenWnn parent) {  
  3.     super.onUpdateState(parent);  
  4.     setShiftByEditorInfo();  
  5. }  
[java]  view plain copy
  1. /** 
  2.      * Set the shift key state from {@link EditorInfo}. 
  3.      */  
  4.     private void setShiftByEditorInfo() {  
  5.         if (mEnableAutoCaps && (mCurrentKeyMode == KEYMODE_JA_HALF_ALPHABET)) {  
  6.             int shift = getShiftKeyState(mWnn.getCurrentInputEditorInfo());  
  7.               
  8.             mShiftOn = shift;  
  9.             changeKeyboard(getShiftChangeKeyboard(shift));  
  10.         }  
  11.     }  
        这一部分代码主要是根据当前输入框的特点相应设置对应的键盘。
6、输入方式(直接上屏或者参与变换)

       另外,在使用输入法输入时,通常会有两种方式,一种是参与变换,一种是直接上屏。参与变换是指,你输入的内容与你需要选择的内容不是一致的,而是通过一系列复杂的变换得到的,比如你输入”kawai“得到”可愛“,就是通过变换而来的;而你在输入字符或者数字时,通常是直接上屏的。前者需要显示CandidateView的,而后者不要。

        这一点可能影响的地方包括:不同的输入框(如密码输入框)和不同的输入模式(或者说是输入内容),比如输入数字和字符时,通常就是直接上屏的。

       对于这一部分的技术处理,我们从DefaultSoftKeyboardJAJP类中的changeKeyMode函数可以看出一点端倪。由于该函数代码有点长,就不展现出来了。我们看其中几行代码:

[java]  view plain copy
  1. case KEYMODE_JA_HALF_ALPHABET:  
  2.            if (USE_ENGLISH_PREDICT) {  
  3.                mInputType = INPUT_TYPE_TOGGLE;  
  4.                mode = OpenWnnEvent.Mode.NO_LV1_CONV;  
  5.            } else {  
  6.                mInputType = INPUT_TYPE_TOGGLE;  
  7.                mode = OpenWnnEvent.Mode.DIRECT;  
  8.            }  
  9.            break;  
        如果当前的模式是半角字母输入,则若使用英文预测,则使用的是参与变换的输入方式,若不是用英文预测,则是直接上屏的输入方式。

       另外changeKeyMode函数的前两行是

[java]  view plain copy
  1. int targetMode = keyMode;  
  2.         commitText();  
       这是调用了commitText函数,该函数的主要功能是上屏,也就是将输入的内容输出到输入框(上屏后的内容没有下划线),在切换输入模式时,通常需要先将当前输入的内容上屏。

      另外,需要提一下,在openwnn日文输入法中,有一个功能是切换输入模式,如下图 


       左下角那个按钮,你按一下他会不断变换,从假名输入、英文输入、数字输入循环切换。

       其实现方式如下:

[java]  view plain copy
  1. /** Input mode toggle cycle table */  
  2.    private static final int[] JP_MODE_CYCLE_TABLE = {  
  3.        KEYMODE_JA_FULL_HIRAGANA, KEYMODE_JA_HALF_ALPHABET, KEYMODE_JA_HALF_NUMBER  
  4.    };  
[java]  view plain copy
  1. /** 
  2.  * Change to the next input mode 
  3.  */  
  4. private void nextKeyMode() {  
  5.     /* Search the current mode in the toggle table */  
  6.     boolean found = false;  
  7.     int index;  
  8.     for (index = 0; index < JP_MODE_CYCLE_TABLE.length; index++) {  
  9.         if (JP_MODE_CYCLE_TABLE[index] == mCurrentKeyMode) {  
  10.             found = true;  
  11.             break;  
  12.         }  
  13.     }  
  14.   
  15.     if (!found) {  
  16.         /* If the current mode not exists, set the default mode */  
  17.         setDefaultKeyboard();  
  18.     } else {  
  19.         /* If the current mode exists, set the next input mode */  
  20.         index++;  
  21.         if (JP_MODE_CYCLE_TABLE.length <= index) {  
  22.             index = 0;  
  23.         }  
  24.         changeKeyMode(JP_MODE_CYCLE_TABLE[index]);  
  25.     }  
  26. }  
        这里从程序中就很容易看出是一个循环切换的过程。同时这个函数,在类似如下场合调用:

[java]  view plain copy
  1. /** @see jp.co.omronsoft.openwnn.DefaultSoftKeyboard#onKey */  
  2.    @Override public void onKey(int primaryCode, int[] keyCodes) {  
  3.   
  4.        switch (primaryCode) {  
  5.        case KEYCODE_JP12_TOGGLE_MODE:  
  6.        case KEYCODE_QWERTY_TOGGLE_MODE:  
  7.            nextKeyMode();  
  8.            break;  

7、输入变换

7.1 12key键盘

       在12key的键盘中,是无法显示所有输入内容的。于是你按一个键,可能包含多个信息。比如诺基亚的12键键盘:


       在这种键盘中,你要输入c,则要按三下”2“键才可以。

       同样的,在日文输入法中,如下键盘(软键盘),你是无法显示所有输入内容的,因此,你可能也像用诺基亚键盘一样,需要按多次才可以输入一个内容。


       比如,在如上键盘中,你按”か“键,则按1下、2下、3下、4下,5下,6下……显示的内容分别是:"か","き", "く", "け", "こ","か",……。(注意需要在假名输入模式下)

       这里的程序实现是比较巧妙,其中涉及的代码如下:

[java]  view plain copy
  1. /** Toggle cycle table for full-width HIRAGANA */  
  2.     private static final String[][] JP_FULL_HIRAGANA_CYCLE_TABLE = {  
  3.         {"\u3042""\u3044""\u3046""\u3048""\u304a""\u3041""\u3043""\u3045""\u3047""\u3049"},  
  4.         {"\u304b""\u304d""\u304f""\u3051""\u3053"},  
  5.         {"\u3055""\u3057""\u3059""\u305b""\u305d"},  
  6.         {"\u305f""\u3061""\u3064""\u3066""\u3068""\u3063"},  
  7.         {"\u306a""\u306b""\u306c""\u306d""\u306e"},  
  8.         {"\u306f""\u3072""\u3075""\u3078""\u307b"},  
  9.         {"\u307e""\u307f""\u3080""\u3081""\u3082"},  
  10.         {"\u3084""\u3086""\u3088""\u3083""\u3085""\u3087"},  
  11.         {"\u3089""\u308a""\u308b""\u308c""\u308d"},  
  12.         {"\u308f""\u3092""\u3093""\u308e""\u30fc"},  
  13.         {"\u3001""\u3002""\uff1f""\uff01""\u30fb""\u3000"},  
  14.     };  
          这是一个循环变换table,其内容用utf-8码显示,对此你可能有点迷糊,但是我把他转为日文,你就懂了:

[java]  view plain copy
  1. {"あ""い""う""え""お""ぁ""ぃ""ぅ""ぇ""ぉ"},  
  2.        {"か""き""く""け""こ"},  
  3.        {"さ""し""す""せ""そ"},  
  4.        {"た""ち""つ""て""と""っ"},  
  5.        {"な""に""ぬ""ね""の"},  
  6.        {"は""ひ""ふ""へ""ほ"},  
  7.        {"ま""み""む""め""も"},  
  8.        {"や""ゆ""よ""ゃ""ゅ""ょ"},  
  9.        {"ら""り""る""れ""ろ"},  
  10.        {"わ""を""ん""ゎ""ー"},  
  11.        {"、""。""?""!""・"" "},  
          看到了吧,第二行就是我们刚才按 “键那个例子中显示的内容。因此其实现我们估计也是很简单的,就是不断的读取这一行的内容。其程序如下:

在@Override public void onKey(int primaryCode, int[] keyCodes)函数中:

[java]  view plain copy
  1. case KEYCODE_JP12_SHARP:  
  2.             /* Processing to input by ten key */  
  3.             if (mInputType == INPUT_TYPE_INSTANT) {  
  4.                 /* Send a input character directly if instant input type is selected */  
  5.                 commitText();  
  6.                 mWnn.onEvent(new OpenWnnEvent(OpenWnnEvent.INPUT_CHAR,  
  7.                                               mCurrentInstantTable[getTableIndex(primaryCode)]));  
  8.             } else {  
  9.                 if ((mPrevInputKeyCode != primaryCode)) {  
  10.                     if ((mCurrentKeyMode == KEYMODE_JA_HALF_ALPHABET)  
  11.                             && (primaryCode == KEYCODE_JP12_SHARP)) {  
  12.                         /* Commit text by symbol character (',' '.') when alphabet input mode is selected */  
  13.                         commitText();  
  14.                     }  
  15.                 }  
  16.   
  17.                 /* Convert the key code to the table index and send the toggle event with the table index */  
  18.                 String[][] cycleTable = getCycleTable();  
  19.                 if (cycleTable == null) {  
  20.                     Log.e("OpenWnn""not founds cycle table");  
  21.                 } else {  
  22.                     int index = getTableIndex(primaryCode);  
  23.                     mWnn.onEvent(new OpenWnnEvent(OpenWnnEvent.TOGGLE_CHAR, cycleTable[index]));  
  24.                     mCurrentCycleTable = cycleTable[index];  
  25.                 }  
  26.                 mPrevInputKeyCode = primaryCode;  
  27.             }  
  28.             break;  
       其中 getCycleTable()定义如下:

[java]  view plain copy
  1. /** 
  2.      * Get the toggle table for input that is appropriate in current mode. 
  3.      *  
  4.      * @return      The toggle table for input 
  5.      */  
  6.     private String[][] getCycleTable() {  
  7.         String[][] cycleTable = null;  
  8.         switch (mCurrentKeyMode) {  
  9.         case KEYMODE_JA_FULL_HIRAGANA:  
  10.             cycleTable = JP_FULL_HIRAGANA_CYCLE_TABLE;  
  11.             break;  
  12.   
  13.         case KEYMODE_JA_FULL_KATAKANA:  
  14.             cycleTable = JP_FULL_KATAKANA_CYCLE_TABLE;  
  15.             break;  
  16.   
  17.         case KEYMODE_JA_FULL_ALPHABET:  
  18.             cycleTable = JP_FULL_ALPHABET_CYCLE_TABLE;  
  19.             break;  
  20.   
  21.         case KEYMODE_JA_FULL_NUMBER:  
  22.         case KEYMODE_JA_HALF_NUMBER:  
  23.             /* Because these modes belong to direct input group, No toggle table exists */   
  24.             break;  
  25.   
  26.         case KEYMODE_JA_HALF_ALPHABET:  
  27.             cycleTable = JP_HALF_ALPHABET_CYCLE_TABLE;  
  28.             break;  
  29.   
  30.         case KEYMODE_JA_HALF_KATAKANA:  
  31.             cycleTable = JP_HALF_KATAKANA_CYCLE_TABLE;  
  32.             break;  
  33.   
  34.         default:  
  35.             break;  
  36.         }  
  37.         return cycleTable;  
  38.     }  

       剩下的就不用解释了吧,看代码就懂了。

7.2 输入变换

       大家看了上面的解释,估计会对源码中的后缀为REPLACE_TABLE的变量有了一定的猜测。这些变量是为了输入时变换使用的,大家有没有注意到假名输入和英文输入时,键盘上会有一个“大—小”的转换键。这个键的功能就是基于后缀为REPLACE_TABLE的变量实现的。

       在使用是日文输入时,你输入“あ”,按“大—小”变换键后会变为“ぁ”。于是我们来看一下,平假名的REPLACE_TABLE,该变量名为JP_FULL_HIRAGANA_REPLACE_TABLE,该变量的内容为utf-8码,转为文字显示如下:

[java]  view plain copy
  1. put("あ""ぁ") put("い""ぃ") put("う""ぅ") put("え""ぇ") put("お""ぉ")  
  2.           put("ぁ""あ") put("ぃ""い") put("ぅ""ヴ") put("ぇ""え") put("ぉ""お")  
  3.           put("か""が") put("き""ぎ") put("く""ぐ") put("け""げ") put("こ""ご")  
  4.           put("が""か") put("ぎ""き") put("ぐ""く") put("げ""け") put("ご""こ")  
  5.           put("さ""ざ") put("し""じ") put("す""ず") put("せ""ぜ") put("そ""ぞ")  
  6.           put("ざ""さ") put("じ""し") put("ず""す") put("ぜ""せ") put("ぞ""そ")  
  7.           put("た""だ") put("ち""ぢ") put("つ""っ") put("て""で") put("と""ど")  
  8.           put("だ""た") put("ぢ""ち") put("っ""づ") put("で""て") put("ど""と")  
  9.           put("づ""つ") put("ヴ""う")  
  10.           put("は""ば") put("ひ""び") put("ふ""ぶ") put("へ""べ") put("ほ""ぼ")  
  11.           put("ば""ぱ") put("び""ぴ") put("ぶ""ぷ") put("べ""ぺ") put("ぼ""ぽ")  
  12.           put("ぱ""は") put("ぴ""ひ") put("ぷ""ふ") put("ぺ""へ") put("ぽ""ほ")  
  13.           put("や""ゃ") put("ゆ""ゅ") put("よ""ょ")  
  14.           put("ゃ""や") put("ゅ""ゆ") put("ょ""よ")  
  15.           put("わ""ゎ")  
  16.           put("ゎ""わ")  
  17.           put("゛""゜")  
  18.           put("゜""゛")  
       看到这个表,大家现在知道是为什么了吧。

8、其他

        到这里,我们就将Keyboard和KeyboardView这一部分介绍完成了。相信大家已经对openwnn中这一部分有了必要的了解。

1)具体的键盘定义,也就是如何从一个xml文件生存一个键盘        但是在解析源码中有两部分内容我们并没有说明

2)按键处理

3)DefaultSoftKeyboard.java,DefaultSoftKeyboardJAJP.java文件中关于事件部分代码并没有解释

        对于1)后续会不上,对于2)和3)我感觉放在其他地方介绍会好一点,因此会在后续的文章中介绍2)和3)。至于1)后续有时间会补上。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值