热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

进入事件entry_你所不知道的输入事件分发机制

点击上方“刘望舒”,马上关注真爱,请置顶或星标前言在你需要掌握的事件分发高阶知识这篇文章中,由于文章篇幅的原因,InputD

点击上方“刘望舒”,马上关注

真爱,请置顶或星标

前言

在你需要掌握的事件分发高阶知识这篇文章中,由于文章篇幅的原因,InputDispatcher的分发过程还有一部分没有讲解,这一部分就是事件分发到目标窗口的过程。

1. 为事件寻找合适的分发目标

我们先来回顾上一篇文章讲解的InputDispatcher的dispatchOnceInnerLocked函数:frameworks/native/services/inputflinger/InputDispatcher.cpp

void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
    ...
    DropReason dropReason = DROP_REASON_NOT_DROPPED;//1
   ...
    switch (mPendingEvent->type) {//2
    ...
    case EventEntry::TYPE_MOTION: {
        MotionEntry* typedEntry = static_cast(mPendingEvent);//如果没有及时响应窗口切换操作if (dropReason == DROP_REASON_NOT_DROPPED && isAppSwitchDue) {
            dropReason = DROP_REASON_APP_SWITCH;
        }//事件过期if (dropReason == DROP_REASON_NOT_DROPPED
                && isStaleEventLocked(currentTime, typedEntry)) {
            dropReason = DROP_REASON_STALE;
        }//阻碍其他窗口获取事件if (dropReason == DROP_REASON_NOT_DROPPED && mNextUnblockedEvent) {
            dropReason = DROP_REASON_BLOCKED;
        }
        done = dispatchMotionLocked(currentTime, typedEntry,
                &dropReason, nextWakeupTime);//3break;
    }default:
        ALOG_ASSERT(false);break;
    }
    ...
}    

dispatchOnceInnerLocked函数中主要做了5件事,这里只截取了其中的一件事:事件的丢弃。
注释1处的dropReason代表了事件丢弃的原因,它的默认值为DROP_REASON_NOT_DROPPED,代表事件不被丢弃。
注释2处根据mPendingEvent的type做区分处理,这里主要截取了对Motion类型的处理。经过条件语句过滤,会调用注释3处的dispatchMotionLocked函数为Motion事件寻找合适的窗口。frameworks/native/services/inputflinger/InputDispatcher.cpp        

bool InputDispatcher::dispatchMotionLocked(
        nsecs_t currentTime, MotionEntry* entry, DropReason* dropReason, nsecs_t* nextWakeupTime) {
    if (! entry->dispatchInProgress) {
        //标记当前已经进入分发的过程
        entry->dispatchInProgress = true;
        logOutboundMotionDetailsLocked("dispatchMotion - ", entry);
    }
    // 如果事件是需要丢弃的,则返回true,不会去为该事件寻找合适的窗口
    if (*dropReason != DROP_REASON_NOT_DROPPED) {//1
        setInjectionResultLocked(entry, *dropReason == DROP_REASON_POLICY
                ? INPUT_EVENT_INJECTION_SUCCEEDED : INPUT_EVENT_INJECTION_FAILED);
        return true;
    }
    bool isPointerEvent = entry->source & AINPUT_SOURCE_CLASS_POINTER;
    // 目标窗口信息列表会存储在inputTargets中
    Vector inputTargets;//2bool conflictingPointerActions = false;int32_t injectionResult;if (isPointerEvent) {//处理点击形式的事件,比如触摸屏幕
        injectionResult = findTouchedWindowTargetsLocked(currentTime,
                entry, inputTargets, nextWakeupTime, &conflictingPointerActions);//3
    } else {//处理非触摸形式的事件,比如轨迹球
        injectionResult = findFocusedWindowTargetsLocked(currentTime,
                entry, inputTargets, nextWakeupTime);//4
    }//输入事件被挂起,说明找到了窗口并且窗口无响应if (injectionResult == INPUT_EVENT_INJECTION_PENDING) {return false;
    }
    setInjectionResultLocked(entry, injectionResult);//输入事件没有分发成功,说明没有找到合适的窗口if (injectionResult != INPUT_EVENT_INJECTION_SUCCEEDED) {if (injectionResult != INPUT_EVENT_INJECTION_PERMISSION_DENIED) {
            CancelationOptions::Mode mode(isPointerEvent ?
                    CancelationOptions::CANCEL_POINTER_EVENTS :
                    CancelationOptions::CANCEL_NON_POINTER_EVENTS);CancelationOptions options(mode, "input event injection failed");
            synthesizeCancelationEventsForMonitorsLocked(options);
        }return true;
    }//分发目标添加到inputTargets列表中
    addMonitoringTargetsLocked(inputTargets);//5// Dispatch the motion.if (conflictingPointerActions) {CancelationOptions options(CancelationOptions::CANCEL_POINTER_EVENTS,"conflicting pointer actions");
        synthesizeCancelationEventsForAllConnectionsLocked(options);
    }//将事件分发给inputTargets列表中的目标
    dispatchEventLocked(currentTime, entry, inputTargets);//6return true;
}                                                                 

注释1处说明事件是需要丢弃的,这时就会直接返回true,不会为该事件寻找窗口,这次的分发任务就没有完成,会在下一次InputDispatcherThread的循环中再次尝试分发。注释3和注释4处会对点击形式和非触摸形式的事件进行处理,将事件处理的结果交由injectionResult。后面会判断injectionResult的值,如果injectionResult的值为INPUT_EVENT_INJECTION_PENDING,这说明找到了窗口并且窗口无响应输入事件被挂起,这时就会返回false;如果injectionResult的值不为INPUT_EVENT_INJECTION_SUCCEEDED,这说明没有找到合适的窗口,输入事件没有分发成功,这时就会返回true。
注释5处会将分发的目标添加到inputTargets列表中,最终在注释6处将事件分发给inputTargets列表中的目标。
从注释2处可以看出inputTargets列表中的存储的是InputTarget结构体:frameworks/native/services/inputflinger/InputDispatcher.h    

struct InputTarget {
  enum {
    //此标记表示事件正在交付给前台应用程序
    FLAG_FOREGROUND &#61; 1 <0,
    //此标记指示MotionEvent位于目标区域内
    FLAG_WINDOW_IS_OBSCURED &#61; 1 <1,
    ...
};
    //inputDispatcher与目标窗口的通信管道
    sp inputChannel;//1//事件派发的标记int32_t flags;//屏幕坐标系相对于目标窗口坐标系的偏移量float xOffset, yOffset;//2//屏幕坐标系相对于目标窗口坐标系的缩放系数float scaleFactor;//3
    BitSet32 pointerIds;
}                                                                                              

InputTarget结构体可以说是inputDispatcher与目标窗口的转换器&#xff0c;其分为两大部分&#xff0c;一个是枚举中存储的inputDispatcher与目标窗口交互的标记&#xff0c;另一部分是inputDispatcher与目标窗口交互参数&#xff0c;比如注释1处的inputChannel&#xff0c;它实际上是一个SocketPair&#xff0c;SocketPair用于进程间双向通信&#xff0c;这非常适合inputDispatcher与目标窗口之间的通信&#xff0c;因为inputDispatcher不仅要将事件分发到目标窗口&#xff0c;同时inputDispatcher也需要得到目标窗口对事件的响应。注释2处的xOffset和yOffset&#xff0c;屏幕坐标系相对于目标窗口坐标系的偏移量&#xff0c;MotionEntry(MotionEvent)中的存储的坐标是屏幕坐标系&#xff0c;因此就需要注释2和注释3处的参数&#xff0c;来将屏幕坐标系转换为目标窗口的坐标系。

2. 处理点击形式的事件

在InputDispatcher的dispatchMotionLocked函数的注释3和注释4处&#xff0c;分别对Motion事件中的点击形式事件和非触摸形式事件做了处理&#xff0c;由于非触摸形式事件不是很常见&#xff0c;这里对点击形式事件进行解析。InputDispatcher的findTouchedWindowTargetsLocked函数如有400多行&#xff0c;这里截取了需要了解的部分&#xff0c;并且分两个部分来讲解。frameworks/native/services/inputflinger/InputDispatcher.cpp

1.findTouchedWindowTargetsLocked函数part1:

int32_t InputDispatcher::findTouchedWindowTargetsLocked(nsecs_t currentTime,
        const MotionEntry* entry, Vector& inputTargets, nsecs_t* nextWakeupTime,bool* outConflictingPointerActions) {
    ...if (newGesture || (isSplit && maskedAction &#61;&#61; AMOTION_EVENT_ACTION_POINTER_DOWN)) {//从MotionEntry中获取坐标点int32_t pointerIndex &#61; getMotionEventActionPointerIndex(action);int32_t x &#61; int32_t(entry->pointerCoords[pointerIndex].
                getAxisValue(AMOTION_EVENT_AXIS_X));int32_t y &#61; int32_t(entry->pointerCoords[pointerIndex].
                getAxisValue(AMOTION_EVENT_AXIS_Y));        
        sp newTouchedWindowHandle;bool isTouchModal &#61; false;size_t numWindows &#61; mWindowHandles.size();//1// 遍历窗口&#xff0c;找到触摸过的窗口和窗口之外的外部目标for (size_t i &#61; 0; i //2//获取InputDispatcher中代表窗口的windowHandle 
            sp windowHandle &#61; mWindowHandles.itemAt(i);//得到窗口信息windowInfo const InputWindowInfo* windowInfo &#61; windowHandle->getInfo();if (windowInfo->displayId !&#61; displayId) {//如果displayId不匹配&#xff0c;开始下一次循环continue; 
            }//获取窗口的flagint32_t flags &#61; windowInfo->layoutParamsFlags;//如果窗口时可见的if (windowInfo->visible) {//如果窗口的flag不为FLAG_NOT_TOUCHABLE(窗口是touchable)if (! (flags & InputWindowInfo::FLAG_NOT_TOUCHABLE)) {// 如果窗口是focusable或者flag不为FLAG_NOT_FOCUSABLE&#xff0c;则说明该窗口是”可触摸模式“
                    isTouchModal &#61; (flags & (InputWindowInfo::FLAG_NOT_FOCUSABLE
                            | InputWindowInfo::FLAG_NOT_TOUCH_MODAL)) &#61;&#61; 0;//3//如果窗口是”可触摸模式或者坐标点落在窗口之上             if (isTouchModal || windowInfo->touchableRegionContainsPoint(x, y)) {
                        newTouchedWindowHandle &#61; windowHandle;//4break; // found touched window, exit window loop
                    }
                }if (maskedAction &#61;&#61; AMOTION_EVENT_ACTION_DOWN
                        && (flags & InputWindowInfo::FLAG_WATCH_OUTSIDE_TOUCH)) {//将符合条件的窗口放入TempTouchState中&#xff0c;以便后续处理。
                    mTempTouchState.addOrUpdateWindow(
                            windowHandle, InputTarget::FLAG_DISPATCH_AS_OUTSIDE, BitSet32(0));//5
                }
            }
        }

开头先从MotionEntry中获取坐标&#xff0c;为了后面筛选窗口用。注释1处获取列表mWindowHandles的InputWindowHandle数量&#xff0c;InputWindowHandle中存储保存了InputWindowInfo&#xff0c;InputWindowInfo中又包含了WindowManager.LayoutParams定义的窗口标志&#xff0c;关于窗口标志见Android解析WindowManager(二)Window的属性这篇文章。除了窗口标志&#xff0c;InputWindowInfo中还包含了InputChannel和窗口各种属性&#xff0c;InputWindowInfo描述了可以接收输入事件的窗口的属性。这么看来&#xff0c;InputWindowHandle和WMS中的WindowState很相似。通俗来讲&#xff0c;WindowState用来代表WMS中的窗口&#xff0c;而InputWindowHandle用来代表输入系统中的窗口。
那么输入系统是如何得到窗口信息的呢&#xff1f;这是因为mWindowHandles列表就是WMS更新到InputDispatcher中的。
注释2处开始遍历mWindowHandles列表中的窗口&#xff0c;找到触摸过的窗口和窗口之外的外部目标。注释3处&#xff0c;如果窗口是focusable或者flag不为FLAG_NOT_FOCUSABLE&#xff0c;则说明该窗口是”可触摸模式“。经过层层的筛选&#xff0c;如果窗口是”可触摸模式“或者坐标点落在窗口之上&#xff0c;会在注释4处&#xff0c;将windowHandle赋值给newTouchedWindowHandle。最后在注释5处&#xff0c;将newTouchedWindowHandle添加到TempTouchState中&#xff0c;以便后续处理。

2.findTouchedWindowTargetsLocked函数part2:

...
    // 确保所有触摸过的前台窗口都为新的输入做好了准备
    for (size_t i &#61; 0; i         const TouchedWindow& touchedWindow &#61; mTempTouchState.windows[i];
        if (touchedWindow.targetFlags & InputTarget::FLAG_FOREGROUND) {
            // 检查窗口是否准备好接收更多的输入
            String8 reason &#61; checkWindowReadyForMoreInputLocked(currentTime,
                    touchedWindow.windowHandle, entry, "touched");//1
            if (!reason.isEmpty()) {//2
            //如果窗口没有准备好&#xff0c;则将原因赋值给injectionResult 
                injectionResult &#61; handleTargetsNotReadyLocked(currentTime, entry,
                        NULL, touchedWindow.windowHandle, nextWakeupTime, reason.string());//3
             //不做后续的处理&#xff0c;直接跳到Unresponsive标签          
                goto Unresponsive;//3
            }
        }
    }
    ...
    //代码走到这里&#xff0c;说明窗口已经查找成功
    injectionResult &#61; INPUT_EVENT_INJECTION_SUCCEEDED;//5
    //遍历TempTouchState中的窗口
    for (size_t i &#61; 0; i         const TouchedWindow& touchedWindow &#61; mTempTouchState.windows.itemAt(i);
        //为每个mTempTouchState中的窗口生成InputTargets 
        addWindowTargetLocked(touchedWindow.windowHandle, touchedWindow.targetFlags,
                touchedWindow.pointerIds, inputTargets);//6
    }
   //在下一次迭代中&#xff0c;删除外部窗口或悬停触摸窗口
    mTempTouchState.filterNonAsIsTouchWindows();
...
Unresponsive:
    //重置TempTouchState
    mTempTouchState.reset();
    nsecs_t timeSpentWaitingForApplication &#61; getTimeSpentWaitingForApplicationLocked(currentTime);
    updateDispatchStatisticsLocked(currentTime, entry,
            injectionResult, timeSpentWaitingForApplication);
#if DEBUG_FOCUS
    ALOGD("findTouchedWindow finished: injectionResult&#61;%d, injectionPermission&#61;%d, "
            "timeSpentWaitingForApplication&#61;%0.1fms",
            injectionResult, injectionPermission, timeSpentWaitingForApplication / 1000000.0);
#endif
    return injectionResult;
}                                                  

注释1处用于检查窗口是否准备好接收更多的输入&#xff0c;并将结果赋值给reason。注释2处&#xff0c;如果reason的值不为空&#xff0c;说明该窗口无法接收更多的输入&#xff0c;注释3处的handleTargetsNotReadyLocked函数会得到无法接收更多输入的原因&#xff0c;赋值给injectionResult&#xff0c;其函数内部会计算窗口处理的时间&#xff0c;如果超时(默认为5秒)&#xff0c;就会报ANR&#xff0c;并设置nextWakeupTime的值为LONG_LONG_MIN&#xff0c;强制InputDispatcherThread在下一次循环中立即被唤醒&#xff0c;InputDispatcher会重新开始分发输入事件。这个时候&#xff0c;injectionResult的值为INPUT_EVENT_INJECTION_PENDING。因为窗口无法接收更多的输入&#xff0c;因此会在注释4处&#xff0c;调用goto语句跳到Unresponsive标签&#xff0c;Unresponsive标签中会调用TempTouchState的reset函数来重置TempTouchState。
如果代码已经走到了注释5处&#xff0c;说明窗口已经查找成功&#xff0c;会遍历TempTouchState中的窗口&#xff0c;在注释6处为每个TempTouchState中 的窗口生成inputTargets。
在第一小节&#xff0c;InputDispatcher的dispatchMotionLocked函数的注释6处&#xff0c;会调用InputDispatcher的dispatchEventLocked函数
将事件分发给inputTargets列表中的分发目标&#xff0c;接下来我们来查看下是如何实现的。

3. 向目标窗口发送事件

InputDispatcher的dispatchEventLocked函数如下所示。frameworks/native/services/inputflinger/InputDispatcher.cpp

void InputDispatcher::dispatchEventLocked(nsecs_t currentTime,
        EventEntry* eventEntry, const Vector& inputTargets) {#if DEBUG_DISPATCH_CYCLE
    ALOGD("dispatchEventToCurrentInputTargets");#endif
    ALOG_ASSERT(eventEntry->dispatchInProgress); // should already have been set to true
    pokeUserActivityLocked(eventEntry);//遍历inputTargets列表for (size_t i &#61; 0; i         const InputTarget& inputTarget &#61; inputTargets.itemAt(i);//根据inputTarget内部的inputChannel来获取Connection的索引ssize_t connectionIndex &#61; getConnectionIndexLocked(inputTarget.inputChannel);//1if (connectionIndex >&#61; 0) {//获取保存在mConnectionsByFd容器中的Connection
            sp connection &#61; mConnectionsByFd.valueAt(connectionIndex);//根据inputTarget&#xff0c;开始事件发送循环
            prepareDispatchCycleLocked(currentTime, connection, eventEntry, &inputTarget);//2
        } else {
#if DEBUG_FOCUS
            ALOGD("Dropping event delivery to target with channel &#39;%s&#39; because it ""is no longer registered with the input dispatcher.",
                    inputTarget.inputChannel->getName().string());#endif
        }
    }
}   

遍历inputTargets列表&#xff0c;获取每一个inputTarget&#xff0c;注释1处&#xff0c;根据inputTarget内部的inputChannel来获取Connection的索引&#xff0c;再根据这个索引作为Key值来获取mConnectionsByFd容器中的Connection。Connection可以理解为InputDispatcher和目标窗口的连接&#xff0c;其内部包含了连接的状态、InputChannel、InputWindowHandle和事件队列等等。注释2处调用prepareDispatchCycleLocked函数根据当前的inputTarget&#xff0c;开始事件发送循环。最终会通过inputTarget中的inputChannel来和窗口进行进程间通信&#xff0c;最终将Motion事件发送给目标窗口。

4. Motion事件分发过程总结

结合只了解View的事件分发是不够的&#xff0c;来看下输入系统对事件的处理和你需要掌握的事件分发高阶知识这两篇文章&#xff0c;可以总结一下Motion事件分发过程&#xff0c;简化为下图。

db9d9724ef7507a340a6aeb2a68c8013.png
  1. Motion事件在InputReaderThread线程中的InputReader进行加工&#xff0c;加工完毕后会判断是否要唤醒InputDispatcherThread&#xff0c;如果需要唤醒&#xff0c;会在InputDispatcherThread的线程循环中不断的用InputDispatcher来分发 Motion事件。

  2. 将Motion事件交由InputFilter过滤&#xff0c;如果返回值为false&#xff0c;这次Motion事件就会被忽略掉。

  3. InputReader对Motion事件加工后的数据结构为NotifyMotionArgs&#xff0c;在InputDispatcher的notifyMotion函数中&#xff0c;用NotifyMotionArgs中的事件参数信息构造一个MotionEntry对象。这个MotionEntry对象会被添加到InputDispatcher的mInboundQueue队列的末尾。

  4. 如果mInboundQueue不为空&#xff0c;取出mInboundQueue队列头部的EventEntry赋值给mPendingEvent。

  5. 根据mPendingEvent的值&#xff0c;进行事件丢弃处理。

  6. 调用InputDispatcher的findTouchedWindowTargetsLocked函数&#xff0c;在mWindowHandles窗口列表中为Motion事件找到目标窗口&#xff0c;并为该窗口生成inputTarget。

  7. 根据inputTarget获取一个Connection&#xff0c;依赖Connection将输入事件发送给目标窗口。

这里只是简单的总结了Motion事件分发过程&#xff0c;和Motion事件类似的还有key事件&#xff0c;就需要读者自行去阅读源码了。

后记

实际上输入系统还有很多内容需要去讲解&#xff0c;比如inputChannel如何和窗口进行进程间通信&#xff0c;InputDispatcher如何得到窗口的反馈&#xff0c;这些内容会在本系列的后续文章中进行讲解。

 — — — END — — —

相关文章

  • Android输入系统的事件传递流程和IMS的诞生

  • 只了解View的事件分发是不够的&#xff0c;来看下输入系统对事件的处理

  • 你需要掌握的事件分发高阶知识

  • WMS的重要成员和Window的添加过程

  • Window的属性

分享大前端、Java和跨平台等技术&#xff0c;

关注职业发展和行业动态。

eb1030f6b88cd1501b67fff46cf2f8bd.png




推荐阅读
  • 开发笔记:实验7的文件读写操作
    本文介绍了使用C++的ofstream和ifstream类进行文件读写操作的方法,包括创建文件、写入文件和读取文件的过程。同时还介绍了如何判断文件是否成功打开和关闭文件的方法。通过本文的学习,读者可以了解如何在C++中进行文件读写操作。 ... [详细]
  • 本文介绍了一个Java猜拳小游戏的代码,通过使用Scanner类获取用户输入的拳的数字,并随机生成计算机的拳,然后判断胜负。该游戏可以选择剪刀、石头、布三种拳,通过比较两者的拳来决定胜负。 ... [详细]
  • Java SE从入门到放弃(三)的逻辑运算符详解
    本文详细介绍了Java SE中的逻辑运算符,包括逻辑运算符的操作和运算结果,以及与运算符的不同之处。通过代码演示,展示了逻辑运算符的使用方法和注意事项。文章以Java SE从入门到放弃(三)为背景,对逻辑运算符进行了深入的解析。 ... [详细]
  • 本文介绍了关系型数据库和NoSQL数据库的概念和特点,列举了主流的关系型数据库和NoSQL数据库,同时描述了它们在新闻、电商抢购信息和微博热点信息等场景中的应用。此外,还提供了MySQL配置文件的相关内容。 ... [详细]
  • 本文主要介绍了gym102222KVertex Covers(高维前缀和,meet in the middle)相关的知识,包括题意、思路和解题代码。题目给定一张n点m边的图,点带点权,定义点覆盖的权值为点权之积,要求所有点覆盖的权值之和膜qn小于等于36。文章详细介绍了解题思路,通过将图分成两个点数接近的点集L和R,并分别枚举子集S和T,判断S和T能否覆盖所有内部的边。文章还提到了使用位运算加速判断覆盖和推导T'的方法。最后给出了解题的代码。 ... [详细]
  • WPF之Binding初探
      初学wpf,经常被Binding搞晕,以下记录写Binding的基础。首先,盗用张图。这图形象的说明了Binding的机理。对于Binding,意思是数据绑定,基本用法是:1、 ... [详细]
  • 近来有一个需求,是需要在androidjava基础库中插入一些log信息,完成这个工作需要的前置条件有编译好的android源码具体android源码如何编译,这 ... [详细]
  • SpringBoot uri统一权限管理的实现方法及步骤详解
    本文详细介绍了SpringBoot中实现uri统一权限管理的方法,包括表结构定义、自动统计URI并自动删除脏数据、程序启动加载等步骤。通过该方法可以提高系统的安全性,实现对系统任意接口的权限拦截验证。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • ALTERTABLE通过更改、添加、除去列和约束,或者通过启用或禁用约束和触发器来更改表的定义。语法ALTERTABLEtable{[ALTERCOLUMNcolu ... [详细]
  • Oracle10g备份导入的方法及注意事项
    本文介绍了使用Oracle10g进行备份导入的方法及相关注意事项,同时还介绍了2019年独角兽企业重金招聘Python工程师的标准。内容包括导出exp命令、删用户、创建数据库、授权等操作,以及导入imp命令的使用。详细介绍了导入时的参数设置,如full、ignore、buffer、commit、feedback等。转载来源于https://my.oschina.net/u/1767754/blog/377593。 ... [详细]
  • 本文详细介绍了Android中的坐标系以及与View相关的方法。首先介绍了Android坐标系和视图坐标系的概念,并通过图示进行了解释。接着提到了View的大小可以超过手机屏幕,并且只有在手机屏幕内才能看到。最后,作者表示将在后续文章中继续探讨与View相关的内容。 ... [详细]
  • 带添加按钮的GridView,item的删除事件
    先上图片效果;gridView无数据时显示添加按钮,有数据时,第一格显示添加按钮,后面显示数据:布局文件:addr_manage.xml<?xmlve ... [详细]
  • 本文概述了JNI的原理以及常用方法。JNI提供了一种Java字节码调用C/C++的解决方案,但引用类型不能直接在Native层使用,需要进行类型转化。多维数组(包括二维数组)都是引用类型,需要使用jobjectArray类型来存取其值。此外,由于Java支持函数重载,根据函数名无法找到对应的JNI函数,因此介绍了JNI函数签名信息的解决方案。 ... [详细]
author-avatar
大叔丶你好哇塞
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有