Android10 InputManagerService事件输入输出_register input event receiver-程序员宅基地

技术标签: android系统  android  

1、Android10 源码编译相关问题

2、Android10 系统进程Zygote启动

3、Android10 系统进程SystemServer

4、Android10 launcher启动流程

5、Android10 系统发送开机广播时机

6、Android10 AppComponentFactory源码梳理

7、Android10 InputManagerService事件输入输出

8、Android10 InputManagerService本地实现

9、Android10 SystemUI系统手势导航


        InputManagerService管理着屏幕点击以及硬件按钮事件的输入输出,InputManagerService的实现是在native代码中,想要对事件进行处理,那就一定要通过InputManagerService进行注册或是监听。如果A应用要想获取到输入事件,那要怎么和InputManagerService连接起来呢?答案是Socket,其中InputChannel就是对其进行封装,InputChannel的实现同样是native代码,实现的类是NativeInputChannel,类路径:frameworks/base/core/jni/android_view_InputChannel.cpp,这里就直接看下InputChannel的openInputChannelPair():

    /**
     * 创建一个新的输入通道对。 一个通道提供给输入调度程序,另一个通道提供给应用程序的输入列。  
     * @param name通道对的描述性(非唯一)名称。  
     * @return 一对输入通道。 第一个通道被指定为服务器通道,应该用于发布输入事件。 第二个通道被指                
     * 定为客户端通道,用于使用输入事件。  
     */
    public static InputChannel[] openInputChannelPair(String name) {
        if (name == null) {
            throw new IllegalArgumentException("name must not be null");
        }

        if (DEBUG) {
            Slog.d(TAG, "Opening input channel pair '" + name + "'");
        }
        return nativeOpenInputChannelPair(name);
    }

    private static native InputChannel[] nativeOpenInputChannelPair(String name);

openInputChannelPair()会创建一对输入通道,一端用于服务器通道,也就是InputManagerService;一端用于客户端,也就是应用这一端。对InputChannel有个初步的了解后,接下来就看主角com.android.server.input.InputManagerService:

public class InputManagerService extends IInputManager.Stub
        implements Watchdog.Monitor {

    // Pointer to native input manager service object.
    private final long mPtr;
    //所有输入事件在分发前,会优先派发到这个回调中处理
    private WindowManagerCallbacks mWindowManagerCallbacks;
    //内部会初始化两个线程,一个用于读取底层的输入事件,一个用于将事件派发到应用层
    private static native long nativeInit(InputManagerService service,
            Context context, MessageQueue messageQueue);
    //前面说InputChannel时会创建一对输入通过对,这里就是将其中的一个注册到底层,也就是所说的服务端
    //InputChannel只会收到注册页面的输入事件
    private static native void nativeRegisterInputChannel(long ptr, InputChannel inputChannel,
            int displayId);
    //解注册页面注册的输入事件
    private static native void nativeUnregisterInputChannel(long ptr, InputChannel inputChannel);
    //这个方法是添加监听,InputChannel会收到所有的输入事件,
    //其中isGestureMonitor表示收到的事件是否是手势,系统的全局手势就是通过这个方法注册的
    private static native void nativeRegisterInputMonitor(long ptr, InputChannel inputChannel,
            int displayId, boolean isGestureMonitor);
    //注入模拟屏幕的点击事件,比如实现全局的返回按钮
    private static native int nativeInjectInputEvent(long ptr, InputEvent event,
            int injectorPid, int injectorUid, int syncMode, int timeoutMillis,
            int policyFlags);

    public InputManagerService(Context context) {
        ... ...
        mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());
        ... ...
    }

    //这个回调是在SystemServer中设置的,其实现是InputManagerCallback,最终会调用到PhoneWindowManager
    public void setWindowManagerCallbacks(WindowManagerCallbacks callbacks) {
        mWindowManagerCallbacks = callbacks;
    }

    /**
     * Creates an input channel that will receive all input from the input dispatcher.
     * @param inputChannelName The input channel name.
     * @param displayId Target display id.
     * @return The input channel.
     */
    public InputChannel monitorInput(String inputChannelName, int displayId) {
        if (inputChannelName == null) {
            throw new IllegalArgumentException("inputChannelName must not be null.");
        }

        if (displayId < Display.DEFAULT_DISPLAY) {
            throw new IllegalArgumentException("displayId must >= 0.");
        }

        InputChannel[] inputChannels = InputChannel.openInputChannelPair(inputChannelName);
        // Give the output channel a token just for identity purposes.
        inputChannels[0].setToken(new Binder());
        nativeRegisterInputMonitor(mPtr, inputChannels[0], displayId, false /*isGestureMonitor*/);
        inputChannels[0].dispose(); // don't need to retain the Java object reference
        return inputChannels[1];
    }

    /**
     * Creates an input monitor that will receive pointer events for the purposes of system-wide
     * gesture interpretation.
     *
     * @param inputChannelName The input channel name.
     * @param displayId Target display id.
     * @return The input channel.
     */
    @Override // Binder call
    public InputMonitor monitorGestureInput(String inputChannelName, int displayId) {
        if (!checkCallingPermission(android.Manifest.permission.MONITOR_INPUT,
                "monitorInputRegion()")) {
            throw new SecurityException("Requires MONITOR_INPUT permission");
        }

        Objects.requireNonNull(inputChannelName, "inputChannelName must not be null.");

        if (displayId < Display.DEFAULT_DISPLAY) {
            throw new IllegalArgumentException("displayId must >= 0.");
        }


        final long ident = Binder.clearCallingIdentity();
        try {
            InputChannel[] inputChannels = InputChannel.openInputChannelPair(inputChannelName);
            InputMonitorHost host = new InputMonitorHost(inputChannels[0]);
            inputChannels[0].setToken(host.asBinder());
            nativeRegisterInputMonitor(mPtr, inputChannels[0], displayId,
                    true /*isGestureMonitor*/);
            return new InputMonitor(inputChannelName, inputChannels[1], host);
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }

    /**
     * Registers an input channel so that it can be used as an input event target.
     * @param inputChannel The input channel to register.
     * @param inputWindowHandle The handle of the input window associated with the
     * input channel, or null if none.
     */
    public void registerInputChannel(InputChannel inputChannel, IBinder token) {
        if (inputChannel == null) {
            throw new IllegalArgumentException("inputChannel must not be null.");
        }

        if (token == null) {
            token = new Binder();
        }
        inputChannel.setToken(token);

        nativeRegisterInputChannel(mPtr, inputChannel, Display.INVALID_DISPLAY);
    }

    /**
     * Unregisters an input channel.
     * @param inputChannel The input channel to unregister.
     */
    public void unregisterInputChannel(InputChannel inputChannel) {
        if (inputChannel == null) {
            throw new IllegalArgumentException("inputChannel must not be null.");
        }

        nativeUnregisterInputChannel(mPtr, inputChannel);
    }

    @Override // Binder call
    public boolean injectInputEvent(InputEvent event, int mode) {
        return injectInputEventInternal(event, mode);
    }

    private boolean injectInputEventInternal(InputEvent event, int mode) {
        ... ...
        final int pid = Binder.getCallingPid();
        final int uid = Binder.getCallingUid();
        final long ident = Binder.clearCallingIdentity();
        final int result;
        try {
            result = nativeInjectInputEvent(mPtr, event, pid, uid, mode,
                    INJECTION_TIMEOUT_MILLIS, WindowManagerPolicy.FLAG_DISABLE_KEY_REPEAT);
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
        ... ...
    }

    // Native callback.
    // 有输入事件时,这里会最先调用到
    private int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
        return mWindowManagerCallbacks.interceptKeyBeforeQueueing(event, policyFlags);
    }

    // Native callback.
    // 如果interceptKeyBeforeQueueing没有处理,在事件分发给应用端前会调用到
    private long interceptKeyBeforeDispatching(IBinder focus, KeyEvent event, int policyFlags) {
        return mWindowManagerCallbacks.interceptKeyBeforeDispatching(focus, event, policyFlags);
    }

}

这里先把InputManagerService的底层实现看成一个沙盒,下一篇文章再聊,这里先来聊聊上面列出的这些方法,类中有一个类型是WindowManagerCallbacks的mWindowManagerCallbacks成员变量,其实现类是InputManagerCallback,但最终调用到的是PhoneWindowManager,事件在分发给应用前,会分别调用interceptKeyBeforeQueueing()和interceptKeyBeforeDispatching(),这也就是说PhoneWindowManager是最先处理输入事件的地方。

        这里先说个小插曲,interceptKeyBeforeQueueing()这个方法为什么是这样命名的,最开始看到这个名字还是挺疑惑的,后面看了底层代码才明白,InputManagerService的底层实现开启了两个线程,一个用于读输入事件(读线程),一个用于将读取的到事件分发(分发线程),读线程如何将事件传递到分发线程呢,这里就会涉及到队列(Queue),所以现在看到这个interceptKeyBeforeQueueing()是不是就很明白了。

对于上面其他的方法,先来看下方法的命名,monitorXXX()、registerXXX()、unregisterXXX()、injectXXX(),可以分为三类:

1、带有monitorXXX()的方法,表示只要有输入事件就会接收到;

2、injectXXX()就是模拟按键发送事件了;

3、registerXXX()和unRegisterXXX()这是成对出现的,这对方法有什么作用呢?一个页面要想接收到输入事件,那就必须调用registerXXX()进行对接收事件的注册,页面销毁了就调用解注册;

下面就来看下android源码中对这些方法的使用:

1、frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java,这是对系统手势处理的类,来看下它内部的使用:

    private void updateIsEnabled() {
        ... ...
        // Register input event receiver
        mInputMonitor = InputManager.getInstance().monitorGestureInput("edge-swipe", mDisplayId);
        //android 10实现方式
        mInputEventReceiver = new SysUiInputEventReceiver(
                    mInputMonitor.getInputChannel(), Looper.getMainLooper());
        //android 11实现方式,两种实现方式是一样的,最终都会回调到这个类的onInputEvent()方法
        mInputEventReceiver = new InputChannelCompat.InputEventReceiver(mInputMonitor.getInputChannel(), 
Looper.getMainLooper(),Choreographer.getInstance(), this::onInputEvent);
        ... ...
    }

调用monitor监听后,当有输入事件输入,就会调用到这里的onInputEvent()方法。

2、对于injectInputEvent()方法,在InputManager中有@hide标识,也就是只能在系统中使用,那现在要怎么才能使用呢?在上一篇文章Android10 AppComponentFactory源码梳理有提到android.app.Instrumentation这个类,来看下它是怎么使用的:

    //需要传入的是KeyEvent中Keycode的常量,比如:KeyEvent.KEYCODE_BACK就是执行返回按键的功能
    public void sendKeyDownUpSync(int key) {        
        sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, key));
        sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, key));
    }

    public void sendKeySync(KeyEvent event) {
        validateNotAppThread();

        long downTime = event.getDownTime();
        long eventTime = event.getEventTime();
        int source = event.getSource();
        if (source == InputDevice.SOURCE_UNKNOWN) {
            source = InputDevice.SOURCE_KEYBOARD;
        }
        if (eventTime == 0) {
            eventTime = SystemClock.uptimeMillis();
        }
        if (downTime == 0) {
            downTime = eventTime;
        }
        KeyEvent newEvent = new KeyEvent(event);
        newEvent.setTime(downTime, eventTime);
        newEvent.setSource(source);
        newEvent.setFlags(event.getFlags() | KeyEvent.FLAG_FROM_SYSTEM);
        InputManager.getInstance().injectInputEvent(newEvent,
                InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

所以,要想实现按键功能就可以通过上面的方法。

3、下面重点来看下registerXX(),这个对于每个显示的页面都会调用到,只是在frameworks层调用了,对于应用层来说无感而已,应用层的所有的处理流程都是从ViewRootImpl开始的,当界面显示时,会调用ViewRootImpl.setView():

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        ... ...
        if ((mWindowAttributes.inputFeatures
                        & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
            mInputChannel = new InputChannel();
        }
        ... ...
        res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
                            mTempInsets);
        ... ...
        mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
                            Looper.myLooper());
        ... ...

    }

先是new了一个InputChannel,但是并没有去创建通道对,也就是说这个InputChannel还没有初始化,这里的mWindowSession是通过WindowManagerService.openSession()返回,其实现是Session类,调用它的addToDisplay()最终调用到的是WindowManagerService.addWindow():

WindowManagerService
    public int addWindow(Session session, IWindow client, int seq,
            LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
            Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
            DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
            InsetsState outInsetsState) {
        ... ...
        final WindowState win = new WindowState(this, session, client, token, parentWindow,
                    appOp[0], seq, attrs, viewVisibility, session.mUid,
                    session.mCanAddInternalSystemWindow);
        ... ...
        win.openInputChannel(outInputChannel);
        ... ...

    }

WindowState
    void openInputChannel(InputChannel outInputChannel) {
        if (mInputChannel != null) {
            throw new IllegalStateException("Window already has an input channel.");
        }
        String name = getName();
        InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
        mInputChannel = inputChannels[0];
        mClientChannel = inputChannels[1];
        mInputWindowHandle.token = mClient.asBinder();
        if (outInputChannel != null) {
            //初始化应用端的InputChannel,实际就是初始化mPtr,这个变量通过native代码可以转化成指针
            mClientChannel.transferTo(outInputChannel);
            mClientChannel.dispose();
            mClientChannel = null;
        } else {
            // If the window died visible, we setup a dummy input channel, so that taps
            // can still detected by input monitor channel, and we can relaunch the app.
            // Create dummy event receiver that simply reports all events as handled.
            mDeadWindowEventReceiver = new DeadWindowEventReceiver(mClientChannel);
        }
        mWmService.mInputManager.registerInputChannel(mInputChannel, mClient.asBinder());
    }

这里通过InputChannel创建了一对通道对inputChannels[0]和inputChannels[1],然后将inputChannels[1]和应用端的InputChannel关联起来,将inputChannels[0]和服务端(事件原始分发)关联起来,这样就将原始输入事件和应用界面关联起来了,再回到ViewRootImpl.setView(),之后还创建了一个WindowInputEventReceiver对象,这一看就是接受输入事件的了,WindowInputEventReceiver基础自InputEventReceiver,这里主要来看下InputEventReceiver:

public abstract class InputEventReceiver {

    private long mReceiverPtr;

    // We keep references to the input channel and message queue objects here so that
    // they are not GC'd while the native peer of the receiver is using them.
    private InputChannel mInputChannel;
    private MessageQueue mMessageQueue;

    private final SparseIntArray mSeqMap = new SparseIntArray();

    private static native long nativeInit(WeakReference<InputEventReceiver> receiver,
            InputChannel inputChannel, MessageQueue messageQueue);

    public InputEventReceiver(InputChannel inputChannel, Looper looper) {
        ... ...
        mInputChannel = inputChannel;
        mMessageQueue = looper.getQueue();
        mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),
                inputChannel, mMessageQueue);
    }

    // Called from native code.
    @SuppressWarnings("unused")
    @UnsupportedAppUsage
    private void dispatchInputEvent(int seq, InputEvent event) {
        mSeqMap.put(event.getSequenceNumber(), seq);
        onInputEvent(event);
    }

    @UnsupportedAppUsage
    public void onInputEvent(InputEvent event) {
        finishInputEvent(event, false);
    }

    public final void finishInputEvent(InputEvent event, boolean handled) {
        if (event == null) {
            throw new IllegalArgumentException("event must not be null");
        }
        if (mReceiverPtr == 0) {
            Log.w(TAG, "Attempted to finish an input event but the input event "
                    + "receiver has already been disposed.");
        } else {
            int index = mSeqMap.indexOfKey(event.getSequenceNumber());
            if (index < 0) {
                Log.w(TAG, "Attempted to finish an input event that is not in progress.");
            } else {
                int seq = mSeqMap.valueAt(index);
                mSeqMap.removeAt(index);
                nativeFinishInputEvent(mReceiverPtr, seq, handled);
            }
        }
        event.recycleIfNeededAfterDispatch();
    }
}

主要的实现也是native代码,这里就不下看了,感兴趣的可以自行查看c++的实现类NativeInputEventReceiver,c++层代码处理完成后会调用这里的dispatchInputEvent()方法,转而调用onInputEvent(),在往回看下WindowInputEventReceiver的实现:

    final class WindowInputEventReceiver extends InputEventReceiver {
        public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
            super(inputChannel, looper);
        }

        @Override
        public void onInputEvent(InputEvent event) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "processInputEventForCompatibility");
            List<InputEvent> processedEvents;
            try {
                processedEvents =
                    mInputCompatProcessor.processInputEventForCompatibility(event);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
            if (processedEvents != null) {
                if (processedEvents.isEmpty()) {
                    // InputEvent consumed by mInputCompatProcessor
                    finishInputEvent(event, true);
                } else {
                    for (int i = 0; i < processedEvents.size(); i++) {
                        enqueueInputEvent(
                                processedEvents.get(i), this,
                                QueuedInputEvent.FLAG_MODIFIED_FOR_COMPATIBILITY, true);
                    }
                }
            } else {
                //正常是会执行到这里
                enqueueInputEvent(event, this, 0, true);
            }
        }

    }

这样一个流程下来,就通过InputChannel把事件传递到了应用界面,接下去就是通过enqueueInputEvent()传递给view进行处理了,在事件处理完成后,会调用到InputEventReceiver.finishInputEvent(),通知服务端事件处理完成,这样一次事件的分发就算是完成了,这里就不在往下看了。

这里在总结下

        InputManagerService可以理解为通往底层输入事件的一个大门,要想获得事件的处理,可以通过以下几种方式:

        1、向InputManagerService中设置回调,在SystemServer中有设置,最终在PhoneWindowManager中实现事件处理;

        2、通过InputManagerService发送模拟按键事件,比如返回键,可以通过Instrumentation;

        3、通过InputManagerService注册或者添加监听,注册一般用于普通应用,每个启动的页面都有注册,添加监听一般用于系统应用,比如系统手势,就是在SystemUi中实现的;

下一篇文章再聊聊InputWindowManagerService的底层实现,这篇就到这了。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/tangedegushi/article/details/120849106

智能推荐

openssl-3.0.0-alpha9编译_all lines in c:/openssl-3.0.9/openssl-3.0.9/extern-程序员宅基地

文章浏览阅读3.5k次。这个通讯传输加密工具,非常厉害。configure 如下之后make和make install就可以了。多谢,亲爱的美美。_all lines in c:/openssl-3.0.9/openssl-3.0.9/external/perl/modules.txt must b

python 写入csv文件固定列_python对csv文件追加写入列的方法-程序员宅基地

文章浏览阅读3.4k次。python对csv文件追加写入列,具体内容如下所示:原始数据[外链图片转存失败(img-zQSQWAyQ-1563597916666)(C:UsersinnduceAppDataRoamingTyporatypora-user-images1557663419920.png)]import pandas as pdimport numpy as npdata = pd.read_csv(r'平均..._numpy写入csv文件一列

2018年世界计算机超算大赛,在世界大学生超级计算机竞赛(ASC18)总决赛中 青海大学超算团队成功获得ASC竞赛全球一等奖...-程序员宅基地

文章浏览阅读216次。5月9日晚,2018ASC世界大学生超级计算机竞赛(ASC18)总决赛在南昌大学落下帷幕,清华大学成功卫冕总冠军,上海科技大学揽获亚军和e Prize计算挑战奖两项大奖,台湾清华大学获得最高计算性能奖。青海大学在此次比赛中取得了突破性的成绩,HPL基准测试性能仅次于台湾清华大学,HPCG性能仅次于清华大学,最终获全球一等奖,展现出优秀的超算系统与应用理解能力以及出色的性能优化能力。ASC18由亚洲..._青海大学超级计算机

asp webForm 三层框架的简单实例(一)未完待续--_asp net web form应用 三层架构部署-程序员宅基地

文章浏览阅读1.1w次,点赞8次,收藏23次。本文通过一个简单的登录实例,介绍了基本的WebForm开发方式的MVC三层框架方式。本文,是个人作为一个初学者,对webform三层框架的总结,配有相应的源代码,希望对同样迷惑的你有所帮助,如果有不对之处敬请批评指导。_asp net web form应用 三层架构部署

人脸比对(1:N)_1:n人脸检索 学术-程序员宅基地

文章浏览阅读2.9w次,点赞12次,收藏81次。第1章 前言设计出人脸1:N,随着N的增大准确率降低最小的解决方案具有很强的现实意义。人脸1:N的框架大致分为:人脸检测、人脸对齐、人脸映射与人脸识别LOSS的设计,结构如下图所示:图1:人脸1:N的主要框架人脸1:N在学术界有着广泛的研究,对于人脸检测与人脸对齐(MTCNN、TCDCN等)在业界已经有较好的效果,目前的主要性能提升有:DeepFace、DeepID,框架为CNN ..._1:n人脸检索 学术

java语言当中的标识符_java语言的标识符-程序员宅基地

文章浏览阅读458次。关于java语言当中的标识符1、什么是标识符?- 在java源程序当中凡是程序员有权利自己命名的单词都是标识符-标识符可以表示什么元素?*类名*方法名*变量名*接口名*常量名......2、标识符命名规则?【不按照这个规则来,编译器会报错,这是语法】*只能由“数字、字母、下划线_、美元符号$”组成,不能含有其他符号*不能数字开头*严格区分大小写*关键字无长度限制,但是最好不要太长3、标识符命名规范?【只是一种规范,不属于语法,不遵守规范编译器不会报_java语言的标识符

随便推点

直接插入排序——华农oj 8638_数组插入排序 oj-程序员宅基地

文章浏览阅读1.1k次。8638 直接插入排序时间限制:1000MS 代码长度限制:10KB提交次数:2050 通过次数:1393题型: 编程题 语言: G++;GCCDescription用函数实现直接插入排序,并输出每趟排序的结果.输入格式第一行:键盘输入待排序关键的个数n第二行:输入n个待排序关键字,用空格分隔数据输出格式每行输出一趟排序结果,数据之间用一个空格分隔输入样例105 4..._数组插入排序 oj

vue element-ui 表格的分页功能_elementui table 分页-程序员宅基地

文章浏览阅读3.5k次,点赞3次,收藏7次。vue element-ui 表格的分页功能_elementui table 分页

用OpenInventor实现的NeHe OpenGL教程-第三十课-程序员宅基地

文章浏览阅读65次。用OpenInventor实现的NeHe OpenGL教程-第三十课 NeHe教程在这节课介绍了碰撞检测。碰撞检测是一种比较复杂的技术。NeHe教程只是检测平面、球体、圆柱体等这些规则物体之间的碰撞检测。OpenInventor提供了任意形状物体之间的碰撞检测,当然这样的碰撞检测需要更多的计算时间。碰撞检测的算法在NeHe的教程中已经做了详尽的解释,我们就不再赘述了。程序的..._openinventor 火焰效果

视频播放加密功能的演示_视频分段加密播放实现-程序员宅基地

文章浏览阅读1.3k次。视频播放密码/设定观看密码功能,对视频文件设置观看权限,划分学员和游客,学员输入正确的密码即可观看视频。_视频分段加密播放实现

vue中用computed简单实现数据的双向绑定(getter 和 setter)_vue computed绑定表单值-程序员宅基地

文章浏览阅读6.2k次,点赞3次,收藏9次。vue是号称实现了数据双向绑定的框架,但事实上在日常开发中我们用的最多的就是 v-model 将data(vue实例中)里面的是数据和view 相关联,实现 data 更新,view自动刷新的效果。但是,在移动成都上来说,这种数据双向绑定的效果并不是特别的明显。今天,我用输入框和 computed 配置来实现一个比较明显的数据双向绑定的效果:先来看一下最终的效果:主要实现的效果:..._vue computed绑定表单值

联想微型计算机C470拆装,联想C470一体机一键U盘重装系统教程图解-程序员宅基地

文章浏览阅读3.7k次。联想C470一体机造型小巧,外观唯美时尚,易于摆放并能脱离冗杂线缆的束缚。该机是一款非常时尚的家用一体电脑,采用21.5英寸触控屏幕,全高清显示相当精细。无论是学习办公,还是家庭娱乐都能够满足用户的需求。下面给大家介绍联想C470一体机一键U盘重装系统教程图解,教大家一体机怎么装系统。相关推荐:联想C470一体机怎么进入bios设置u盘启动准备一个u盘,再下载U盘装机大师启动盘制作工具,把其做成U..._联想c470一体机拆机图解

推荐文章

热门文章

相关标签