在银行APP里经常要自定义键盘,例如实现下面这样的效果

 

 

 

首先在xml文件里定义键盘

<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
          android:keyWidth="30%p"
          android:horizontalGap="@dimen/keyboard_horizontalGap"
          android:verticalGap="@dimen/keyboard_verticalGap"
          android:keyHeight="47dp">
    <Row>
        <Key android:codes="49" android:keyLabel="1" />
        <Key android:codes="50" android:keyLabel="2" />
        <Key android:codes="51" android:keyLabel="3" />
    </Row>

    <Row>
        <Key android:codes="52" android:keyLabel="4" />
        <Key android:codes="53" android:keyLabel="5" />
        <Key android:codes="54" android:keyLabel="6" />
    </Row>

    <Row>
        <Key android:codes="55" android:keyLabel="7" />
        <Key android:codes="56" android:keyLabel="8" />
        <Key android:codes="57" android:keyLabel="9" />
    </Row>

    <Row>
        <Key android:codes="-2" android:keyLabel="ABC" />
        <Key android:codes="48" android:keyLabel="0" />
        <Key android:codes="-35"
            android:isRepeatable="true"/>
    </Row>
</Keyboard>

keyWidth:每一个按钮的宽度

keyHeight:每一个按钮高度,可以设置百分比

horizontalGap:水平间隔

verticalGap:竖直间隔

Row:一行

每一个按键都将会有一个 codes 值,代表键盘上的按键

KhKeyboardView
public class KhKeyboardView {
    private Activity mContext;
    private View parentView;
    private KeyboardView mLetterView;   //字母键盘view
    private KeyboardView mNumberView;   //数字键盘View
    private Keyboard mNumberKeyboard;   // 数字键盘
    private Keyboard mLetterKeyboard;   // 字母键盘
    private Keyboard mSymbolKeyboard;   // 符号键盘

    private boolean isNumber = true;    // 是否数字键盘
    public static   boolean isUpper = false;    // 是否大写
    private boolean isSymbol = false;   // 是否是符号
    private EditText mEditText;
    private View headerView;

    public void setEditText(EditText text) {
        mEditText = text;
    }

    public KhKeyboardView(Activity context, View view) {
        mContext = context;
        parentView = view;

        mNumberKeyboard = new Keyboard(mContext, R.xml.keyboard_numbers);
        mLetterKeyboard = new Keyboard(mContext, R.xml.keyboard_word);
        mSymbolKeyboard = new Keyboard(mContext, R.xml.keyboard_symbol);
        mNumberView = (KeyboardView) parentView.findViewById(R.id.keyboard_view);
        mLetterView = (KeyboardView) parentView.findViewById(R.id.keyboard_view_2);

        mNumberView.setKeyboard(mNumberKeyboard);
        mNumberView.setEnabled(true);
        mNumberView.setPreviewEnabled(false);
        mNumberView.setOnKeyboardActionListener(listener);
        mLetterView.setKeyboard(mLetterKeyboard);
        mLetterView.setEnabled(true);
        mLetterView.setPreviewEnabled(true);
        mLetterView.setOnKeyboardActionListener(listener);
        headerView = parentView.findViewById(R.id.keyboard_header);

    }

    private KeyboardView.OnKeyboardActionListener listener = new KeyboardView.OnKeyboardActionListener() {
        /**
         * 按下,在onKey之前,可以在这里做一些操作,这里让有的没有按下的悬浮提示
         * @param primaryCode
         */
        @Override
        public void onPress(int primaryCode) {
            Log.d("primaryCode","onPress--"+primaryCode);
            if (primaryCode == Keyboard.KEYCODE_SHIFT) {
                mLetterView.setPreviewEnabled(false);
            } else if (primaryCode == Keyboard.KEYCODE_DELETE) {
                mLetterView.setPreviewEnabled(false);
            } else if (primaryCode == 32) {
                mLetterView.setPreviewEnabled(false);
            } else {
                mLetterView.setPreviewEnabled(true);
            }

        }

        /**
         * 松开
         * @param primaryCode
         */
        @Override
        public void onRelease(int primaryCode) {
            Log.d("primaryCode","onRelease--"+primaryCode);
        }

        /**
         * 按下
         * @param primaryCode
         * @param keyCodes
         */
        @Override
        public void onKey(int primaryCode, int[] keyCodes) {
            Log.d("primaryCode","onKey--"+primaryCode);
            try {
                if (mEditText == null)
                    return;
                Editable editable = mEditText.getText();
                int start = mEditText.getSelectionStart();
                if (primaryCode == Keyboard.KEYCODE_CANCEL) {
                    // 隐藏键盘
                    hideKeyboard();
                } else if (primaryCode == Keyboard.KEYCODE_DELETE || primaryCode == -35) {

                    // 回退键,删除字符
                    if (editable != null && editable.length() > 0) {
                        if (start > 0) {
                            editable.delete(start - 1, start);
                        }
                    }
                } else if (primaryCode == Keyboard.KEYCODE_SHIFT) {
                    // 大小写切换
                    changeKeyboart();
                    mLetterView.setKeyboard(mLetterKeyboard);

                } else if (primaryCode == Keyboard.KEYCODE_MODE_CHANGE) {
                    // 数字与字母键盘互换
                    if (isNumber) {
                        showLetterView();
                        showLetterView2();
                    } else {
                        showNumberView();
                    }

                } else if (primaryCode == 90001) {
//                  字母与符号切换
                    if (isSymbol) {
                        showLetterView2();
                    } else {
                        showSymbolView();
                    }

                } else {
                    // 输入键盘值
                    editable.insert(start, Character.toString((char) primaryCode));
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

        }

        @Override
        public void onText(CharSequence text) {

        }

        @Override
        public void swipeLeft() {

        }

        @Override
        public void swipeRight() {

        }

        @Override
        public void swipeDown() {

        }

        @Override
        public void swipeUp() {

        }
    };

    //  字母-符号,显示字母
    private void showLetterView2() {
        if (mLetterView != null) {
            isSymbol = false;
            mLetterView.setKeyboard(mLetterKeyboard);
        }
    }

    //  字母-符号,显示符号
    private void showSymbolView() {
        try {
            if (mLetterKeyboard != null) {
                isSymbol = true;
                mLetterView.setKeyboard(mSymbolKeyboard);
            }
        } catch (Exception e) {
        }
    }

    //  数字-字母,显示字母键盘
    private void showLetterView() {
        try {
            if (mLetterView != null && mNumberView != null) {
                isNumber = false;
                mLetterView.setVisibility(View.VISIBLE);
                mNumberView.setVisibility(View.INVISIBLE);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    // 数字-字母, 显示数字键盘
    private void showNumberView() {
        try {
            if (mLetterView != null && mNumberView != null) {
                isNumber = true;
                mLetterView.setVisibility(View.INVISIBLE);
                mNumberView.setVisibility(View.VISIBLE);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 切换大小写
     */
    private void changeKeyboart() {
        List<Keyboard.Key> keyList = mLetterKeyboard.getKeys();
        if (isUpper) {
            // 大写切换小写
            isUpper = false;
            for (Keyboard.Key key : keyList) {
                Drawable icon = key.icon;

                if (key.label != null && isLetter(key.label.toString())) {
                    key.label = key.label.toString().toLowerCase();
                    key.codes[0] = key.codes[0] + 32;
                }
            }
        } else {
            // 小写切换成大写
            isUpper = true;
            for (Keyboard.Key key : keyList) {
                if (key.label != null && isLetter(key.label.toString())) {
                    key.label = key.label.toString().toUpperCase();
                    key.codes[0] = key.codes[0] - 32;
                }
            }
        }
    }

    /**
     * 判断是否是字母
     */
    private boolean isLetter(String str) {
        String wordStr = "abcdefghijklmnopqrstuvwxyz";
        return wordStr.contains(str.toLowerCase());
    }

    public void hideKeyboard() {
        try {
            int visibility = mLetterView.getVisibility();
            if (visibility == View.VISIBLE) {
                headerView.setVisibility(View.GONE);
                mLetterView.setVisibility(View.GONE);
            }
            visibility = mNumberView.getVisibility();
            if (visibility == View.VISIBLE) {
                headerView.setVisibility(View.GONE);
                mNumberView.setVisibility(View.GONE);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 显示键盘
     *
     * @param editText
     */
    public void showKeyboard(EditText editText) {
        try {
            this.mEditText = editText;
            int visibility = 0;
            int inputText = mEditText.getInputType();
            headerView.setVisibility(View.VISIBLE);
            switch (inputText) {
                case InputType.TYPE_CLASS_NUMBER:
                    showNumberView();
                    break;
                case InputType.TYPE_CLASS_PHONE:
                    showNumberView();
                    break;
                case InputType.TYPE_NUMBER_FLAG_DECIMAL:
                    showNumberView();
                    break;
                default:
                    showLetterView();
                    break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

 

布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/rl_key"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#00000000"
    android:orientation="vertical">

    <View
        android:id="@+id/keyboard_back_hide"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#7d7d7d"
        android:orientation="vertical">

        <RelativeLayout
            android:id="@+id/keyboard_header"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:visibility="visible">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:text="智能安全加密键盘"
                android:textColor="#bfbfbf"
                android:textSize="15sp" />

            <TextView
                android:id="@+id/keyboard_finish"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:layout_marginRight="10dp"
                android:padding="14dp"
                android:text="完成"
                android:textColor="#ffffff"
                android:textSize="15sp" />
        </RelativeLayout>

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:layout_marginBottom="10dp"
            android:background="#555457" />

        <FrameLayout
            android:id="@+id/keyboard_layer"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <com.kh.keyboard.CustomKeyboardView
                android:id="@+id/keyboard_view"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="#7d7d7d"
                android:focusable="true"
                android:focusableInTouchMode="true"
                android:keyBackground="@drawable/keyboard_number_selector_bg"
                android:keyPreviewLayout="@null"
                android:keyTextColor="#ffffff"
                android:visibility="gone" />

            <com.kh.keyboard.CustomKeyboardView
                android:id="@+id/keyboard_view_2"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="#7d7d7d"
                android:focusable="true"
                android:focusableInTouchMode="true"
                android:keyBackground="@drawable/keyboard_selector_bg"
                android:keyPreviewHeight="90dp"
                android:keyPreviewLayout="@layout/keyboard_key_preview_layout"
                android:keyPreviewOffset="45dp"
                android:keyTextColor="#ffffff"
                android:visibility="gone" />
        </FrameLayout>

    </LinearLayout>
</RelativeLayout>

  

 
keyPreviewLayout就是点击时键盘按键上的悬浮效果

 
这里自定义了KeyboardView,因为我需要按钮的背景颜色不一样,而使用keyBackground都是一样的
public class CustomKeyboardView extends KeyboardView {
    private Context context;

    public CustomKeyboardView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        try {
            List<Keyboard.Key> keys = getKeyboard().getKeys();
            for (Keyboard.Key key : keys) {
                //   Log.e("KEY", "Drawing key with code " + key.codes[0]);
                if (key.codes[0] == -5) {
                    Drawable dr = (Drawable) context.getResources().getDrawable(R.drawable.keyboard_word_del_layerlist);
                    dr.setBounds(key.x, key.y, key.x + key.width, key.y + key.height);
                    dr.draw(canvas);
                } else if (key.codes[0] == -35) {
                    Drawable dr = (Drawable) context.getResources().getDrawable(R.drawable.keyboard_word_del_layerlist2);
                    dr.setBounds(key.x, key.y, key.x + key.width, key.y + key.height);
                    dr.draw(canvas);
                } else if (key.codes[0] == -1) {
                    Drawable dr = (Drawable) context.getResources().getDrawable(R.drawable.keyboard_word_shift_layerlist);
                    Drawable dr_da = (Drawable) context.getResources().getDrawable(R.drawable.keyboard_word_shift_layerlist_da);
                    dr.setBounds(key.x, key.y, key.x + key.width, key.y + key.height);
                    dr_da.setBounds(key.x, key.y, key.x + key.width, key.y + key.height);

                    if (KhKeyboardView.isUpper) {
                        dr_da.draw(canvas);
                    } else {
                        dr.draw(canvas);
                    }

                } else if (key.codes[0] == -2 || key.codes[0] == 90001) {
                    Drawable dr = (Drawable) context.getResources().getDrawable(R.drawable.keyboard_selector_blue_bg);
                    dr.setBounds(key.x, key.y, key.x + key.width, key.y + key.height);
                    dr.draw(canvas);
                    drawText(canvas, key);
                } else {

                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void drawText(Canvas canvas, Keyboard.Key key) {
        try {
            Rect bounds = new Rect();
            Paint paint = new Paint();
            paint.setTextAlign(Paint.Align.CENTER);

            paint.setAntiAlias(true);

            paint.setColor(Color.WHITE);

            if (key.label != null) {
                String label = key.label.toString();

                Field field;

                if (label.length() > 1 && key.codes.length < 2) {
                    int labelTextSize = 0;
                    try {
                        field = KeyboardView.class.getDeclaredField("mLabelTextSize");
                        field.setAccessible(true);
                        labelTextSize = (int) field.get(this);
                    } catch (NoSuchFieldException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                    paint.setTextSize(labelTextSize);
                    paint.setTypeface(Typeface.DEFAULT_BOLD);
                } else {
                    int keyTextSize = 0;
                    try {
                        field = KeyboardView.class.getDeclaredField("mLabelTextSize");
                        field.setAccessible(true);
                        keyTextSize = (int) field.get(this);
                    } catch (NoSuchFieldException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                    paint.setTextSize(keyTextSize);
                    paint.setTypeface(Typeface.DEFAULT);
                }

                paint.getTextBounds(key.label.toString(), 0, key.label.toString()
                        .length(), bounds);
                canvas.drawText(key.label.toString(), key.x + (key.width / 2),
                        (key.y + key.height / 2) + bounds.height() / 2, paint);
            } else if (key.icon != null) {
                key.icon.setBounds(key.x + (key.width - key.icon.getIntrinsicWidth()) / 2, key.y + (key.height - key.icon.getIntrinsicHeight()) / 2,
                        key.x + (key.width - key.icon.getIntrinsicWidth()) / 2 + key.icon.getIntrinsicWidth(), key.y + (key.height - key.icon.getIntrinsicHeight()) / 2 + key.icon.getIntrinsicHeight());
                key.icon.draw(canvas);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}
 
最后
还需要一个工具类来显示自定义的键盘,这里我使用了dialog
public class KeyBoardDialogUtils implements View.OnClickListener {
    protected View view;
    protected Dialog popWindow;
    protected Activity mContext;
    private EditText contentView;
    private List<String> contentList;
    private KhKeyboardView keyboardUtil;

    public KeyBoardDialogUtils(Activity mContext) {
        try {
            this.mContext = mContext;
            if (contentList == null) {
                contentList = new ArrayList<>();
            }

            if (popWindow == null) {
                view = LayoutInflater.from(mContext).inflate(R.layout.keyboard_key_board_popu, null);
                view.findViewById(R.id.keyboard_finish).setOnClickListener(this);
                view.findViewById(R.id.keyboard_back_hide).setOnClickListener(this);
            }
            popWindow.setContentView(view);
            popWindow.setCanceledOnTouchOutside(true);
            Window mWindow = popWindow.getWindow();
            mWindow.setWindowAnimations(R.style.keyboard_popupAnimation);
            mWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
            mWindow.setGravity(Gravity.BOTTOM | Gravity.FILL_HORIZONTAL);
            mWindow.setLayout(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT);
            popWindow.setOnDismissListener(new DialogInterface.OnDismissListener() {

                @Override
                public void onDismiss(DialogInterface dialog) {
                    if (contentView != null && contentView.isFocused()) {
                        contentView.clearFocus();
                    }
                }
            });
            initView();

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    private void initView() {
        try {
            if (keyboardUtil == null)
                keyboardUtil = new KhKeyboardView(mContext, view);

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public void show(final EditText editText) {
        editText.setFocusable(true);
        editText.setFocusableInTouchMode(true);
        editText.requestFocus();
        popWindow.show();
        keyboardUtil.showKeyboard(editText);
    }

    public void dismiss() {
        if (popWindow != null && popWindow.isShowing()) {
            popWindow.dismiss();

        }

    }

    @Override
    public void onClick(View v) {
        try {
            int i = v.getId();
            if (i == R.id.keyboard_finish) {
                keyboardUtil.hideKeyboard();
                dismiss();

            } else if (i == R.id.keyboard_back_hide) {
                keyboardUtil.hideKeyboard();
                dismiss();

            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

  

使用

        et = (EditText) findViewById(R.id.et);
        keyBoardDialogUtils = new KeyBoardDialogUtils(this);
        et.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                keyBoardDialogUtils.show(et);
            }
        });
注意这里点击会先弹出系统键盘,因为弹出键盘会先于keyBoardDialogUtils.show(et)执行,所以设置EditText的focusableInTouchMode="false",在keyutil里我们再把它设为true。
 
 
项目源码:
 
 
 
 
 
 
 

Android自定义安全键盘的更多相关文章

  1. Android开发案例 - 自定义虚拟键盘

    所有包含IM功能的App(如微信, 微博, QQ, 支付宝等)都提供了Emoji表情之类的虚拟键盘,  如下图:    本文只着重介绍如何实现输入法键盘和自定义虚拟键盘的流畅切换, 而不介绍如何实现虚 ...

  2. 关于在android手机中腾讯、阿里产品不自定义虚拟键盘的想法

    1,自定义虚拟键盘,影响用户体验.你每个用户的喜好不一样,都有自己心仪的一款输入法.腾讯或是阿里设计出来的输入法很难满足上亿用户的喜好,到时候又是一场口水战,再说了就是专业的输入法肯定要比应用里嵌套的 ...

  3. (转)[原] Android 自定义View 密码框 例子

    遵从准则 暴露您view中所有影响可见外观的属性或者行为. 通过XML添加和设置样式 通过元素的属性来控制其外观和行为,支持和重要事件交流的事件监听器 详细步骤见:Android 自定义View步骤 ...

  4. Android 自定义View合集

    自定义控件学习 https://github.com/GcsSloop/AndroidNote/tree/master/CustomView 小良自定义控件合集 https://github.com/ ...

  5. [原] Android 自定义View 密码框 例子

    遵从准则 暴露您view中所有影响可见外观的属性或者行为. 通过XML添加和设置样式 通过元素的属性来控制其外观和行为,支持和重要事件交流的事件监听器 详细步骤见:Android 自定义View步骤 ...

  6. android自定义view系列:认识activity结构

    标签: android 自定义view activity 开发中虽然我们调用Activity的setContentView(R.layout.activity_main)方法显示View视图,但是vi ...

  7. android 自定义view 前的基础知识

    本篇文章是自己自学自定义view前的准备,具体参考资料来自 Android LayoutInflater原理分析,带你一步步深入了解View(一) Android视图绘制流程完全解析,带你一步步深入了 ...

  8. 【朝花夕拾】Android自定义View篇之(八)多点触控(上)MotionEvent简介

    前言 在前面的文章中,介绍了不少触摸相关的知识,但都是基于单点触控的,即一次只用一根手指.但是在实际使用App中,常常是多根手指同时操作,这就需要用到多点触控相关的知识了.多点触控是在Android2 ...

  9. 【朝花夕拾】Android自定义View篇之(六)Android事件分发机制(中)从源码分析事件分发逻辑及经常遇到的一些“诡异”现象

    前言 转载请注明,转自[https://www.cnblogs.com/andy-songwei/p/11039252.html]谢谢! 在上一篇文章[[朝花夕拾]Android自定义View篇之(五 ...

随机推荐

  1. intellij idea svn使用一 导入、更新、提交、解决冲突

    大体上是转载,针对版本14有一些特殊的添加. 查看svn的资源库: 下面的多出了一个svn的窗口,在左边有加号可以添加一个svn的库 输入svn的地址,我用的是本地的测试,所以地址为svn://127 ...

  2. python_web----------数据可视化从0到1的过程

    一.数据可视化项目配置 1. django + Echarts 2. 服务器(linux:Ubuntu 17.04 (GNU/Linux 4.10.0-40-generic x86_64)) 3. I ...

  3. 使用canvas实现绚丽的时钟特效

    源码 https://github.com/2016Messi/Gorgeous-clock 效果展示 https://2016messi.github.io/Gorgeous-clock/ 如果各位 ...

  4. 一次对象过大引起的gc性能问题的分析与定位

    现象:一个接口在4C的机器上跑最大只有7TPS,CPU使用率就已经90%多. 定位: 1.  使用top命令查看CPU使用情况,找到进程号 2.  使用top -H -pid命令,查看进程信息,看到有 ...

  5. Python进阶之迭代器和生成器

    可迭代对象 Python中任意的对象,只要它定义了可以返回一个迭代器的__iter__方法,或者定义了可以支持下标索引的__getitem__方法,那么它就是一个可迭代对象.简单来说,可迭代对象就是能 ...

  6. Python学习日记:day7-----集合

    1.基础数据类型汇总补充 1,list: 在循环一个列表是,最好不能删除列表中的元素. 2,bool 空列表.int:0.空str.空dict.空set--->bool:false 其余为tru ...

  7. 在IntelliJ IDEA里创建简单的基于Maven的SpringMVC项目

    后来发现了一种更加方便的创建方式,即第一步不选择Create from archetype,创建完毕后打开Project Structure-Modules,然后添加Web,但是注意添加的Web里面的 ...

  8. Memcached的简介和使用

    缘起: 在数据驱动的web开发中,经常要重复从数据库中取出相同的数据,这种重复极大的增加了数据库负载.缓存是解决这个问题的好办法.但是ASP.NET中的虽然已经可以实现对页面局部进行缓存,但还是不够灵 ...

  9. window下mysql数据备份

    今天我有个朋友让我帮他在windowServer服务器上备份一下mysql的数据库,于是花了一天的时间完成了一个每天定时备份数据库的功能,小编在这里为大家记录一下: 首先对于mysql命令行的导入导出 ...

  10. AdaBoost对实际数据分类的Julia实现

    写在前面 AdaBoost是机器学习领域一个很重要很流行的算法,而Julia是一门新兴的发展迅速的科学计算语言.本文将从一个实际例子出发,展示如何用Julia语言实现AdaBoost算法. 什么是Ad ...