首页 文章 精选 留言 我的

精选列表

搜索[垂直分库],共6056篇文章
优秀的个人博客,低调大师

RAG (检索增强生成)技术详解:揭秘基于垂直领域专有数据的Chatbots是如何实现的

编者按:相信很多人都对Chatbots背后的技术原理很感兴趣,其实Chatbots并非通过“魔法”与我们交流,而是依靠一种被称为检索增强生成(RAG)的技术。 文章详细梳理了 RAG 技术的具体实现原理。首先,RAG 将用户输入的问题与知识库中的私有数据进行匹配,获取相关知识片段。然后,通过预训练的大语言模型,用提取到的知识片段来增强对问题的回答生成过程。在知识提取步骤,借助词向量的相似度找到与用户提出的问题最匹配的内容。生成回答时,直接向语言模型提供增强知识来指导其产出更符合语境的回答。 RAG 技术看似神奇,但其本质是结合了检索和生成两个子任务的一种系统工程,而每个子任务又都有明确的技术原理支撑。作为 AI 开发者,理解这一工作流程尤为重要。相信本文有助于读者进一步掌握 RAG 的技术原理,从而更好地运用 Chatbots 为用户创造更多价值。 以下是译文,enjoy! 作者 | Cory Zue 编译 | 岳扬 🚢🚢🚢欢迎小伙伴们加入AI技术软件及技术交流群,追踪前沿热点,共探技术难题~ 目录 01 什么是检索增强生成(RAG)技术? 02 让我们从后往前看:给LLM额外的知识来回答问题 03 使用system prompt为LLM提供自定义指令 04 为LLM提供特定的知识来源 05 将所有内容整合在一起并提出问题 06 检索步骤:从知识库中获取正确信息 07 什么是嵌入?它们与知识检索有什么关系? 08 使用嵌入查找最佳的知识库片段 09 为知识库创建索引 10 总结整个过程 这段代码实现了一个简单的Chatbots: from langchain.document_loaders import WebBaseLoader from langchain.indexes import VectorstoreIndexCreator loader = WebBaseLoader("http://www.paulgraham.com/greatwork.html") index = VectorstoreIndexCreator().from_loaders([loader]) index.query("What should I work on?") 可以看到这段代码发送了一个查询请求,因为该Chatbots是使用Paul Graham的文章[1]作为专有数据实现的,因此得到输出结果如下: The work you choose should ideally have three qualities: it should be something you have a natural aptitude for, something you have a deep interest in, and something that offers scope to do great work. If you're unsure, you could start by working on your own projects that seem excitingly ambitious to you. It's also beneficial to be curious, try lots of things, meet lots of people, read lots of books, and ask lots of questions. When in doubt, optimize for interestingness. It's okay to guess and be wrong sometimes, as this can lead to discovering what you're truly good at or interested in. 注意:如果您感兴趣,可以在这里[2]尝试使用Paul Graham的文章构建的Chatbot。 当我第一次运行时,感觉就像是一种魔法。它到底是怎么实现的? 答案是使用一种叫做检索增强生成(Retrieval Augmented Generation,简称RAG)的技术。这是一个非常简单的概念,但其具体实现细节却有着惊人的技术深度。 本篇文章将对 RAG 做一个高层次的概述。我们将从Chatbots的大体工作流程开始,然后放大到所有单独的部分。文章结束时,您应该对这三行神奇的代码是如何工作的,以及创建这些Chatbots所涉及的所有原理有了扎实的了解。 如果您是一名开发人员,并且正在尝试构建像这样的机器人,那么您将学习到哪些参数可以调整以及如何调整它们。如果您是一名非开发人员,并且希望在私有数据集上使用AI工具,那么您将获得帮助您充分利用这些工具的知识。如果您只是对Chatbots十分好奇的人,希望您能学到一两件关于这些能够颠覆我们生活的技术的知识。 接下来让我们深入地进行探讨。 01 什么是检索增强生成(RAG)技术? 检索增强生成是将用户输入的信息补充到大语言模型(LLM)中的过程。然后,LLM 可以使用这些信息来增强其生成的回答或响应。 下图展示了在生产环境中它是如何工作的: 这个流程始于用户提出的问题,例如“我该如何做<某件事>?” 首先进行的是检索步骤。这个过程是根据用户的问题,从知识库中检索可能回答问题的最相关内容。 到目前为止,检索步骤是 RAG 链中最重要、最复杂的一个部分。但现在,只需把这个步骤当成一个黑盒子即可,它知道如何提取与用户查询最相关的信息块。 难道我们不能把整个知识库交给 LLM 吗? 您可能会想知道,为什么我们要这样费心进行检索,而不是直接将整个知识库发送给LLM。其中一个原因是大模型预置了每次可读取文本的数量限制(尽管这些限制的上限正在快速增长)。第二个原因是成本 —— 发送大量文本是非常昂贵的。最后,有证据表明,仅发送少量相关信息会产生更好的答案。 一旦我们从知识库中获取到了相关信息,就会将其与用户的问题一起发送给大语言模型(LLM)。LLM会“读取”提供的信息并回答问题。这就是增强生成步骤。 看起来相当简单,对吧? 02 让我们从后往前看:给LLM额外的知识来回答问题 我们从最后一步开始:生成回答。也就是说,假设我们已经从知识库中提取了我们认为能够回答问题的相关信息。那么我们如何使用这些信息来生成答案呢? 这个过程可能让人感觉像魔法,但其实背后只是一个语言模型。因此,概括地说,答案就是“向LLM提问”。 那么我们如何让LLM做到这一点呢?我们将以ChatGPT为例进行阐释。就像通常的ChatGPT使用一样,这一切都取决于prompts和相关信息。 03 使用system prompt为LLM提供自定义指令 第一个组成部分是system prompt。system prompt为语言模型提供整体指导。对于ChatGPT来说,system prompt类似于这种:“You are a helpful assistant.”。 在这种情况下,我们会希望它做一些更具体的事情。由于它是语言模型,我们可以直接告诉它我们想让它做什么。以下是一个简短的system prompt示例,它为LLM提供了更详细的指令: You are a Knowledge Bot. You will be given the extracted parts of a knowledge base (labeled with DOCUMENT) and a question. Answer the question using information from the knowledge base. 您是一个Knowledge Bot。接下来将给您一份知识库中提取的文档内容(标记为DOCUMENT)和一个相关的问题。请使用知识库中的信息回答问题。 我们基本上是在说:“嘿,AI,我们将给你一些东西阅读。阅读后回答我们的问题,好吗?谢谢。”由于AI很擅长遵循我们的指示,因此它可以很好地完成任务。 04 为LLM提供特定的知识来源 接下来,我们需要为AI提供阅读材料。还是忍不住再次说明,目前最新的AI非常擅长从文档中直接找出使用者需要的东西。但是,我们仍然可以通过一些固定的结构或格式来提高它的效率。 下面是一个向 LLM 传递文档的格式示例: ------------ DOCUMENT 1 ------------- This document describes the blah blah blah... ------------ DOCUMENT 2 ------------- This document is another example of using x, y and z... ------------ DOCUMENT 3 ------------- [more documents here...] 一定需要所有的这些格式吗?可能不需要,但让内容尽可能明确是件好事。您也可以使用 JSON 或 YAML 等机器可读的格式。或者,如果您感觉这样做让您很烦,您可以将所有内容都写成一个巨大的文本块。但是,对于更高级的使用场景,如,希望LLM在回答问题时引用其来源的场景,格式保持一致十分非常重要。 格式化文件后,我们就可以将其作为普通聊天信息发送给 LLM。请记住,在system prompt中,我们只需要告诉它我们将给它一些文档。 05 将所有内容整合在一起并提出问题 一旦有了system prompt和“文档(documents)”信息,我们就只需将它们与用户的问题一起发送给LLM即可。以下是使用OpenAI ChatCompletion API的Python代码示例: openai_response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ { "role": "system", "content": get_system_prompt(), # the system prompt as per above }, { "role": "system", "content": get_sources_prompt(), # the formatted documents as per above }, { "role": "user", "content": user_question, # the question we want to answer }, ], ) 就是这样!一条自定义的system prompt,两条信息,就可以获得针对具体语境的答案! 这只是一个简单的使用案例,可以对其进行进一步地扩展和改进。 我们还没有做的一件事就是告诉人工智能,如果它在信息源中找不到答案该怎么办。我们可以将这些指令添加到system prompt中,通常是告诉它拒绝回答,或者使用它了解的常识,具体取决于您的机器人想要做什么。您还可以让LLM引用其用于回答问题的特定资料来源。我们将在以后的文章中讨论这些策略,但现在,这些就是答案生成的基本要素了。 说完了简单的部分,现在该回到我们跳过的那个黑盒子了...... 06 检索步骤:从知识库中获取正确信息 在上文中,我们假设已经有了正确的知识片段可以发送给LLM。但是,我们如何才能从用户的问题中获取这些信息呢?这就是检索步骤,它是任何“与数据聊天(chat with your data)”的系统中的核心基础设施。 检索的核心是搜索操作——我们希望根据用户的输入查找最相关的信息。 就像搜索一样,检索有两个主要部分: 索引:将知识库转换为可搜索/查询的内容。 查询:从搜索内容中提取最相关的知识片段。 值得注意的是,任何搜索过程都可以用于检索,任何接受用户输入并返回结果的程序都可以使用。因此,举例来说,你可以尝试找到与用户问题相匹配的文本,然后将其发送给 LLM,或者您可以在Google中搜索问题,并将前几个结果发送过去——顺便说一下,这大致是Bing聊天机器人的工作原理。 也就是说,如今大多数RAG系统都依赖于一种称为语义搜索的技术,它使用了人工智能的另一项核心技术:嵌入(Embedding)。在这里,我们将重点讨论这一使用案例。 那么...什么是嵌入向量呢? 07 什么是嵌入?它们与知识检索有什么关系? LLM 充满奥妙。它们最神秘的地方之一,就是没有人真正知道它们是如何理解语言的。嵌入是其中的重要一环。 如果你问一个人他们如何将单词转化为自己理解的意义的,他们可能会支支吾吾地说出 "因为我知道它们是什么意思 "这样含糊不清、自说自话的话。在我们大脑深处有一个复杂的结构,它知道 "孩子 "和 "小孩 "基本上是一样的,"红色 "和 "绿色 "都是颜色,而 "高兴"、"快乐 "和 "欣喜 "代表的是同一种情绪,只是程度不同而已。我们无法解释其原理,但我们就是知道。 语言模型对语言也有类似的复杂理解,只不过,因为它们是计算机,所以理解并不在它们的大脑中,而是由数字组成的。在LLM的世界中,任何一段人类语言都可以用数字向量(列表)来表示。这个数字向量就是嵌入。 LLM 技术的一个关键部分是翻译器,它可以从人类文字语言翻译成人工智能数字语言。我们称这种翻译机为 "嵌入机(embedding machine)",尽管在其底层它只是调用了一个应用程序接口。输入人类语言,输出人工智能数字语言。 这些数字意味着什么?没有人知道!它们只对AI来说有“意义”。但是,我们知道的是,相似的单词最终会有相似的一组数字。因为在幕后,AI使用这些数字来“阅读”和“说话”。因此,这些数字在AI语言中具有某种神奇的意义,即使我们无法理解。嵌入机(embedding machine)就是我们的翻译器。 现在,由于我们有了这些神奇的AI数字,我们可以将它们绘制出来。上述例子的简化图可能是这样的,其中的坐标轴仅是人类/AI语言的某种抽象表示: 绘制出这些点之后,我们可以发现,在这个假设的语言空间中,两个点越接近,它们就越相似。 例如,“Hello, how are you?” 和 “Hey, how’s it going?” 几乎在同一个位置。另一个问候语“Good morning”也不太远。而“I like cupcakes”则完全与其他内容分开,它们在语言空间中的位置远离其他文本片段。 当然,不可能在一张二维图上表示全部人类语言,但理论是一样的。实际上,嵌入具有更多维的坐标(OpenAI当前使用的模型使用的嵌入向量是由1,536个数字组成的。)。但是你仍然可以通过基本的数学运算来确定两个嵌入——因此两个文本片段之间的接近程度。 这些嵌入和去确定“接近程度”是语义搜索的核心原理,它为检索步骤提供了动力。 08 使用嵌入查找最佳的知识库片段 一旦我们了解了使用嵌入进行搜索的工作原理,我们就能构建出检索步骤的高层次图景。 在索引方面,我们首先要将知识库分解成多个文本块。这个过程本身就是一个完整的优化问题,我们将在下一步进行介绍,但现在只需假设我们已经知道如何做。 完成上述操作后,我们将每个知识片段传递给嵌入机(embedding machine)(实际上是OpenAI API或类似的程序接口)进行处理,然后得到该文本的嵌入表征。然后,我们将该片段与其嵌入一起保存在向量数据库中(这是一种专门用于处理数字向量的数据库,可用于存储和检索嵌入向量。) 现在,我们已经有了一个包含所有内容嵌入的数据库。从理论上来讲,可以将其视为我们整个知识库在“语言”图谱上的绘制: 有了这个图谱之后,在查询方面,我们也要做类似的处理。首先,我们获取用户输入的嵌入: 然后,我们将其绘制在同一向量空间中,并找出最接近的片段(本例中为 1 和 2): 这个神奇的嵌入机(embedding machine)认为这些是与所提出的问题最相关的答案,因此这些就是我们提取出来发送给LLM的知识片段! 实际上,“最接近的点是哪个”这个问题是通过查询向量数据库来解决的。因此,实际过程看起来更像是这样: 知识片段的查询本身涉及一些比较复杂的数学计算,通常使用余弦距离进行计算,当然还有其他计算方法。 数学是一个可以深入研究的领域,但不在本篇文章的讨论范围之内,而且从实用的角度来看,它可以大部分转移到函数库或数据库中。 现在回到LangChain代码 在一开始的LangChain代码示例中,已经涵盖了这一行代码所做的所有工作。这个小函数的调用隐藏了背后很复杂的流程! index.query("What should I work on?") 09 为知识库创建索引 好的,我们马上就要完成了。我们现在已经了解了如何使用嵌入来找到知识库中最相关的内容,然后将所有内容传递给LLM,并获得检索增强后的答案。我们将要介绍的最后一步是从知识库中创建初始索引。换句话说,就是这张图片中的“知识分割机(knowledge splitting machine)”: 也许会令你惊讶,为知识库创建索引通常是整个流程中最难也是最重要的部分。不幸的是,它更像是一门艺术,而不是科学,需要反复试验、不断试错。 从大的方面看,索引过程可以分为两个高层次的步骤: 加载:从通常存储知识库的位置获取其内容。 分割:将知识(knowledge)分割成适合嵌入搜索的片段大小。 区分一下下面这两个技术概念 从技术角度来看,“加载器(loaders)”和“分割器(splitters)”之间的区别有些粗暴。你可以想象一个组件同时完成所有工作,也可以把加载阶段拆分成多个子组件。 尽管如此,"加载器 "和 "分割器 "在 LangChain 中就是这样实现的, "加载器"和"分割器"提供了一个有用的抽象层,使得底层的概念更易于理解和使用。它们为开发者提供了一种高层次的接口,使得他们可以更方便地处理和操作知识库的内容。 接下来以我自己的使用案例为例。我想构建一个Chatbot来回答关于我的SaaS脚手架产品SaaS Pegasus[3]的相关问题。我想要添加到知识库中的第一个内容就是文档站点。加载器是一种基础设施,它访问我的文档站点,找出可用的页面,然后拉取每个页面。当加载器工作完成后,会输出单个文档,每个站点页面都有一个文档。 在加载器内部执行了很多步骤!需要爬取所有页面,抓取每个页面的内容,然后将HTML格式化为可用的文本。而针对其他内容的加载器,例如针对PDF或Google Drive,都有不同的加载器组成。还需要解决并行化、错误处理等等问题。再一次强调,这是一个几乎无限复杂的话题,但在本文中,我们将大部分内容转移使用函数库。因此,我们再一次假设有一个神奇的盒子,将“知识库”放入其中,然后单个的 "文档 "从这里出来。 LangChain内置的加载器是LangChain中最有用的部分之一。它提供了一长串内置加载器[4],可用于从微软 Word 文档到整个 Notion 站点的任何内容中提取内容。 LangChain加载器的接口与上面描述的完全相同。将“知识库”输入,然后输出一个“文档”列表。 从加载器中输出后,我们将得到与文档站点中的每个页面相对应的文档集合。此时,理想情况下,已经删除了额外的内容标记,只保留了底层结构和文本。 现在,我们可以将整个网页传递给嵌入机( embedding machine),并将其用作知识片段。但是,每个页面都可能涵盖了很多内容!而且,页面中的内容越多,该页面的嵌入就越“不具体” 。这意味着我们的“closeness”搜索算法可能会失灵。(译者注:“closeness”搜索算法用于确定用户提出的问题与知识库中的文本片段之间的相似度,以便找到最相关的答案。该算法通常基于文本的词频、词向量或其他文本特征来计算相似度。) 更有可能的情况是,用户问题的主题与页面内的某些文本相吻合。这就是分割器(splitting)的作用所在。通过分割器,我们将任何单个文档分割成适合搜索的小块,使其更适合进行嵌入搜索。 此外,将文档分割成小块也是一门艺术,包括平均分割块的大小(如果太大,则无法很好地匹配查询,如果太小,则没有足够的有用上下文来生成答案),如何分割(通常是按标题分割,如果有的话),等等。但是,一些合理的默认设置足以开始处理和优化数据。 在LangChain中,分割器(splitters)属于称为“文档转换器[5](document transformers)”的这一更大的类别。除了提供各种分割文档的策略外,它们还具有删除冗余内容、翻译、添加元数据等工具。在这里,我们只关注分割器,因为它们占了文档转换中的大多数工作。 获得文档片段后,如上所述,我们将它们保存到向量数据库中,最后就大功告成了! 这就是为知识库创建索引的全部完整过程。 在LangChain中,整个索引过程都封装在这两行代码中。首先,我们初始化网站加载器,并告诉它我们要使用哪些内容: loader = WebBaseLoader("http://www.paulgraham.com/greatwork.html") 然后,我们从加载器构建整个索引,并将其保存到向量数据库中: index = VectorstoreIndexCreator().from_loaders([loader]) 加载、分割、转化为嵌入和保存都是在幕后进行的。 10 总结整个过程 最后,我们可以完整地展示整个RAG管道。如下所示: 首先,我们需要对知识库进行索引。 使用加载器获取知识并将其转换为单个文档,然后使用分割器将其转换为小块或片段。有了这些片段后,我们将它们传递给嵌入机,嵌入机将它们转换为可以用于语义搜索的向量。我们将这些嵌入向量与其文本片段一起保存在向量数据库中。 接下来是检索。该过程始于提出的问题,然后将问题通过相同的嵌入机发送到向量数据库中,以确定最匹配的片段,最后将使用这些片段来回答问题。 最后是检索增强的答案生成。将知识片段与自定义的system prompt和我们提出的问题一起格式化,最终得到针对具体语境的答案。 END 参考资料 [1]http://www.paulgraham.com/greatwork.html [2]https://scriv.ai/a/scriv/bots/b/pg-bot/chat/ [3]https://www.saaspegasus.com/ [4]https://python.langchain.com/docs/integrations/document_loaders/ [5]https://python.langchain.com/docs/modules/data_connection/document_transformers/ 本文经原作者授权,由Baihai IDP编译。如需转载译文,请联系获取授权。 原文链接: https://scriv.ai/guides/retrieval-augmented-generation-overview/ 🚢🚢🚢欢迎小伙伴们加入AI技术软件及技术交流群,追踪前沿热点,共探技术难题~

优秀的个人博客,低调大师

专访阿里云闵万里:云上逐鹿,ET大脑要做行业化、垂直化的创新运用

编者按:10月11-14日,为期四天的2017杭州云栖大会(门票火热抢购中!)将再度在杭州云栖小镇起航,作为全球最具影响力的科技展会之一,本届大会将有不少阿里集团专家以及各企业行业领袖的精彩演讲。从今天开始,云栖社区将会对大会嘉宾做一系列专访(关注“云栖大会”社区公众号了解更多大会信息)。本期我们采访的是阿里云人工智能科学家闵万里,他将在10月的云栖大会·阿里云生态峰会上分享阿里云ET大脑的项目实践,并重点介绍ET大脑的应用场景、技术挑战和未来发展。 2017杭州云栖大会详情请戳这里! 闵万里,阿里云人工智能科学家,ET大脑负责人。2004年博士毕业以后加入IBM,参与IBM智慧城市研究,主攻大数据的研发和运用,2010-2012年在IBM新加坡做智慧城市,推动创新项目落地。2012年加入Google,担任数据科学家,做广告竞价的竞价

优秀的个人博客,低调大师

Android 自定义控件之SlidingMenuVertical顶部悬浮(垂直折叠抽屉,有滑动渐变回调,可自行添加渐变动画)

GitHub APK 使用方法 将libray模块复制到项目中,或者直接在build.gradle中依赖: allprojects { repositories { maven { url 'https://jitpack.io' } } } dependencies { compile 'com.github.AnJiaoDe:SlidingMenuVertical:V1.1.2' } 注意:如果sync报错,是因为和com.android.tools.build:gradle 3.0有关,可以改将compile改为implementation 或者api 状态栏全透明请参考 注意:布局一定是SlidingMenuVertical包含2个直接子View <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.cy.translucentparent.StatusBarView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#ff3F51B5"/> <RelativeLayout android:layout_width="match_parent" android:layout_height="48dp" android:background="#ff3F51B5"> <ImageView android:layout_width="wrap_content" android:layout_height="match_parent" android:padding="10dp" android:scaleType="centerInside" android:src="@drawable/back" /> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="抽屉" android:textColor="#ffffff" /> <TextView android:id="@+id/tv_switch" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_alignParentRight="true" android:gravity="center" android:padding="10dp" android:text="开关" android:textColor="#ffffff" /> </RelativeLayout> <com.cy.slidemenuvertical.SlidingMenuVertical android:id="@+id/slidingMenu" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#33454545" android:text="上面面\n上面面\n上面面\n上面面\n上面面\n上面面\n上面面\n上面面\n上面面\n上面面\n上面面\n上面面\n上面面\n上面面\n上面面\n" /> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/tv_middle" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:paddingBottom="10dp" android:paddingTop="10dp" android:text="悬浮" android:textColor="#454545" /> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent" android:background="#e6e6e6" android:overScrollMode="never" android:scrollbars="vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="下面\n下面\n下面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n下面\n菜逼" /> </ScrollView> </LinearLayout> </com.cy.slidemenuvertical.SlidingMenuVertical> </LinearLayout> public abstract class BaseActivity extends AppCompatActivity implements View.OnClickListener { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); StatusNavUtils.setStatusBarColor(this,0x00000000); } public void startAppcompatActivity(Class<?> cls) { startActivity(new Intent(this, cls)); } } public class MainActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final TextView tv_middle = (TextView) findViewById(R.id.tv_middle); final SlidingMenuVertical slidingMenuVertical = ((SlidingMenuVertical) findViewById(R.id.slidingMenu)); slidingMenuVertical.setDuration_max(300);//设置 设置松手后 开闭最长过渡时间 slidingMenuVertical.setAmbit_scroll(100);//修改滑动界限 值,值越大 开闭越难 单位ms slidingMenuVertical.setOnSwitchListener(new SlidingMenuVertical.OnSwitchListener() { /* 滑动中 y_now:实时view_bottom的top y, y_opened:抽屉打开时view_bootom的top y,y_closed:抽屉关闭时view_bottom的top y top y:在屏幕中的top y坐标 */ @Override public void onSwitching(boolean isToOpen, int y_now, int y_opened, int y_closed) { tv_middle.setBackgroundColor(Color.argb((int) (1.0f * (y_opened - y_now) / (y_opened - y_closed) * 255), Color.red(0xff3F51B5), Color.green(0xff3F51B5), Color.blue(0xff3F51B5))); tv_middle.setTextColor(Color.argb((int) (1.0f * (y_opened - y_now) / (y_opened - y_closed) * 255), Color.red(0xffffffff), Color.green(0xffffffff), Color.blue(0xffffffff))); } @Override public void onSwitched(boolean opened) { if (opened) { tv_middle.setBackgroundColor(0xffffffff); tv_middle.setTextColor(0xff454545); } } }); findViewById(R.id.tv_switch).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { slidingMenuVertical.open(!slidingMenuVertical.isOpened()); } }); } @Override public void onClick(View v) { } } 源码 SlidingMenuVertical public class SlidingMenuVertical extends LinearLayout { private Scroller mScroller; private View view_top; private View view_bottom; private float downX; private float downY; private boolean opened = true;//状态是否开闭 private OnSwitchListener onSwitchListener; private int duration_max = 300;//最长过度时间 private int ambit_scroll = 100;//滑动界限,开闭 private int y_opened = -1; // * y_opened:抽屉打开时view_bootom的top y public SlidingMenuVertical(Context context) { this(context, null); } public SlidingMenuVertical(Context context, AttributeSet attrs) { super(context, attrs); mScroller = new Scroller(context); setOrientation(VERTICAL); } @Override protected void onFinishInflate() { // 当xml解析完成时的回调 view_top = getChildAt(0); view_bottom = getChildAt(1); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); view_top.measure(widthMeasureSpec, ViewMeasureUtils.getChildHeightMeasureSpec(view_top, heightMeasureSpec)); // view_middle.measure(widthMeasureSpec,ViewMeasureUtils.getChildHeightMeasureSpec(view_middle,heightMeasureSpec)); view_bottom.measure(widthMeasureSpec, heightMeasureSpec); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { setY_opened(); // 拦截 // 竖直滑动时,去拦截 switch (event.getAction()) { case MotionEvent.ACTION_DOWN: downX = event.getX(); downY = event.getY(); break; case MotionEvent.ACTION_MOVE: float moveX = event.getX(); float moveY = event.getY(); // 竖直滑动 if (Math.abs(moveY - downY) > Math.abs(moveX - downX)) { //上面隐藏 if (opened == false) { return false; } //上面显示并且下滑 if (opened == true && (moveY - downY) > 0) { return false; } return true; } break; case MotionEvent.ACTION_UP: break; default: break; } return super.onInterceptTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: downX = event.getX(); downY = event.getY(); break; case MotionEvent.ACTION_MOVE: float moveX = event.getX(); float moveY = event.getY(); int dy = (int) (downY - moveY + 0.5f);// 四舍五入 20.9 + 0.5-->20 // Log.e("dy","++++++++++++++++++++++++++++"+dy); int scrollY = getScrollY(); //mDownY - moveY>0上滑 if (scrollY + dy > 0) { scrollBy(0, dy); if (scrollY + dy > getHeight_top()) { scrollTo(0, getHeight_top()); } } downX = moveX; downY = moveY; break; case MotionEvent.ACTION_UP: // Log.e("heigth_top", "+++++++++++++++++" + height_top); // Log.e("scrollY", "+++++++++++++++++" + getScrollY()); if (opened) { open(!(getScrollY() > ambit_scroll || getScrollY() > getHeight_top() / 3)); } else { open(getScrollY() < getHeight_top() - ambit_scroll || getScrollY() < getHeight_top() * 2 / 3); } break; } // 消费掉 return true; } /** * 开闭抽屉 * * @param open */ public void open(boolean open) { setY_opened(); this.opened = open; //打开 if (open) { // Log.e("打开", "+++++++++++++++++++++++++++++"); int startX = getScrollX();// 起始的坐标X int startY = getScrollY();// 起始的坐标Y int endX = 0; int endY = 0; int dx = endX - startX;// 增量X int dy = endY - startY;// 增量Y // 1px = 10 int duration = Math.abs(dy) * 10; if (duration > duration_max) { duration = duration_max; } mScroller.startScroll(startX, startY, dx, dy, duration); } else { Log.e("关闭", "+++++++++++++++++++++++++++++" + getScrollY()); int startX = getScrollX();// 起始的坐标X int startY = getScrollY();// 起始的坐标Y int endX = 0; int endY = getHeight_top(); int dx = endX - startX;// 增量X int dy = endY - startY;// 增量Y // 1px = 10 int duration = Math.abs(dy) * 10; if (duration > duration_max) { duration = duration_max; } // 模拟数据变化 mScroller.startScroll(startX, startY, dx, dy, duration); } invalidate();// 触发ui绘制 --> draw() --> dispatchDraw()--> drawChild --> } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) {// 如果正在计算的过程中 // 更新滚动的位置 scrollTo(0, mScroller.getCurrY()); invalidate(); } } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); // Log.e("y_now", ScreenUtils.getViewScreenLocation(view_bottom)[1] + "++++++++++++++++++++++"); // // Log.e("y_closed", y_opened - height_top + "++++++++++++++++++++++"); if (onSwitchListener != null) { onSwitchListener.onSwitching(t - oldt < 0 ? true : false, getY_now(), getY_opened(), getY_opened() - getHeight_top()); if (getY_now() == getY_opened()) { // Log.e("true", "++++++++++++++++++++++++"); onSwitchListener.onSwitched(true); } if (getY_now() == getY_opened() - getHeight_top()) { // Log.e("false", "++++++++++++++++++++++++"); onSwitchListener.onSwitched(false); } } } public boolean isOpened() { return opened; } public int getDuration_max() { return duration_max; } /** * 设置松手后 开闭最长过渡时间 * * @param duration_max */ public void setDuration_max(int duration_max) { this.duration_max = duration_max; } public View getView_top() { return view_top; } public View getView_bottom() { return view_bottom; } public int getHeight_top() { return view_top.getMeasuredHeight(); } /** * 获取 * y_opened:抽屉打开时view_bootom的top y */ private void setY_opened(){ if (y_opened<0){ y_opened=getViewScreenLocation(view_bottom)[1]; Log.e("y _open",y_opened+"++++++++++++++++++++"); } } /** * y_opened:抽屉打开时view_bootom的top y * * @return */ public int getY_opened() { if (y_opened<0){ Log.e("还未计算出来","+++++++++++++++++++++++++++++++++++"); return 0; } return y_opened; } /** * y_now:抽屉实时view_bootom的top y * * @return */ public int getY_now() { return getViewScreenLocation(view_bottom)[1]; } public int getAmbit_scroll() { return ambit_scroll; } /** * 修改滑动界限 值,值越大 开闭越难 单位ms * * @param ambit_scroll <height_top */ public void setAmbit_scroll(int ambit_scroll) { this.ambit_scroll = ambit_scroll; } /** * 计算指定的 View 在屏幕中的坐标。 */ public int[] getViewScreenLocation(View view) { int[] location = new int[2]; // 获取控件在屏幕中的位置,返回的数组分别为控件左顶点的 x、y 的值 view.getLocationOnScreen(location); return location; } public interface OnSwitchListener { /* 滑动中 y_now:实时view_bottom的top y, y_opened:抽屉打开时view_bootom的top y,y_closed:抽屉关闭时view_bottom的top y top y:在屏幕中的top y坐标 */ public void onSwitching(boolean isToOpen, int y_now, int y_opened, int y_closed); /* 滑动停止,状态是否开闭 */ public void onSwitched(boolean opened); } public void setOnSwitchListener(OnSwitchListener onSwitchListener) { this.onSwitchListener = onSwitchListener; } } ViewMeasureUtils public class ViewMeasureUtils { /** * 根据父 View 规则和子 View 的 LayoutParams,计算子类的宽度(width)测量规则 * * @param view */ public static int getChildWidthMeasureSpec(View view, int parentWidthMeasureSpec) { // 获取父 View 的测量模式 int parentWidthMode = MeasureSpec.getMode(parentWidthMeasureSpec); // 获取父 View 的测量尺寸 int parentWidthSize = MeasureSpec.getSize(parentWidthMeasureSpec); // 定义子 View 的测量规则 int childWidthMeasureSpec = 0; // 获取子 View 的 LayoutParams ViewGroup.LayoutParams layoutParams = (ViewGroup.LayoutParams) view.getLayoutParams(); if (parentWidthMode == MeasureSpec.EXACTLY || parentWidthMode == MeasureSpec.AT_MOST) { /* 这是当父类的模式是 dp 的情况 */ if (layoutParams.width > 0) { childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY); } else if (layoutParams.width == ViewGroup.LayoutParams.WRAP_CONTENT) { childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(parentWidthSize, MeasureSpec.AT_MOST); } else if (layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT) { childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(parentWidthSize, MeasureSpec.EXACTLY); } } else if (parentWidthMode == MeasureSpec.UNSPECIFIED) { /* 这是当父类的模式是 MATCH_PARENT 的情况 */ if (layoutParams.width > 0) { childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY); } else if (layoutParams.width == ViewGroup.LayoutParams.WRAP_CONTENT) { childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); } else if (layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT) { childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); } } // 返回子 View 的测量规则 return childWidthMeasureSpec; } /** * 根据父 View 规则和子 View 的 LayoutParams,计算子类的宽度(width)测量规则 * * @param view */ public static int getChildHeightMeasureSpec(View view, int parentHeightMeasureSpec) { // 获取父 View 的测量模式 int parentHeightMode = MeasureSpec.getMode(parentHeightMeasureSpec); // 获取父 View 的测量尺寸 int parentHeightSize = MeasureSpec.getSize(parentHeightMeasureSpec); // 定义子 View 的测量规则 int childHeightMeasureSpec = 0; // 获取子 View 的 LayoutParams ViewGroup.LayoutParams layoutParams = (ViewGroup.LayoutParams) view.getLayoutParams(); if (parentHeightMode == MeasureSpec.EXACTLY || parentHeightMode == MeasureSpec.AT_MOST) { /* 这是当父类的模式是 dp 的情况 */ if (layoutParams.height > 0) { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY); } else if (layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(parentHeightSize, MeasureSpec.AT_MOST); } else if (layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT) { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(parentHeightSize, MeasureSpec.EXACTLY); } } else if (parentHeightMode == MeasureSpec.UNSPECIFIED) { /* 这是当父类的模式是 MATCH_PARENT 的情况 */ if (layoutParams.height > 0) { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY); } else if (layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); } else if (layoutParams.height == ViewGroup.LayoutParams.MATCH_PARENT) { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); } } // 返回子 View 的测量规则 return childHeightMeasureSpec; } } GitHub 关注专题Android开发常用开源库 简书 微信公众号 QQ群

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册