
磨碎的纳豆.
github传送门 目录 前言 PowerVR CMake环境 FreeImage CMakeLists.txt解析 源码 最后 前言 作为一个梦想成为游戏制作人的菜鸟程序员, 我终究没悬念地踏上了撰写shader的道路(手动滑稽). 这是一篇比较细致的Ubuntu18.04下OpenGL_ES环境搭建的文件, 也是我爬过n多个坑之后的总结, 希望能帮助到Mac背后的你(手动滑稽). PowerVR 模拟器方面, 我选择PowerVR, 当然, 你可以选择别的, 来到官网, 下载对应的SDK, 运行下载后的文件, 即可安装. 当然了, 如果不能运行, 用chmod添加运行权限即可. 我的第一个Hello, World案例也是基于PowerVR的Hello案例魔改的. 值得一提的就是安装目录记一下, 之后要用到: CMake环境 这里我选用CLion开发, 这样更友好, 尽管CLion的vim似乎不够强大, 但是考虑到调试等功能, 还是值得拥有的. 当然, 自己用vim搭建IDE也是完全OK的. 先确保有安装了build-essential, libx11-dev, 当然了, 你的Linux可能不是X11, 做出相应修改即可. sudo apt-get install build-essential libx11-dev FreeImage 纹理加载我选用的是FreeImage, 使用也比较简单, 下载, 编译, 安装即可. make make install 不知道为啥, 我打不开官网, 不过有人在github备份了. CMakeLists.txt解析 先贴出CMakeLists.txt全文. cmake_minimum_required(VERSION 3.15) project(gles_demo) set(CMAKE_CXX_STANDARD 14) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(SDK_ROOT /opt/Imagination/PowerVR_Graphics/PowerVR_SDK/SDK_2020_R2) # sdk目录 set(CMAKE_MODULE_PATH ${SDK_ROOT}/cmake/modules) if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Release") endif () set(INCLUDE_DIR ${SDK_ROOT}/include/) # gles库 find_library(EGL_LIBRARY EGL "/opt/Imagination/PowerVR_Graphics/PowerVR_Tools/PVRVFrame/Library/Linux_x86_64/") find_library(GLES_LIBRARY GLESv2 "/opt/Imagination/PowerVR_Graphics/PowerVR_Tools/PVRVFrame/Library/Linux_x86_64/") # FreeImage库 find_library(FI_LIBRARY freeimage "/usr/lib/") list(APPEND PLATFORM_LIBS ${EGL_LIBRARY} ${GLES_LIBRARY} ${FI_LIBRARY} ${CMAKE_DL_LIBS}) if (UNIX) set(WS_DEFINE "") if (NOT WS) set(WS "X11") set(WS_DEFINE "${WS}") endif () if (NOT DEFINED CMAKE_PREFIX_PATH) set(CMAKE_PREFIX_PATH $ENV{CMAKE_PREFIX_PATH}) endif () add_definitions(-D${WS_DEFINE}) if (${WS} STREQUAL X11) find_package(X11 REQUIRED) if (NOT ${X11_FOUND}) message(FATAL_ERROR "X11 libraries could not be found. Please try setting: -DCMAKE_PREFIX_PATH pointing towards your X11 libraries") endif () list(APPEND PLATFORM_LIBS ${X11_LIBRARIES}) include_directories(${X11_INCLUDE_DIR}) set(SRC_FILES gles_x11.cpp) # 源码 else () message(FATAL_ERROR "Unrecognised WS: Valid values are NullWS(default), X11, Wayland, Screen.") endif () add_definitions(-D${WS}) #Add a compiler definition so that our header files know what we're building for add_executable(gles_demo ${SRC_FILES}) endif () if (PLATFORM_LIBS) target_link_libraries(gles_demo ${PLATFORM_LIBS}) endif () target_include_directories(gles_demo PUBLIC ${INCLUDE_DIR}) # include目录 target_compile_definitions(gles_demo PUBLIC $<$<CONFIG:Debug>:DEBUG=1> $<$<NOT:$<CONFIG:Debug>>:RELEASE=1>) # Defines DEBUG=1 or RELEASE=1 可以看到, 我手动添加了FreeImage库和模拟器给的两个库, libEGL.so, libGLESv2.so. 这里我想吐槽一下win, 非要搞出一个.lib, 又一个.dll, 明明一个.so就搞定的事情.至于X11的库, 之前也说了, 如果你是其他的Linux, 找对应的库, 修改CMake内容即可, 当然了, cpp文件也要重写. 所以, 这里才用了PowerVR的例子, 他们已经把全平台的CMake和源码都写好了, 改改就行(手机狗头). 当然, OpenGL_ES指南有一份跨平台的源码, 我也尝试过, 缺点是似乎只能使用c语言, 我反复修改构建也是如此, 可能是我对编译原理的理解还不到位, 所以就放弃了指南的源码. 毕竟都是要二次封装的, 只用c的话, 臣妾做不到啊(手动无奈). 源码 源码部分, 我先用一个Util类封装了大部分不需要过多关系的操作, 把处理重心放在初始化和绘制上面. 所以先来看下main函数的内容. int main(int /*argc*/, char ** /*argv*/) { start = clock(); // opengl_es工具类实例 std::string appName("GLES Demo"); GLESUtils glesUtils(WIN_WIDTH, WIN_HEIGHT, appName); // 初始化本地和EGL相关 glesUtils.initNativeAndEGL(); // 初始化shader if (!glesUtils.initShaders()) { glesUtils.cleanProc(); } // 绘图, 循环次数为帧数 for (int i = 0; i < 80000; ++i) { if (!glesUtils.renderScene()) { break; } } // 释放资源 glesUtils.deInitGLState(); return 0; } 生成工具类实例 设置窗口大小和名称 初始化本地和EGL相关变量 然后是关键的初始化shader和绘图 最后 来看看效果吧. 至于更多有关OpenGL_ES的内容, 就要等后续的部分啦. 喜欢记得点赞或者关注哦~
目录 前言 python3 exe安装 pip whl安装 setup.py安装 最后 前言 总有某些公司是offline的, 你懂的, 然后用着古老的服务器. 所有有了这种奇怪的需求. 这里分成三个部分来说, 首先是py3的离线安装, 就是大家熟悉的exe安装. 然后是pip使用下载好的whl文件进行库的安装. 最后是使用setup.py进行安装. python3 exe安装 首先来到官网的Windows下载页, 选择合适的版本, 比如我选的py3.7.5的64位exe安装包: 然后就是常规安装, 记得勾选Add... 装完之后打开powershell测试一下: pip whl安装 然后下载必备的lxml文件, 选择对应的版本: 之后pip install whl文件路径即可. setup.py安装 为啥说python-docx典型呢, 因为它只有source, 没有whl文件. 然后进入目录, 运行python setup.py install即可.值得一提的就是, python-docx是依赖lxml的, 所以要先装lxml, 当然, py3的安装肯定是最先的. 最后 喜欢记得点赞, 有意见或者建议, 评论区见哦~
目录 前言 安装GCC 最后 前言 最近迷上了泛型编程, 看到了C++11, 14, 17的很多酷炫新特性. 之前也是在Linux下也跑了一些代码, 所以不觉得放到mac会有什么问题, 直到我看了mac默认的GCC版本, 4.2.1. C++11需要至少GCC4.7. 那这样肯定是不行的. 安装GCC 于是我呼唤homebrew brew search gcc 作为一个编程多年, 趟过无数坑的渣渣, 我一般不会直接选最新, 于是先尝试brew install gcc@4.9. 很遗憾, 不行. 然后我brew install gcc@5. 下载成功, 然后我打开我的.zshrc, 最后写入: alias gcc='gcc-5' alias cc='gcc-5' alias g++='g++-5' alias c++='c++-5' source更新下.zshrc. 找了一个cpp文件开始编译. 果不其然, 报错了. stackoverflow一下, 说是安装xcode-select, 很遗憾, 我已经装了. 然后说是, open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg, 很遗憾, 我安装失败.于是我觉得从5跳到6. 重复刚才的操作: 然后编译, 成功运行. 最后 喜欢记得点赞, 有意见或者建议评论区见哦~
CV预备(一): conv2, filter2, imfilter的差别CV预备(二): im2col与col2im 目录 前言 安装 测试 macOS安装OpenCV3.X 最后 前言 为什么在OpenCV4.X出了n多个版本的时候, 我要来搭建3.X, 无他, 就是我目前的一些工程要调用的库需要3.X. 顺带, 在mac上也安装一下. 安装 首先如果是我, 会用ssh访问Ubuntu, 所以要先安装下ssh. sudo apt install net-tools sudo apt-get install openssh-server 然后补一些必要的库: sudo apt-get install build-essential cmake git libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev 用wget下载OpenCV3.X, 这里是3.4.7 sudo apt-get install wget wget https://github.com/opencv/opencv/archive/3.4.7.zip 之后解压这个zip包, 进入解压目录, 新建build文件夹 mkdir build ; cd build cmake -D CMAKE_BUILD_TYPE=Release -D CMAKE_INSTALL_PREFIX=/usr/local -D PYTHON_DEFAULT_EXECUTABLE=/usr/bin/python3 -D BUILD_opencv_python3=ON -D BUILD_opencv_python2=OFF .. cmake这里需要说一下, /usr/local是安装路径, 可以修改. 然后, 后面一大串的目的就是编译的时候, python默认用python3. 然后开始编译和安装. sudo make -j2 sudo make install 之后如果你用python, 你需要用pip3下载opencv-python, 如果你是C++, 就可以跳过. sudo apt install python3-pip pip3 install opencv-python 运行python3, 输入import cv2, 无错误就是成功. 测试 官方提供了一个打开摄像头的测试用例, 进入如图目录, 编译运行. cmake . make ./opencv_example 或者, 你可以用g++嘛. 注意不是单引号', 是波浪线下面那个点`. g++ example.cpp `pkg-config opencv --libs --cflags opencv` 当然, 可以用自己的案例. 比如这里, 我用了一个Ransac算法, 新建build目录, 然后编译运行, 也完全ok. macOS安装OpenCV3.X mac上安装比较简单, 主要还是依靠homebrew. 首先用homebrew看下版本: brew search opencv 这里我们要装的是opencv@3 brew install opencv@3 下载完成之后, 你需要把动态库进行链接.比方说, 我已经进入我用virtualenv构建的虚拟环境env1的/lib/python3.7/site-packages, 使用如下链接指令, 就可以把.so文件链接到当前环境目录下. ln -s /usr/local/Cellar/opencv@3/3.4.5_6/lib/python3.7/site-packages/cv2/python-3.7/cv2.cpython-37m-darwin.so cv2.so 链接成功之后, 可以用ll查看: 然后可以进入python环境进行测试, 可以成功导入并查看版本号就是链接成功. 最后 喜欢记得点赞或者关注我哦, 有意见或者建议评论区见~
目录 前言 读取Word内容 NPOI NPOI安装 NPOI提取Word内容 用Costura.Fody打包DLL python-docx 读取PDF内容 python-docx自动生成Word 全局字体 内容字体 单元格合并 最后 前言 Word就是那种很难用, 很丑陋, 但是你不得不用的东西, 在这一点上, 它甚至比Windows更甚(毕竟Gates是通过帮水果写Office才有机会接触Macintash和施乐的嘛, 你听过的, 两个小偷的故事). Windows可以用macOS + PlayStation进行1000%的替代, 没有多打0(手动滑稽). 但是Office不能够, 并不是没有比Office更好的东西, 这是一个历史残留问题, 就像牙膏厂CPU里面, 那些莫名其妙的字段一样.总之, 这里通过使用一些库, Python的python-docx, C#的pdfbox和npoi, 来让对Word和PDF的处理变得更加自动化一些.最后, 如果你想设计一些定制化的功能, 还是希望可以从官方文档进行学习, 而不是通过看博客. 尤其是当你只能够用某度, 而不是某歌, 那些前几页给出的搜索结果老旧又不顶用, 说真的, 用某度还不如在博客网站进行站内搜索, 不过我最近发现某日头条的全网搜索给出的结果还不错, 如果你不能某歌, 可以用下某日头条, 再不济用某应(Bing)也好过某度. 读取Word内容 好了, 不多说废话了. 直接看从Word获取内容. 这里可以用C#的NPOI和python-docx实现. NPOI NPOI安装 来看下维基的介绍. Apache POI是Apache软件基金会的开放源码库, POI提供API给Java程序对Microsoft Office格式文件读和写的功能. .NET的开发人员则可以利用NPOI(POI for .NET)来访问POI的功能.其实, 最近这几年, 巨硬通过推出像.NET Core这样的跨平台应用程序开发框架, 已经让C#有了一点起死回生的迹象, 我不喜欢巨硬, 但我很推崇这种战略, 当然了, 甚至在硬件上推出了Duo这样的Surface安卓设备. 虽然之前写Unity游戏的时候用过一些C#, 但是这次是我第一次从软件开发的角度使用C#, 不得不说, NuGet令我印象深刻, 很好用.这里假设你已经装了vs2019或者旧一点的版本, 但是注意, .NET Framework的程序依旧只能在Windows进行开发, 因为我暂时还没有摸像Mono这样的环境, 如果你感兴趣, 可以试下. 新建.NET Framework控制台应用: 然后你只需要在搜索框输入nuget, 点击管理NuGet程序包: 之后搜索NPOI, 点击安装, 就可以了. 可能比起mac的brew install, linux的apt-get install和python的pip3 install多了两步, 但是我已经很满意了, 比什么找DLL, 拷贝DLL之类的, 要显得9102的多. 安装之后, 在右侧的解决方案引用里面, 已经可以看到添加的库了: NPOI提取Word内容 其实NPOI非常强大, 足以用来做和Word有关的一切了, 但是, 这里只演示一下提取Word中的内容, 因为后面有python-docx这样更加轻巧的库, 不需要vs不需要Windows, 你就可以处理docx类型的文件了. 源码如下: using NPOI.XWPF.UserModel; using System.IO; using System.Text; namespace getWord { class Program { static void Main(string[] args) { string in_path = System.Console.ReadLine(); string out_path = System.Console.ReadLine(); Stream stream = File.OpenRead(in_path); XWPFDocument doc = new XWPFDocument(stream); string text = ""; string tmp_text; foreach (var para in doc.Paragraphs) { tmp_text = para.ParagraphText; if (tmp_text.Trim() != "") text += tmp_text + "\n"; } StreamWriter swPdfChange = new StreamWriter(out_path, false, Encoding.GetEncoding("gb2312")); swPdfChange.Write(text); swPdfChange.Close(); } } } 我从控制台读取了输入输出路径, 然后循环读取Word内容写入缓存, 最后转码成gb2312到输出文件.最终, 我还是希望你去NPOI官网看看. 用Costura.Fody打包DLL 如果你就这样直接生成Release版本, 你会被老板骂的狗血淋头, 太不专业了. 至少你应该把DLL打包进EXE或DLL.你可以把DLL作为资源文件进行打包, 但是这样不优雅, 很土. 同样, 我们用9102年应该用的方法.在NuGet搜索Costura.Fody, 安装即可. 这样的话, 编译成Release版本的时候, 直接就打包成一个EXE文件了. python-docx 好了, 到了Python, 一切都舒服了, 忘记刚才为了写C#安装的好几个G甚至几十个G的vs吧, 毕竟Gates说过'640K is more memory than anyone will ever need.'(手动滑稽)现在你只需要: pip3 install python-docx 而且, 官方文档写得很不错, 并且我发现在作业部落(对, 就是我的macOS上有什么里面推荐的那个cmd markdown)的一篇python-docx中文, 几乎就是官方中文. 所以, 我基本就靠这两个外加谷歌, 完成了全部的内容学习, 当然, 你会发现, 难点还是在Table处理和样式修改那里. import docx doc = docx.Document('./t.docx') doc_text = '' doc_table_text = '' for paragraph in doc.paragraphs: doc_text += paragraph.text + '\n' for table in doc.tables: for row in table.rows: for cell in row.cells: doc_table_text += cell.text + '\n' with open('./tt.txt', 'w') as f: f.write(doc_text) f.write(doc_table_text) # doc.save ('./tt.docx') 代码其实很好懂, 关于python-docx的一些细节操作, 除了官方文档, 我在后面的自动化生成Word里面也会分享一些我的处理经验, 当然, 更多的是处理时候的坑(手动无奈). 读取PDF内容 同样, 这次用的是C#的库, 名为Pdfbox. 其实呢, 这个Pdfbox是个Java库. 是由Apache PDFBox团队为.NET生成的. using org.apache.pdfbox.pdmodel; using org.apache.pdfbox.util; using System.IO; using System.Text; namespace getPDFCon { class Program { static void Main(string[] args) { string in_path = System.Console.ReadLine(); string out_path = System.Console.ReadLine(); PDDocument doc = PDDocument.load(in_path); PDFTextStripper pdfStripper = new PDFTextStripper(); string text = pdfStripper.getText(doc); // Console.WriteLine(Utf8ToGB2312(text)); // Console.ReadKey(); StreamWriter swPdfChange = new StreamWriter(out_path, false, Encoding.GetEncoding("gb2312")); swPdfChange.Write(text); swPdfChange.Close(); } } } 和之前读取Word几乎是一样的思路, 不多说了. python-docx自动生成Word 这里我来细说一下, python-docx的一些操作. 从样式修改, 表格合并处理这些难点来谈. 后续也会逐步更新新遇到的坑. 全局字体 首先, 你可以设置全局字体. doc.styles['Normal'].font.name = u'宋体' doc.styles['Normal'].font.size = Pt (9) doc.styles['Normal']._element.rPr.rFonts.set(qn('w:eastAsia'), u'宋体') 注意, 如果是汉字, 第三行是必须要加的, 否则不生效. 第二行是设置字体大小, 你需要用from docx.shared import Pt进行导包. 当然, 你直接导入整个docx包就完事了. 内容字体 如果你想只修改某段内容的字体, 不影响全局, 之前的方案就不行. p = doc.add_paragraph () font = p.add_run ('标题').font font.bold = True font.size = Pt (14) p.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER 这里看到一个p.add_run, 这是给当前Paragraph实例添加的Run实例, 也就是运行时候的一些设置, 只对当前Paragraph实例生效. 来看下和直接设置Paragraph实例属性的比较. doc = Document () p = doc.add_paragraph () font = p.add_run ('标题1').font font.bold = True font.size = Pt (14) p.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER p2 = doc.add_paragraph () p2.text = ('标题2') p2.style.font.size = Pt (20) p3 = doc.add_paragraph () p3.text = ('标题3') p3.style.font.size = Pt (40) doc.save ('a.docx') 这段代码在想象中应该是段落内容越来越大, 对吧, 但是很遗憾, 对于标题3的字体设置会覆盖标题2的字体设置, 但是通过run对象进行设置的标题1就不会受到影响, 来看图说话: 这样一来, 想要很好处理某段内容的风格, 就必须使用run. 否则就会关联其他. 修改下代码, 都设置成add_run, 看看是否如此: 很好, 这才是想要的效果. 同理, 在表格内容里面也是如此, 不多赘述. 但是还有一点要注意, 比如你已经通过, p.text进行文字赋值, 但是, 你又用, p.add_run ('标题').font进行设置, 那么你会得到两份内容. 所以, 这里要特别注意, 如果通过样式填充, 就不用再用text字段进行赋值. 单元格合并 比如我现在建立一张表, 尝试合并. 然后你会发现, 合并之后, 把两份内容都保留了, 如果这是你需要的, 自然没有问题. 但是如果不是, 你就要思考内容合并的策略, 你不可能一个一个设置. 一个比较合理的策略就是用临时变量保留你要的内容, 合并完成之后, 将临时变量内容覆盖合并后的内容. 最后 其实, 不论是NPOI还是python-docx, 已经是非常不错的库了, 都可以很好地帮助开发者进行自动化word的生成. 如果你不这么觉得, 我举个反例. Microsoft.Office.Interop.Word是巨硬提供的com组件, 那么要如何使用它呢, 你要先装Windows, 再装Office, Office2013对应这个com组件的15.x版本, Office2007对应组件的12.x. 然后你写完代码, 每次运行还需要启动Word, 可以后台启动, 但终归是启动了, 所以效率非常低. 当然, 本章内容还会在后续使用当中持续更新, 直到我觉得没有必要更新了. 喜欢可以点个赞, 有意见或者建议, 评论区见哦~
目录 前言 homebrew iTerm2 oh-my-zsh 配色 毛玻璃 字体 powerlevel9k zsh插件 vim设置 配色 代码折叠 插件管理和使用 vim-powerline 多行注释 python配置 修改pip源 virtualenv配置 virtualenvwrapper使用 前言 最近mac不知道怎么了, 估计是新品又要到来了, 水果决定解决老机型过于流畅的bug, 出现各种问题, 比如屏幕底部会突然花屏, 一次约0.1s, 或者是界面卡死之类的. 还有就是插上扩展坞网速就为零. 所以趁着中秋, 重装一下, 然后这些bug都没了(我太难了.jpg). 顺带写下这篇配置篇, 省得以后麻烦.更新了vim配置内容 homebrew 每次提到homebrew, 除了必备神器之外, 还有就是谷歌: 我们90%的工程师使用您编写的软件(Homebrew), 但是您却无法在面试时在白板上写出翻转二叉树这道题, 这太糟糕了.(手动滑稽) 安装也很简单: /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" iTerm2 你可以从官网下载iterm2, 也可以用homebrew: brew install iTerm2 然后你会发现一个非常非常朴素的终端, 基本和mac自带的终端差不多, 不多说, 上一张素颜照: 接下来, 你就会和我一起, 将它打造成一个性冷淡御姐, 你懂我意思吧(老奸巨猾.jpg) oh-my-zsh 指令安装: sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)" 然后从bash切换到zsh, 其实我安装完成之后, 它自动切换了: chsh -s /bin/zsh 配色 iTerm2自带了一些配色, 但那肯定是不够的. mkdir ~/iterm2 ; cd ~/iterm2 git clone https://github.com/mbadolato/iTerm2-Color-Schemes 建一个目录, 叫什么都行, 然后下载这个配色方案包, 之后通过command + ,打开配置, 导入刚才下载的配色: 然后你就收获了满满的幸福: 然后你可以使用mv iterm2 .iterm2指令隐藏这个文件夹, 也可以不隐藏, 看你喜欢了. 毛玻璃 然后可以调整一下透明度和模糊度, b格满满的毛玻璃效果就出现了: 字体 字体其实是非常非常重要的, 回忆一下window终端的糟糕字体吧, 其实字体是非常影响整个系统的观感的, 从软件的角度来说也是如此. 这里安装nerd-fonts字体, 它的好处是还支持图标. brew tap caskroom/fonts brew cask install font-hack-nerd-font 然后在配置文件里面勾选, 注意, ascii和非ascii要一样大, 不一样会造成之后图标有些不对齐: powerlevel9k powerlevel9k真的是一个很酷的东西. git clone https://github.com/bhilburn/powerlevel9k.git ~/.oh-my-zsh/custom/themes/powerlevel9k 然后打开zsh的配置文件, 将主题设置进去: vim ~/.zshrc ZSH_THEME="powerlevel9k/powerlevel9k" 退出来之后更新一下zshsource ~/.zshrc. powerlevel9k本身还有许多设置内容, 这里我简单设置一下, 大家可以按需设置. POWERLEVEL9K_MODE="nerdfont-complete" POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(ssh dir vcs) POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(status root_indicator background_jobs virtualenv) 我的设置第一行代表用之前的nerd-fonts字体 第二行设置左边的图标显示内容, 分别是ssh, 目录和git等版本管理 第三行设置右, 依次是状态, 是否是root, 作业指示器, py的环境. 更多设置, 可以参看这篇文章 zsh插件 多的不说, 语法高亮和指令提示肯定要的. brew install zsh-syntax-highlighting brew install zsh-autosuggestions 然后在.zshrc里面补上如下内容: source /usr/local/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh source /usr/local/share/zsh-autosuggestions/zsh-autosuggestions.zsh vim设置 我个人还是很喜欢用vim的, 只要不是太复杂的环境, 我都尽量使用vim进行代码编辑. 来看看一些设置和配置吧. 当然, 你可以直接打造成IDEhomebrew进行安装: brew install vim 然后我先贴出全部的vimrc配置文件内容: " vundle 环境设置 filetype off set rtp+=~/.vim/bundle/Vundle.vim " 插件列表开始 call vundle#begin() Plugin 'VundleVim/Vundle.vim' Plugin 'altercation/vim-colors-solarized' Plugin 'tomasr/molokai' Plugin 'vim-scripts/phd' Plugin 'Lokaltog/vim-powerline' Plugin 'octol/vim-cpp-enhanced-highlight' Plugin 'nathanaelkane/vim-indent-guides' Plugin 'derekwyatt/vim-fswitch' Plugin 'kshenoy/vim-signature' Plugin 'vim-scripts/BOOKMARKS--Mark-and-Highlight-Full-Lines' Plugin 'majutsushi/tagbar' Plugin 'vim-scripts/indexer.tar.gz' Plugin 'vim-scripts/DfrankUtil' Plugin 'vim-scripts/vimprj' Plugin 'dyng/ctrlsf.vim' Plugin 'terryma/vim-multiple-cursors' Plugin 'scrooloose/nerdcommenter' Plugin 'vim-scripts/DrawIt' Plugin 'SirVer/ultisnips' Plugin 'derekwyatt/vim-protodef' Plugin 'scrooloose/nerdtree' Plugin 'fholgado/minibufexpl.vim' Plugin 'gcmt/wildfire.vim' Plugin 'sjl/gundo.vim' Plugin 'Lokaltog/vim-easymotion' Plugin 'suan/vim-instant-markdown' Plugin 'lilydjwg/fcitx.vim' " 插件列表结束 call vundle#end() filetype plugin indent on " 定义快捷键的前缀, 即<Leader> let mapleader=";" " 设置状态栏主题风格 let g:Powerline_colorscheme='solarized256' " 基于缩进或语法进行代码折叠 set foldmethod=syntax " 启动 vim 时关闭折叠代码 set nofoldenable " 打开语法高亮 syntax on " 使用配色方案 colorscheme gruvbox " 打开文件类型检测功能 filetype on " 不同文件类型采用不同缩进 filetype indent on " 允许使用插件 filetype plugin on filetype plugin indent on " 关闭vi模式 set nocp " 与mac共享剪贴板 set clipboard+=unnamed " 取消VI兼容 set nocompatible " 显示行号 set nu " 历史命令保存行数 set history=1000 " 当文件被外部改变时自动读取 set autoread " 取消自动备份及产生swp文件 set nobackup set nowb set noswapfile " 允许使用鼠标点击定位 set mouse=a " 允许区域选择 set selection=exclusive " 高亮光标所在行 set cursorline " 取消光标闪烁 set novisualbell " 总是显示状态行 set laststatus=2 " 状态栏显示当前执行的命令 set showcmd " 标尺功能, 显示当前光标所在行列号 set ruler " 设置命令行高度为2 set cmdheight=2 " 粘贴时保持格式 " set paste " 高亮显示匹配的括号 set showmatch " 在搜索的时候忽略大小写 set ignorecase " 高亮被搜索的句子 set hlsearch " 在搜索时, 输入的词句的逐字符高亮 set incsearch " 继承前一行的缩进方式 set autoindent " 为c程序提供自动缩进 set smartindent " 使用c样式的缩进 set cindent " 制表符为4 set tabstop=4 " 统一缩进为4 set softtabstop=4 set shiftwidth=4 " 允许使用退格键 set backspace=eol,start,indent set whichwrap+=<,>,h,l " 取消换行 set nowrap " 在被分割的窗口间显示空白 set fillchars=vert:\ ,stl:\ ,stlnc:\ " 光标移动到buffer的顶部和底部时保持3行距离 set scrolloff=3 " 设定默认解码 set fenc=utf-8 set fencs=utf-8,usc-bom,euc-jp,gb18030,gbk,gb2312,cp936 " 设定编码 set enc=utf-8 set fileencodings=ucs-bom,utf-8,chinese set langmenu=zh_CN.UTF-8 language message zh_CN.UTF-8 source $VIMRUNTIME/delmenu.vim source $VIMRUNTIME/menu.vim " 自动补全 filetype plugin indent on set completeopt=longest,menu " 自动补全命令时候使用菜单式匹配列表 set wildmenu autocmd FileType ruby,eruby set omnifunc=rubycomplete#Complete autocmd FileType python set omnifunc=pythoncomplete#Complete autocmd FileType javascript set omnifunc=javascriptcomplete#CompleteJS autocmd FileType html set omnifunc=htmlcomplete#CompleteTags autocmd FileType css set omnifunc=csscomplete#CompleteCSS autocmd FileType xml set omnifunc=xmlcomplete#CompleteTags autocmd FileType java set omnifunc=javacomplete#Complet 配色 其实之前iTerm已经配色过了的, 但是vim有自己的独立配色. 用法也很简单:用如下命令创建.vim/colors目录, 然后下载配色文件: mkdir -p ~/.vim/colors ; cd ~/.vim/colors curl -O https://raw.githubusercontent.com/nanotech/jellybeans.vim/master/colors/jellybeans.vim 打开~/.vimrc, 加入colorscheme jellybeans, 比方说, 我用了这个jellybeans.vim主题配色.这里再推荐一个gruvbox主题, 效果如下: 代码折叠 使用自带代码折叠 " 基于缩进或语法进行代码折叠 "set foldmethod=indent set foldmethod=syntax " 启动 vim 时关闭折叠代码 set nofoldenable 进入vim命令模式, za即可折叠当前块 zM关闭所有折叠 zR打开所有折叠 插件管理和使用 安装管理工具vundle: git clone https://github.com/VundleVim/Vundle.vim.git ~/.vim/bundle/Vundle.vim 打开.vimrc, 加入下列插件内容: " vundle 环境设置 filetype off set rtp+=~/.vim/bundle/Vundle.vim " vundle 管理的插件列表必须位于 vundle#begin() 和 vundle#end() 之间 call vundle#begin() Plugin 'VundleVim/Vundle.vim' Plugin 'altercation/vim-colors-solarized' Plugin 'tomasr/molokai' Plugin 'vim-scripts/phd' Plugin 'Lokaltog/vim-powerline' Plugin 'octol/vim-cpp-enhanced-highlight' Plugin 'nathanaelkane/vim-indent-guides' Plugin 'derekwyatt/vim-fswitch' Plugin 'kshenoy/vim-signature' Plugin 'vim-scripts/BOOKMARKS--Mark-and-Highlight-Full-Lines' Plugin 'majutsushi/tagbar' Plugin 'vim-scripts/indexer.tar.gz' Plugin 'vim-scripts/DfrankUtil' Plugin 'vim-scripts/vimprj' Plugin 'dyng/ctrlsf.vim' Plugin 'terryma/vim-multiple-cursors' Plugin 'scrooloose/nerdcommenter' Plugin 'vim-scripts/DrawIt' Plugin 'SirVer/ultisnips' Plugin 'derekwyatt/vim-protodef' Plugin 'scrooloose/nerdtree' Plugin 'fholgado/minibufexpl.vim' Plugin 'gcmt/wildfire.vim' Plugin 'sjl/gundo.vim' Plugin 'Lokaltog/vim-easymotion' Plugin 'suan/vim-instant-markdown' Plugin 'lilydjwg/fcitx.vim' " 插件列表结束 call vundle#end() filetype plugin indent on 进入vim, 命令模式下键入:PluginInstall, 即可自动安装: vim-powerline 利用插件Lokaltog/vim-powerline, 设置状态栏风格: " 设置状态栏主题风格 let g:Powerline_colorscheme='solarized256' 多行注释 多行注释肯定要的. 首先添加按钮, 你可以添加自己顺手的, 我这里是;: " 定义快捷键的前缀, 即<Leader> let mapleader=";" 然后cc注释选中区域, 你可以用v进入选中模式选择多行.cu取消注释 代码补全 vim同样自带了代码自动补全, 使用也很简单, ctrl + p即可, 然后继续ctrl + p是向上, ctrl + n是向下, 当然了, 有更加复杂的插件进行提示. 最终效果可以达到vs的等级, 但是我个人认为没必要. 如果以后有这方面的需求, 回来更新文章内容的. 那事实上, 还有其他按键, 你用ctrl + x, 即可显示全部提示, 看所示图的最下面一行, 可以ctrl + ], ctrl + D等等. python配置 修改pip源 首先改一下pip的源: mkdir .pip ; cd .pip vim pip.conf 替换阿里源: [global] index-url=http://mirrors.aliyun.com/pypi/simple/ [install] trusted-host=mirrors.aliyun.com 或者你喜欢的: http://pypi.douban.com/ http://pypi.hustunique.com/ http://pypi.sdutlinux.org/ http://pypi.mirrors.ustc.edu.cn/ virtualenv配置 我使用的是virtualenv, 如果你是其它环境也是可以的: pip3 install virtualenv 使用virtualenv --version看下是否安装成功. virtualenvwrapper使用 Virtaulenvwrapper是对virtualenv的封装, 可以更方便地管理虚拟环境: pip3 install virtualenvwrapper 建立一个环境目录, 比方说mkdir ~/pyEnv打开.zshrc文件, 输入如下内容: export WORKON_HOME=~/pyEnv source /usr/local/bin/virtualenvwrapper.sh source ~/.zshrc更新配置文件, 会有如下图内容: 之后就能欢乐地建立环境了: 建立环境: mkvirtualenv env1 删除环境: rmvirtualenv env1 切换环境到其它环境: workon env2 退出环境: deactivate 列出所有环境: lsvirtualenv -b 查看环境里的轮子: lssitepackages
目录 前言 filter2 实操 conv2 imfilter 最后 前言 最近开始准备深入学习一下计算机视觉(CV)方面的内容, 这里会更新几期基础知识, 主要是Matlab和Python方面的. 这次的就是三个Matlab的函数filter2, conv2, imfilter. filter2 filter2是相关滤波函数, 假设输入图像I大小为M1 X N1,相关核f大小为M2 X N2. J = filter2 (f, I, shape) f: 相关核, 即滤波掩模 I: 输入图像 J: 输出图像 shape: 可选, 其参数如下: 参数 描述 same(默认值) 返回与I同样尺寸滤波后的图像, M1 X N1 full 返回全部二维滤波结果, (M1 + M2 - 1) X (N1 + N2 - 1) valid 不考虑边界补零, 即只要有边界补出的零参与运算的都舍去, (M1 - M2 + 1) X (N1 - N2 + 1) 实操 这里实操一下, 首先是'same' I = [10 10 10 10; 10 10 10 10; 10 10 10 10]; f = [1 1 1; 1 1 1; 1 1 1]; J = filter2(f, I, 'same') 输出是: >> iFilter J = 40 60 60 40 60 90 90 60 40 60 60 40 对输入图像补零, 第一行之前和最后一行之后都补M2 -1行,第一列之前和最后一列之后都补N2 - 1列, (注意filter2和conv2不支持其他的边界补充选项, 函数内部对输入总是补零. 也就是补成如下: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10 10 10 10 0 0 0 0 10 10 10 10 0 0 0 0 10 10 10 10 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 所以第一个40就是相关核与红框内部分对应相乘然后结果求和所得, 也就是(0*1 + 0*1 + 0*1 + 0*1 + 10 *1 + 10 *1 + 0*1 + 10 *1 + 10 *1), 之后的就滑动相关核, 将相关核的中心位于图像矩阵的每一个元素. 'full'的话, 就是将边缘补全零的计算结果也一并输出, 所以尺寸是(M1 + M2 - 1) X (N1 + N2 - 1) J = 10 20 30 30 20 10 20 40 60 60 40 20 30 60 90 90 60 30 20 40 60 60 40 20 10 20 30 30 20 10 最后'valid', 就是边界补出的零参与运算的都舍去. J = 90 90 conv2 和filter2最大的不同就是计算之前, 把卷积核旋转180°. 所以, 如果卷积核旋转180°和原来一样, 那么conv2和filter2的计算结果都是一样的. 所以我这里就修改一下算子. 可以很明显看到filter2的结果旋转180°就是conv2的结果. I = [10 10 10 10; 10 10 10 10; 10 10 10 10]; f = [1 1 1; 1 1 1; 1 2 1]; J = filter2(f, I, 'same') J = conv2(I, f, 'same') J = 50 70 70 50 70 100 100 70 40 60 60 40 J = 40 60 60 40 70 100 100 70 50 70 70 50 imfilter J = imfilter(I, f, filtering_mode, boundary_options, size_options) 参数列表 选项 描述 filtering_mode ‘corr’ 相关(默认) ‘conv’ 卷积 boundary_options X 输入图像的边界通过用值X值来填充扩展其默认值为0 ‘replicate’ 复制外边界值 ‘symmetric’ 镜像反射 ‘circular’ 图像大小通过将图像看成是一个二维周期函数的一个周期来扩展 size_options ‘full’ 输出图像的大小与被扩展图像的大小相同 ‘same’ 输出图像的大小与输入原始输入图像一样(默认) 这样的话, J = imfilter(I, f, 'corr', 0, 'same')与J = filter2(f, I, 'same')是等效的. 最后 喜欢记得点赞或者关注哦, 有意见或者建议评论区见~
目录 前言 Intel四级页表 实操寻址 获取cr3 获取PGD 获取PUD 获取PMD 获取PTE 获取内容 最后 前言 Linux四级页表的作用主要就是地址映射, 将逻辑地址映射到物理地址. 很多时候, 有些地方想不明白就可以查看实际物理地址进行分析. Intel 四级页表 其实很多设计的根源或者说原因都来自于CPU的设计, OS很多时候都是辅助CPU. Linux的四级页表就是依据CPU的四级页表来设计的. 这里主要说的就是Intel x64页面大小为4KB的情况, 如图所示: 当然, 你可以用指令确认下: getconf PAGE_SIZE 实操寻址 首先这里先贴出几个工具, fileview, dram, registers. 这些都是可以帮助快速获取地址的, 上一篇文章说的kgdb工具也是可以的, 就是麻烦一点, 你懂的. 具体内容就不贴了, 这里仅展示用户态的代码: #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <sys/stat.h> #include <sys/types.h> #define BUFSIZE 4096 int main() { int fd, ret; char buf[BUFSIZE]; unsigned long int a = 0x1234567890abcdef; printf( "a=0x%016lX addr: %p \n", a, &a ); if ( (fd = open( "/proc/registers", O_RDONLY ) ) < 0 ) { fprintf( stderr, "Open /proc/registers file failed! \n" ); exit( EXIT_FAILURE ); } lseek( fd, 0L, SEEK_SET ); if ( (ret = read( fd, buf, sizeof buf - 1 ) ) < 0 ) { perror( "/proc/registers" ); exit( EXIT_FAILURE ); } buf[ret] = 0; close( fd ); puts( buf ); while ( 1 ); return(0); } 使用make指令编译工具, 插入dram.ko和registers.ko驱动模块. 编译运行用户态程序, 如图所示: 获取cr3 这之中最关键的是cr3地址以及局部变量地址, 这里看到, 变量地址是0x7ffdcbffaba8, 变量值是0x1234567890ABCDEF. cr3寄存器中地址是0x40c78000. 当然了, 按照CPU的图示, cr3肯定是指向PML4E. 在Linux当中, 第一级页表称为PGD, 当然是有历史原因的, 可以自行google. 所以Linux的四级页表分别是PGD -> PUD -> PMD -> PTE. 获取PGD 想要获取PGD中的内容需要通过计算. 这里先来处理一下局部变量地址. 首先写成二进制. 0x7ffdcbffaba8 0111 1111 1111 1101 1100 1011 1111 1111 1010 1011 1010 1000 然后按照Intel的设计, 重新整合. 011111111 111110111 001011111 111111010 101110101000 重新写成16进制: ff 1f7 5f 1fa ba8 这就是只要用的offset. 因为每个单元是64-bits因此需要在序号基础上乘以8获得地址. 所以PGD地址为: 0x40c78000(cr3) + ff * 8 = 0x40c787f8 然后使用启动之前编译的小工具: ./fileview /dev/dram 输入之前计算出来的地址0x40c787f8, 就可以得到之中的内容, 也就是PUD, 从CPU图来说就是PDPTE: 获取PUD 这里获取到的是67 50 75 76 00 00 00 80, 但是注意, Intel是和显示顺序反过来的. 也就是76755067, 然后后面的12-bits是页面属性. 所以, 具体地址就是: 76755000 + 1f7 * 8 = 76755fb8 同样输入地址到工具, 得到67 80 E8 2C 00 00 00 00. 获取PMD 直接计算了: 2ce88000 + 5f * 8 = 2ce882f8 得到67 00 DD 48 00 00 00 00. 获取PTE 直接计算了: 48dd0000 + 1fa * 8 = 48dd0fd0 得到67 58 59 20 00 00 00 80. 获取内容 最后就可以获取到内容了: 20595000 + ba8 = 20595ba8 最后 当然了, 这次是在用户态下进行从线性地址到物理地址转换的, 如果是内核态有些地方会发生变化. 暂时写到这里, 内核态等后续的更新了. 喜欢记得点赞, 有意见或者建议评论区见~
内核必须懂(一): 用系统调用打印Hello, world!内核必须懂(二): 文件系统初探内核必须懂(三): 重编Ubuntu18.04LTS内核4.15.0内核必须懂(四): 撰写内核驱动 目录 前言 用户态代码 驱动模块代码 per-CPU变量 关闭抢占 演示 最后 前言 之前内核必须懂(四): 撰写内核驱动说到了基础的驱动模块写法. 这次目标就是计算进入驱动ioctl或者其他某个驱动函数的次数. 当然, 你可能会觉得, 这弄个全局变量计数不就完了吗? 但是这里的要求是要并行进行访问, 所以统计的是多核多线程的访问次数. 是不是感觉没有那么简单了? 你可能会回答, 上锁, 那基本等于串行, 太low, 这里展示真正的并行访问与计数. 用户态代码 先来看下用户态代码: #include <iostream> #include <sys/ioctl.h> #include <sys/stat.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <string> using namespace std; #define NUM_THREADS 20 // 文件描述符 int fd = 0; // 多线程调用函数 void *pthread_fx( void *args ) { ioctl( fd, 1, 0 ); } int main() { int ret = 0; // 打开文件 if((fd = open("/dev/hellodr", O_RDWR)) < 0) { cerr << strerror(errno) << endl; return -1; } // 开启多线程 pthread_t tids[NUM_THREADS]; for ( int i = 0; i < NUM_THREADS; ++i ) { ret = pthread_create( &tids[i], NULL, pthread_fx, NULL ); if ( ret != 0 ) { printf( "pthread_create error: error_code = %d\n", ret ); } } // 回收线程资源 for ( int i = 0; i < NUM_THREADS; ++i ) { pthread_join(tids[i], NULL); } // 关闭文件 close( fd ); return 0; } 这里就是开20个线程调用驱动的ioctl函数. 没什么要说的. 驱动代码 是依据上一次的内容内核必须懂(四): 撰写内核驱动扩展的, 变化基本在DriverClose和DriverIOControl两个函数中. #include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/init.h> #include <linux/delay.h> #define MAJOR_NUM 231 #define DEVICE_NAME "hellodr" DEFINE_PER_CPU( long, gUsage ) = 0; int DriverOpen( struct inode *pslINode, struct file *pslFileStruct ) { printk( KERN_ALERT DEVICE_NAME " hello open.\n" ); return(0); } ssize_t DriverWrite( struct file *pslFileStruct, const char __user *pBuffer, size_t nCount, loff_t *pOffset ) { printk( KERN_ALERT DEVICE_NAME " hello write.\n" ); return(0); } int DriverClose( struct inode *pslINode, struct file *pslFileStruct ) { int i = 0; unsigned long ulBaseAddr = 0; unsigned long ulOffset = 0; long *pUsage = NULL; long usageSum = 0; ulOffset = (unsigned long) (&gUsage); for_each_online_cpu( i ) { ulBaseAddr = __per_cpu_offset[i]; pUsage = (long *) (ulBaseAddr + ulOffset); usageSum += (*pUsage); printk( KERN_ALERT DEVICE_NAME " pUsage = %lx, *pUsage = %ld\n", (unsigned long) pUsage, *pUsage ); } printk( KERN_ALERT DEVICE_NAME " %ld\n", usageSum ); return(0); } long DriverIOControl( struct file *pslFileStruct, unsigned int uiCmd, unsigned long ulArg ) { long *pUsage = NULL; /* printk( KERN_ALERT DEVICE_NAME ": pUsage = 0x%lx %lx %ld", (unsigned long) pUsage, (unsigned long) (&gUsage), (*pUsage) ); */ preempt_disable(); pUsage = this_cpu_ptr( (long *) (&gUsage) ); (*pUsage)++; preempt_enable(); return(0); } struct file_operations hello_flops = { .owner = THIS_MODULE, .open = DriverOpen, .write = DriverWrite, .release = DriverClose, .unlocked_ioctl = DriverIOControl }; static int __init hello_init( void ) { int ret; ret = register_chrdev( MAJOR_NUM, DEVICE_NAME, &hello_flops ); if ( ret < 0 ) { printk( KERN_ALERT DEVICE_NAME " can't register major number.\n" ); return(ret); } printk( KERN_ALERT DEVICE_NAME " initialized.\n" ); return(0); } static void __exit hello_exit( void ) { printk( KERN_ALERT DEVICE_NAME " removed.\n" ); unregister_chrdev( MAJOR_NUM, DEVICE_NAME ); } module_init( hello_init ); module_exit( hello_exit ); MODULE_LICENSE( "GPL" ); MODULE_AUTHOR( "Sean Depp" ); per-CPU变量 但是在说代码之前, 要说一下per-CPU变量 变量, 也就是DEFINE_PER_CPU( long, gUsage ) = 0;这一行. 可以先看看这篇文章和这篇文章, 简单来说就是在单cpu环境下生效的变量. 那你可能会说, 我就一个核啊, 岂不是没用了, 但是这是相对于虚拟cpu来说的, 也就是说, 如果你是4核8线程, 就可以同时有8个这样的per-CPU变量生效. 正因为有这样的变量, 计数变得非常简单, 只需要统计每个cpu中的这个变量即可. 还有个问题需要解决, 那就是获取每个cpu的基地址, 这里又有一个宏for_each_online_cpu(), 只需要给个变量即可循环输出活跃的cpu了. 宏真是好东西哦~ 关闭抢占 多核情况下, 就还有一个问题, 抢占. 当然了, 这里的代码简单, 不关闭抢占大多数情况下也不会出错, 但是情况复杂的话, 出错概率就会大大提高, 甚至你不知道怎么错的. 而且这可是内核, 错了往往十分致命. preempt_disable(); pUsage = this_cpu_ptr( (long *) (&gUsage) ); (*pUsage)++; preempt_enable(); 这里还有一个宏this_cpu_ptr, 获取per cpu变量的线性地址. 这是操作系统的知识了, 我就不多说了, 自行google咯. 那我来说一下, 为什么要关闭抢占.试想一下, 获取到地址之后, 正打算++操作, 结果中断抢占, 到了另一个核, 之前的地址就对不上了, 这时候进行++操作就完全不对了. 所以为了不发生这样的问题, 就需要关闭抢占, 当然, 关闭中断也行, 但是中断对操作系统影响太大了, 不推荐. 演示 说了这么多, 万一演示不出来, 就没有任何意义, 所以跑下程序. 编译生成.ko, .out这些不多说了, mknod上篇文章内核必须懂(四): 撰写内核驱动也说了. 这里补充一下, 看到了警告, 这是缺了个库, 最直接的解决方案就是, 安装这个库之后, 重编译内核. 否则其他方案都过于麻烦. 这里看到有8个cpu, 因为这是4核8线程的cpu, 然后一共跑了20次, 每个核跑的次数也可以看到. 所以, 实验成功. 最后 喜欢记得点赞, 有意见或者建议评论区见哦~
目录 前言 gnome-tweak-tool 主题, 图标, 鼠标, 文字, 终端 文字 终端 最后 前言 之前我也写过一篇关于Ubuntu 16.04 LTS美化的. 其实大部分内容依旧实用, 不过由于Ubuntu的界面由unity变成了gnome, 所以有些变化. gnome-tweak-tool 同样, 先要安装美化管理工具gnome-tweak-tool. 界面如下展示: sudo apt-get install gnome-tweak-tool 然后安装一些扩展. 扩展当中, 有一个特别值得推荐的, 就是dash to dock, 所以这里也顺便装了. sudo apt-get install gnome-shell-extensions sudo apt-get install gnome-shell-extension-dashtodock 然后我也是mac用惯了, 喜欢把放大缩小关闭按钮放置在左侧. 主题, 图标, 鼠标, 文字, 终端 主题, 图标, 字体包括终端等等, 这些用之前文章里的依旧可行, 虽然有些图标主题已经崩了. 那我再提供一个网站, 大家按照自己的喜欢来. 那这里就演示一下如何将下载的主题用起来, 首先选一个模仿水果mac暗黑主题的, 下载.之后解压, 将文件夹拷贝到/usr/share/themes/即可, 这样gnome-tweak-tool选择主题的时候就能看到了. 那图标也是同理了. 值得一说的就是, 鼠标也是放在icons目录下. 这里有个好看的黑色鼠标. 文字 还是推荐文泉驿微米黑, 当然了, 最近开源字体其实蛮多的, 看个人喜好了. sudo apt-get install fonts-wqy-microhei 终端 终端我用的是oh-my-zsh, 很多在舒适美观的mac终端, iTerm+zsh+powerlevel9k+vim+virtualenv中都说过了, 差别就在, mac下面有homebrew, 安装方便, linux就要稍微多几步了. git, vim和curl肯定是要的. sudo apt-get install git vim curl 然后装上zsh和oh-my-zsh, 切换shell到zsh. sudo apt-get install zsh sudo wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | sh cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc chsh -s /bin/zsh 然后装一些oh-my-zsh插件, 这里是自动补全和语法高亮, 别的也是同理. cd ~/.oh-my-zsh/custom/plugins git clone git://github.com/zsh-users/zsh-syntax-highlighting.git git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions 然后vim打开~/.zshrc文件, 添加插件名称到其中. plugins=( git zsh-syntax-highlighting zsh-autosuggestions ) 最后 最后大体如下图所示啦, 没有很花哨, 简约为主. 喜欢记得点个赞或者关注我哦~
目录 前言 开发环境一览 显卡驱动安装 下载驱动 禁用nouveau 安装驱动 安装CUDA10.0 第一个CUDA程序 安装cudnn7.5 安装TensorFlow1.13 最后 前言 之前写过cuda环境的搭建文章, 这次干脆补全整个深度学习环境的搭建. 开发环境一览 CPU: Intel core i7 4700MQ GPU: NVIDIA GT 750M OS: UBUNTU 18.04.1LTS 64位 用指令看下英伟达显卡: lspci | grep -I nvidia 当你搭建完成环境之后, 可以用代码查看硬件信息, 自己写或者官方的例子, 我的NVIDIA GT 750M信息显示如下图, 当然可以直接到英伟达官网查看显卡信息. 这张信息表目前看来就是些参数, 但是后续的并行算法很多时候是依据这些参数来设计的: 显卡驱动安装 千万不要用UBUNTU附加驱动里提供的显卡驱动!!!千万不要用UBUNTU附加驱动里提供的显卡驱动!!!千万不要用UBUNTU附加驱动里提供的显卡驱动!!!一般来说, 你会遇到一些奇怪的问题, 当然, 锦鲤是不会出问题的(手动滑稽).这是第一个坑点, 大体有三种展现方式: 装完重启进不去系统, 卡住ubuntu加载页面; 无限登录; 装好了, 进入了系统, 然后输入nvidia-smi指令没有任何反应. 正常情况会弹出一张表, 如下所示: 下载驱动 行了, 来说说我的实操:首先到官网下载显卡驱动, 比方说我是GT 750M, 操作系统是64位Linux, 我就找对应的版本进行下载. 删掉以往的驱动. 注意, 就算你啥都没装, 这步也是无害的. sudo apt-get remove --purge nvidia* 更新并安装一些需要的库, 先装这么多, 之后装CUDA还有一波. sudo apt-get update sudo apt-get install dkms build-essential linux-headers-generic 禁用nouveau 打开blacklist.conf, 在最后加入禁用nouveau的设置, 这是一个开源驱动, 如图所示: sudo vim /etc/modprobe.d/blacklist.conf blacklist nouveau blacklist lbm-nouveau options nouveau modeset=0 alias nouveau off alias lbm-nouveau off 禁用nouveau内核模块 echo options nouveau modeset=0 sudo update-initramfs -u 重启. 如果运行如下指令没用打印出任何内容, 恭喜你, 禁用nouveau成功了. lsmod | grep nouveau 安装驱动 来到tty1(快捷键ctrl + alt + f1,如果没反应就f1-f7一个个试, 不同Linux, 按键会略有不同). 运行如下指令关闭图形界面.我在ubuntu18.04.1LTS是ctrl + alt + f3-f6. 然后注意, 以下指令适用于16.04及以前. sudo service lightdm stop 这不适用于18.04. 18.04可以如下操作: 关闭用户图形界面 sudo systemctl set-default multi-user.target sudo reboot 开启用户图形界面 sudo systemctl set-default graphical.target sudo reboot 安装驱动, 注意有坑, 一定要加-no-opengl-files, 不加这个就算安装成功, 也会出现无限登录问题. 但是在最近几次安装环境的时候, 例如系统是18.04, 驱动是418.43, 这个参数变得无效. 所以如果不能开启安装页面, 可以去掉此参数. sudo chmod u+x NVIDIA-Linux-x86_64-390.87.run sudo ./NVIDIA-Linux-x86_64-390.87.run –no-opengl-files 如果你已经装了, 但是没有加-no-opengl-files, 按照如下操作可以救一下. 或者你安装失败了, 有些库缺少了之类的, 可以用以下命令卸载干净重来. sudo ./NVIDIA-Linux-x86_64-390.87.run –uninstall 顺带一提, 可能会弹出Unable to find a suitable destination to install 32-bit compatibility libraries on Ubuntu 18.04 Bionic Beaver Linux的bug, 然后你需要下面三条指令: sudo dpkg --add-architecture i386 sudo apt update sudo apt install libc6:i386 并且中途的选项都选no比较好, 指不定卡死在安装哪个奇怪的东西上. 重启. 用nvidia-smi指令试一下, 如果看到类似下图, 恭喜你, 驱动安装成功. 或者看到附加驱动显示继续使用手动安装的驱动. 安装之后在软件和更新当中会显示如下图: 安装CUDA10.0 先来补库. sudo apt-get install freeglut3-dev libx11-dev libxmu-dev libxi-dev libgl1-mesa-glx libglu1-mesa libglu1-mesa-dev 到官网下载要的CUDA版本, 我这里是10.0, 下载runfile(local)版本, 如下图所示: md5检测一下, 不合格要重新下载. 下图是我的检测结果: md5sum cuda_10.0.130_410.48_linux.run 再次关闭图形界面 sudo service lightdm stop 这不适用于18.04. 18.04可以如下操作: 关闭用户图形界面 sudo systemctl set-default multi-user.target sudo reboot 开启用户图形界面 sudo systemctl set-default graphical.target sudo reboot 安装时候依旧要加-no-opengl-files参数, 之后一路默认就好. 最好不要安装与OpenGL相关的. sudo sh cuda_10.0.130_410.48_linux.run –no-opengl-files 然后会看到三个installed. 添加环境变量 vim ~/.bashrc 最后写入: export CUDA_HOME=/usr/local/cuda export PATH=$PATH:$CUDA_HOME/bin export LD_LIBRARY_PATH=/usr/local/cuda-10.0/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} 保存退出, 并其生效. source ~/.bashrc 运行一些检测命令, 如果和我显示的类似, 恭喜你, 环境配置完成. cat /proc/driver/nvidia/version nvcc -V 可以跑一下英伟达提供的学习案例: 第一个CUDA程序 之前在开发环境部分展示过一个小栗子, 来看看具体代码吧~ vim Device.cu #include <stdio.h> int main() { int nDevices; cudaGetDeviceCount(&nDevices); for (int i = 0; i < nDevices; i++) { cudaDeviceProp prop; cudaGetDeviceProperties(&prop, i); printf("Device Num: %d\n", i); printf("Device name: %s\n", prop.name); printf("Device SM Num: %d\n", prop.multiProcessorCount); printf("Share Mem Per Block: %.2fKB\n", prop.sharedMemPerBlock / 1024.0); printf("Max Thread Per Block: %d\n", prop.maxThreadsPerBlock); printf("Memory Clock Rate (KHz): %d\n", prop.memoryClockRate); printf("Memory Bus Width (bits): %d\n", prop.memoryBusWidth); printf("Peak Memory Bandwidth (GB/s): %.2f\n\n", 2.0 * prop.memoryClockRate * (prop.memoryBusWidth / 8) / 1.0e6); } return 0; } nvcc Device.cu -o Device.o ./Device.o 安装cudnn7.5 首先到官网去下载勾选的4个: 然后解压tgz包, 复制文件到cuda环境, 接着安装deb包. tar -zxvf cudnn-10.0-linux-x64-v7.5.0.56.tgz sudo cp cuda/include/cudnn.h /usr/local/cuda/include sudo cp cuda/lib64/libcudnn* /usr/local/cuda/lib64 sudo chmod a+r /usr/local/cuda/include/cudnn.h /usr/local/cuda/lib64/libcudnn* sudo dpkg -i libcudnn7_7.5.0.56-1+cuda10.0_amd64.deb sudo dpkg -i libcudnn7-dev_7.5.0.56-1+cuda10.0_amd64.deb sudo dpkg -i libcudnn7-doc_7.5.0.56-1+cuda10.0_amd64.deb 这样就完成安装了, 用个小栗子来测试下吧, 结果如图示: cp -r /usr/src/cudnn_samples_v7/ ~ cd ~/cudnn_samples_v7/mnistCUDNN make clean && make ./mnistCUDNN 安装TensorFlow1.13 很遗憾我的GPU算力只有3.0, 最低要求是3.5, 不过这里还是安装一下tf, 当做练手了. sudo apt-get install python-pip python3-pip python-dev sudo pip3 install tensorflow-gpu 可以用如下python代码查询版本号和路径: #!/usr/bin/python3 import tensorflow as tf print (tf.__version__) print (tf.__path__) 最后我给出一个测试例子, 但是很遗憾, 我是无法运行的. #!/usr/bin/python3 import tensorflow as tf # Just disables the warning, doesn't enable AVX/FMA import os os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' hello = tf.constant("Hello, tf!") sess = tf.Session() printf (sess.run(hello)) 最后 喜欢记得点赞哦, 有意见或者建议评论区见~
目录 前言 CPU矩阵转置 GPU实现 简单移植 单block tile 利用率计算 shared memory 最后 前言 之前在第三章对比过CPU和GPU, 差距非常大. 这一次来看看GPU自身的优化, 主要是shared memory的用法. CPU矩阵转置 矩阵转置不是什么复杂的事情. 用CPU实现是很简单的: #include <stdio.h> #include <stdlib.h> #include <sys/time.h> #define LOG_ #define N 1024 /* 转置 */ void transposeCPU( float in[], float out[] ) { for ( int j = 0; j < N; j++ ) { for ( int i = 0; i < N; i++ ) { out[j * N + i] = in[i * N + j]; } } } /* 打印矩阵 */ void logM( float m[] ) { for ( int i = 0; i < N; i++ ) { for ( int j = 0; j < N; j++ ) { printf( "%.1f ", m[i * N + j] ); } printf( "\n" ); } } int main() { int size = N * N * sizeof(float); float *in = (float *) malloc( size ); float *out = (float *) malloc( size ); /* 矩阵赋值 */ for ( int i = 0; i < N; ++i ) { for ( int j = 0; j < N; ++j ) { in[i * N + j] = i * N + j; } } struct timeval start, end; double timeuse; int sum = 0; gettimeofday( &start, NULL ); transposeCPU( in, out ); gettimeofday( &end, NULL ); timeuse = end.tv_sec - start.tv_sec + (end.tv_usec - start.tv_usec) / 1000000.0; printf( "Use Time: %fs\n", timeuse ); #ifdef LOG logM( in ); printf( "\n" ); logM( out ); #endif free( in ); free( out ); return(0); } GPU实现 简单移植 如果什么都不考虑, 只是把代码移植到GPU: #include <stdio.h> #include <stdlib.h> #include <sys/time.h> #define N 1024 #define LOG_ /* 转置 */ __global__ void transposeSerial( float in[], float out[] ) { for ( int j = 0; j < N; j++ ) for ( int i = 0; i < N; i++ ) out[j * N + i] = in[i * N + j]; } /* 打印矩阵 */ void logM( float m[] ){...} int main() { int size = N * N * sizeof(float); float *in, *out; cudaMallocManaged( &in, size ); cudaMallocManaged( &out, size ); for ( int i = 0; i < N; ++i ) for ( int j = 0; j < N; ++j ) in[i * N + j] = i * N + j; struct timeval start, end; double timeuse; gettimeofday( &start, NULL ); transposeSerial << < 1, 1 >> > (in, out); cudaDeviceSynchronize(); gettimeofday( &end, NULL ); timeuse = end.tv_sec - start.tv_sec + (end.tv_usec - start.tv_usec) / 1000000.0; printf( "Use Time: %fs\n", timeuse ); #ifdef LOG logM( in ); printf( "\n" ); logM( out ); #endif cudaFree( in ); cudaFree( out ); } 不用想, 这里肯定是还不如单线程的CPU的, 真的是完完全全的资源浪费. 实测下来, 耗时是CPU的20多倍, 大写的丢人. 单block 单block最多可以开1024线程, 这里就开1024线程跑下. /* 转置 */ __global__ void transposeParallelPerRow( float in[], float out[] ) { int i = threadIdx.x; for ( int j = 0; j < N; j++ ) out[j * N + i] = in[i * N + j]; } int main() { ... transposeParallelPerRow << < 1, N >> > (in, out); ... } 效率一下就提升了, 耗时大幅下降. tile 但是的话, 如果可以利用多个block, 把矩阵切成更多的tile, 效率还会进一步提升. /* 转置 */ __global__ void transposeParallelPerElement( float in[], float out[] ) { int i = blockIdx.x * K + threadIdx.x; /* column */ int j = blockIdx.y * K + threadIdx.y; /* row */ out[j * N + i] = in[i * N + j]; } int main() { ... dim3 blocks( N / K, N / K ); dim3 threads( K, K ); ... transposeParallelPerElement << < blocks, threads >> > (in, out); ... } 这些都是GPU的常规操作, 但其实利用率依旧是有限的. 利用率计算 利用率是可以粗略计算的, 比方说, 这里的Memory Clock rate和Memory Bus Width是900Mhz和128-bit, 所以峰值就是14.4GB/s. 之前的最短耗时是0.001681s. 数据量是1024*1024*4(Byte)*2(读写). 所以是4.65GB/s. 利用率就是32%. 如果40%算及格, 这个利用率还是不及格的. shared memory 那该如何提升呢? 问题在于读数据的时候是连着读的, 一个warp读32个数据, 可以同步操作, 但是写的时候就是散开来写的, 有一个很大的步长. 这就导致了效率下降. 所以需要借助shared memory, 由他转置数据, 这样, 写入的时候也是连续高效的了. /* 转置 */ __global__ void transposeParallelPerElementTiled( float in[], float out[] ) { int in_corner_i = blockIdx.x * K, in_corner_j = blockIdx.y * K; int out_corner_i = blockIdx.y * K, out_corner_j = blockIdx.x * K; int x = threadIdx.x, y = threadIdx.y; __shared__ float tile[K][K]; tile[y][x] = in[(in_corner_i + x) + (in_corner_j + y) * N]; __syncthreads(); out[(out_corner_i + x) + (out_corner_j + y) * N] = tile[x][y]; } int main() { ... dim3 blocks( N / K, N / K ); dim3 threads( K, K ); struct timeval start, end; double timeuse; gettimeofday( &start, NULL ); transposeParallelPerElementTiled << < blocks, threads >> > (in, out); ... } 这样利用率就来到了44%, 及格了. 所以这就是依据架构来设计算法, 回顾一下架构图: 最后 但是44%也就是达到了及格线, 也就是说, 还有更深层次的优化工作需要做. 这些内容也就放在后续文章中了, 有意见或者建议评论区见~
目录 前言 cuda-gdb 未优化并行规约 优化后并行规约 结果分析 最后 前言 之前第三篇也看到了, 并行方面GPU真的是无往不利, 现在再看下第二个例子, 并行规约. 通过这次的例子会发现, 需要了解GPU架构, 然后写出与之对应的算法的, 两者结合才能得到令人惊叹的结果. 这次也会简要介绍下cuda-gdb的用法, 其实和gdb用法几乎一样, 也就是多了个cuda命令. cuda-gdb 如果之前没有用过gdb, 可以速学一下, 就几个指令.想要用cuda-gdb对程序进行调试, 首先你要确保你的gpu没有在运行操作系统界面, 比方说, 我用的是ubuntu, 我就需要用sudo service lightdm stop关闭图形界面, 进入tty1这种字符界面. 当然用ssh远程访问也是可以的.接下来, 使用第二篇中矩阵加法的例子. 但是注意, 编译的使用需要改变一下, 加入-g -G参数, 其实和gdb是相似的. nvcc -g -G CUDAAdd.cu -o CUDAAdd.o 然后使用cuda-gdb CUDAAdd.o即可对程序进行调试. 在调试之前, 我把代码贴出来: #include <stdio.h> __global__ void add(float * x, float *y, float * z, int n){ int index = threadIdx.x + blockIdx.x * blockDim.x; int stride = blockDim.x * gridDim.x; for (int i = index; i < n; i += stride){ z[i] = x[i] + y[i]; } } int main() { int N = 1 << 20; int nBytes = N * sizeof(float); float *x, *y, *z; cudaMallocManaged((void**)&x, nBytes); cudaMallocManaged((void**)&y, nBytes); cudaMallocManaged((void**)&z, nBytes); for (int i = 0; i < N; ++i) { x[i] = 10.0; y[i] = 20.0; } dim3 blockSize(256); // 4096 dim3 gridSize((N + blockSize.x - 1) / blockSize.x); add << < gridSize, blockSize >> >(x, y, z, N); cudaDeviceSynchronize(); float maxError = 0.0; for (int i = 0; i < N; i++){ maxError = fmax(maxError, (float)(fabs(z[i] - 30.0))); } printf ("max default: %.4f\n", maxError); cudaFree(x); cudaFree(y); cudaFree(z); return 0; } 之后就是常规操作了, 添加断点, 运行, 下一步, 查看想看的数据. 不同点是cuda的指令, 例如cuda block(1,0,0)可以从一开始block(0,0,0)切换到block(1,0,0). 未优化并行规约 如果按照常规的思路, 两两进行进行加法运算. 每次步长翻倍即可, 从算法的角度来说, 这是没啥问题的. 但是没有依照GPU架构进行设计. #include <stdio.h> const int threadsPerBlock = 512; const int N = 2048; const int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock; /* 4 */ __global__ void ReductionSum( float * d_a, float * d_partial_sum ) { /* 申请共享内存, 存在于每个block中 */ __shared__ float partialSum[threadsPerBlock]; /* 确定索引 */ int i = threadIdx.x + blockIdx.x * blockDim.x; int tid = threadIdx.x; /* 传global memory数据到shared memory */ partialSum[tid] = d_a[i]; /* 传输同步 */ __syncthreads(); /* 在共享存储器中进行规约 */ for ( int stride = 1; stride < blockDim.x; stride *= 2 ) { if ( tid % (2 * stride) == 0 ) partialSum[tid] += partialSum[tid + stride]; __syncthreads(); } /* 将当前block的计算结果写回输出数组 */ if ( tid == 0 ) d_partial_sum[blockIdx.x] = partialSum[0]; } int main() { int size = sizeof(float); /* 分配显存空间 */ float * d_a; float * d_partial_sum; cudaMallocManaged( (void * *) &d_a, N * size ); cudaMallocManaged( (void * *) &d_partial_sum, blocksPerGrid * size ); for ( int i = 0; i < N; ++i ) d_a[i] = i; /* 调用内核函数 */ ReductionSum << < blocksPerGrid, threadsPerBlock >> > (d_a, d_partial_sum); cudaDeviceSynchronize(); /* 将部分和求和 */ int sum = 0; for ( int i = 0; i < blocksPerGrid; ++i ) sum += d_partial_sum[i]; printf( "sum = %d\n", sum ); /* 释放显存空间 */ cudaFree( d_a ); cudaFree( d_partial_sum ); return(0); } 优化后并行规约 其实需要改动的地方非常小, 改变步长即可. __global__ void ReductionSum( float * d_a, float * d_partial_sum ) { // 相同, 略去 /* 在共享存储器中进行规约 */ for ( int stride = blockDim.x / 2; stride > 0; stride /= 2 ) { if ( tid < stride ) partialSum[tid] += partialSum[tid + stride]; __syncthreads(); } // 相同, 略去 } 结果分析 之前的文章里面也说过warp.warp: GPU执行程序时的调度单位, 目前cuda的warp的大小为32, 同在一个warp的线程, 以不同数据资源执行相同的指令, 这就是所谓SIMT.说人话就是, 这32个线程必须要干相同的事情, 如果有线程动作不一致, 就需要等待一波线程完成自己的工作, 然后再去做另外一件事情.所以, 用图说话就是, 第二种方案可以更快将warp闲置, 交给GPU调度, 所以, 肯定是第二种更快. 图一在运算依次之后, 没有warp可以空闲, 而图二直接空闲2个warp. 图一到了第二次可以空闲2个warp, 而图二已经空闲3个warp. 我这副图只是示意图, 如果是实际的, 差距会更大. 所以来看下运行耗时, 会发现差距还是很大的, 几乎是差了一倍. 不过GPU确实算力太猛, 这样看还不太明显, 有意放大数据量会更加明显. 最后 所以GPU又一次展示了强大的算力, 而且, 这次也看到了只是小小变动, 让算法更贴合架构, 就让运算耗时减半, 所以在优化方面可以做的工作真的是太多了, 之后还有更多优化相关的文章, 有意见或者建议, 评论区见哦~
目录 前言 CMake 用CMake向已有AS项目添加C/C++代码 ndk-build 最后 前言 mac上安装软件真的很简单, 一路下一步就可以安装好android studio. 这里有一篇旧文-Mac下安装配置Android Studio 2.x和3.x并配置使用adb可供参考.而写这篇的目的, 主要是我发现之前的ndk开发方式已经过时了, 需要更新一下新的流程. CMake CMake的方式是官方默认的ndk构建方式, 先从默认栗子开始看吧. 新建一个项目, 勾选C++ support: 你会发现初始的Activity就只能是基础或者空的类型了, 其他的都没了. 这里默认C++标准即可: C++ Standard: 选择哪一种C++标准, 默认选择Toolchain Default选项, 其会使用默认的CMake配置 Exceptions Support: 是否启用对C++异常处理的支持, 如果选中, AS会将-fexceptions标志添加到模块级build.grade文件的cppFlags中 Runtime Type Information Support: 是否支持RTTI, 如果选中, AS会将-frtti标志添加到模块级build.gradle文件的cppFlags中 来看看项目都多了什么, 先切换到Android标签下, 多了cpp目录(ps: 注意, 这里就算切换到Project标签, 依旧是cpp哈), 一些头文件, 和native-lib.cpp, 不用说, 这个cpp里面肯定是jni代码了, 我贴出来: #include <jni.h> #include <string> extern "C" JNIEXPORT jstring JNICALL Java_com_so_testcmake_MainActivity_stringFromJNI( JNIEnv *env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); } 然后切换到Project标签, 这个CMakeLists.txt就特别惹眼了, 我把里面大段注释都去掉, 然后贴出代码. .externalNativeBuild文件夹: 用于存放cmake编译好的文件, 包括支持的各种硬件等信息. 其实看到前面的.也知道是系统管理的了. cmake_minimum_required(VERSION 3.4.1) add_library( # Sets the name of the library. native-lib # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). src/main/cpp/native-lib.cpp) find_library( # Sets the name of the path variable. log-lib # Specifies the name of the NDK library that # you want CMake to locate. log) target_link_libraries( # Specifies the target library. native-lib # Links the target library to the log library # included in the NDK. ${log-lib}) 很明显, 关键在于add_library这一段 第一个参数生成函数库的名称, 即libnative-lib.so或libnative-lib.a(lib和.so/.a默认缺省) 第二个参数生成库类型: 动态库为SHARED, 静态库为STATIC 第三个参数依赖的c/cpp文件(相对路径) 最后回到Activity类来看看, 操作还是一样的, 加载库, 声明native函数. public class MainActivity extends AppCompatActivity { // Used to load the 'native-lib' library on application startup. static { System.loadLibrary("native-lib"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Example of a call to a native method TextView tv = (TextView) findViewById(R.id.sample_text); tv.setText(stringFromJNI()); } /** * A native method that is implemented by the 'native-lib' native library, * which is packaged with this application. */ public native String stringFromJNI(); } 再来到build.gradle文件, 发现多出来了两个标签段, 也就是说, 如果我们自己要建CMake环境, 是要加这两段的. 用CMake向已有AS项目添加C/C++代码 新建一个空项目, 不含C++ support, 刚才的项目不要关, 之后会大段复制黏贴: 新建JNI目录, 发现在Android标签下是cpp, 到了Project标签下又是jni, 我一直很想知道谷歌是怎么实现这一点的. 创建一个Java类, 将之前项目的代码复制过来, 如下: public class MyJNI { // Used to load the 'native-lib' library on application startup. static { System.loadLibrary("native-lib"); } /** * A native method that is implemented by the 'native-lib' native library, * which is packaged with this application. */ public native String stringFromJNI(); } 然后在jni目录下创建cpp文件, 复制之前项目的代码, 注意包名的变动: #include <jni.h> #include <string> extern "C" JNIEXPORT jstring JNICALL Java_com_so_addcmake_MyJNI_stringFromJNI( JNIEnv *env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); } 然后将之前项目的CMakeLists.txt复制到这个项目的app目录下, 修改相对路径, 即将cpp变成jni, 然后文件名也可以更改, 但是注意对应. 接下来在build.gradle中加入代码, 之后同步: ndk { abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' } externalNativeBuild { cmake { path "CMakeLists.txt" } } 当然, 你可以手动操作进行关联, 右击app目录, 点击Link C++ Project with Gradle, 选择之前的CMakeLists.txt文件. 最后回到Activity, 设置组件显示从cpp函数返回的字符串, 编译运行: TextView tvTest = (TextView) findViewById(R.id.tv_test); tvTest.setText(new MyJNI().stringFromJNI()); 最后来自效果图: ndk-build 这是个有些过时的方式, 但是依旧是可以用的, 同样, 新建空项目. 然后和之前一样, 建一个cpp/jni目录. 复用之前的JNI类, 也就是加载了C++库和声明了本地函数的Java类. 创建Android.mk, Application.mk, helloNDK.cpp文件, 代码依次贴出: LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := HelloNDK LOCAL_SRC_FILES := helloNDK.cpp include $(BUILD_SHARED_LIBRARY) APP_MODULES := HelloNDK APP_ABI := all // // Created by 杨骁 on 2019/2/2. // #include <jni.h> #include <stdio.h> #ifdef __cplusplus extern "C" { #endif /** * 函数名规则: Java_包名_类名_方法名 * @param env 表示一个指向JNI环境的指针, 可以通过它来方位JNI提供的接口方法 * @param thiz 表示Java对象中的this * @return */ jstring Java_com_so_addndk_HelloNDK_get(JNIEnv *env, jobject thiz) { printf("invoke get in c++\n"); return env->NewStringUTF("Hello from JNI in helloJni.so !"); } void Java_com_so_addndk_HelloNDK_set(JNIEnv *env, jobject thiz, jstring string) { printf("invoke set from C++\n"); char* str = (char*)env->GetStringUTFChars(string,NULL); printf("%s\n", str); env->ReleaseStringUTFChars(string, str); } #ifdef __cplusplus } #endif 然后打开终端, 进入到jni目录, 使用ndk-build指令生成.so文件, 接着把生成的.so文件拷贝到app目录下的libs目录: 最后在Activity中调用就大功告成了: 最后 要说操作上这两种的复杂度感觉差不多, 但是我依旧推荐CMake方案, 至少这种是短时间不会过时的方案.
Ubuntu 操作 刚装好的Ubuntu18.04LTS是没有ssh, 想要开启操作如下: sudo apt-get update sudo apt-get install openssh-server sudo apt install net-tools 然后使用ifconfig指令查看ip, 比方说这里就是10.252.153.231, 用户名是so, 那么就可以通过其他客户机使用ssh so@10.252.153.231访问到这台主机, 当然, 前提是在同一网络内, 如果是不同网络访问还需其他配置, 这里暂且不提. 测试 这里就用我的mac对主机进行访问, 可以看到连接成功之后, 键入密码即可对主机进行操作. Mac 反过来, 我想用Ubuntu访问mac要怎么操作呢, 默认情况是被拒绝的. 打开偏好->共享, 如图开启远程登录, 这样就可以访问了.
关于NVIDIA显卡的驱动安装, 可以参考旧文, 装好驱动之后, 只需要一条指令即可完成cuda环境的搭建. sudo apt install nvidia-cuda-toolkit 大小在2G左右, 和官网下载的安装包大小相当. 然后用nvcc -V进行查看, 发现没有安装最新的CUDA10.0, 安装的是CUDA9.1. 操纵系统是Ubuntu18.04LTS. 然后使用如下代码进行测试: #include <stdio.h> __global__ void helloFromGPU () { printf("Hello, world! from GPU!\n"); } int main() { printf("Hello, world! from CPU!\n"); helloFromGPU <<<2, 10>>>(); cudaDeviceReset(); return 0; } 使用nvcc HWGPU.cu -o HWGPU.o指令编译, 运行结果如下:
目录 前言 准备工作 安装 Initializing IOV卡住 缺少网卡驱动 安装ESXi6.7 Multiboot could not setup the video subsystem 建立虚拟机 最后 前言 ESXi直接安装在物理服务器上(裸机), 并将其划分为多个逻辑服务器, 即虚拟机. 相比个人电脑上常见的先装OS, 再装VMware Fusion等虚拟机软件, 再分配空间建立虚拟机. ESXi更多用于服务器, 也更高效能. 准备工作 win7 u盘 ESXi镜像 网卡驱动 镜像重打包软件 u盘刻录软件 来一一说明: win7用于跑镜像重打包软件ESXi-Customizer和刻录软件软碟通. u盘用于刻录ESXi镜像. 镜像下载需要一个vmware的账号, 镜像下载地址 在安装ESXi的时候, 一般会缺少网卡驱动, 只能手动打包进去, 网卡驱动下载地址. 我下的是net55-r8168. 镜像重打包软件ESXi-Customizer用于将驱动打包进镜像, 用的是ESXi-Customizer-v2.7.2, 有点旧了, 但是依旧好用. 注意, 只能在win7下跑, win10不行. u盘刻录软件软碟通就不说了, 别的刻录软件也行. 安装 Initializing IOV卡住 如果你是4代酷睿, 你会遇到第一个bug. 卡在Initializing IOV. 解决方案就是进入之前按住shift + O. 然后输入 noIOMMU, 注意最前面有个空格的. 回车. 之后安装好了, 要启动之前, 还需要shift + O. 然后输入 noIOMMU 最后进入系统后, 管理界面 > F2 进入配置 > 登录 > 故障解决选项 > 启用Esxi Shell > alt + F1 > 登录 > 执行 esxcli system settings kernel set --setting=noIOMMU -v TRUE alt + F2退出, 然后可以再禁用ESXI shell. 缺少网卡驱动 如果你用的官方镜像, 八成是缺少网卡驱动的. 你会看到下图: 这就需要之前说的工具了, 手动下载驱动, 打包进镜像. 难度不大, 不多赘述. 安装ESXi6.7 然后就可以开始真正的安装了, 按流程走就好, 不难. 完工之后, 客户机通过ip访问服务器就行了. Multiboot could not setup the video subsystem 一般来说, 安装完了, 启动之前还会看到这个错误, 主要是分辨率没达到, 需要进入BIOS调整. 找到CSM Configuration > Video, 改成UEFI. 建立虚拟机 建立虚拟机和平常的虚拟机软件操作类似, 分两步, 上传镜像到服务器, 然后建立对应虚拟机即可. 最后 整体流程其实并不繁琐, 就是有些bug令人头疼. 喜欢记得点赞哦, 有意见或者建议评论区见哦~
目录 前言 GPU架构 GPU处理单元 概念GPU GPU线程与SM GPU线程 SM 加法 统一内存 乘法 最后 前言 在实际CUDA编程之前, 先来了解下GPU的结构. 和CPU相比显得粗暴又强大(手动滑稽). GPU架构 GPU处理单元 从这张GPU概念内核图开始讲起, 会发现和CPU内核是不同的, 少了三级缓存, 分支预测等等. 但是增加了ALU的数量, 扩大了上下文存储池(Pool of context storge). 可以看到, 上下文存储池分成4份, 也就是说, 可以执行4条指令流, 比方说指令1阻塞, 立马切换指令2, 指令2阻塞切换指令3, 这就起到了隐藏延迟的效果. 当然数量到底是多少是很讲究的, 不是越多越好.总的来看, 内核含8个ALU, 4组执行环境(Execution context), 每组有8个Ctx. 这样, 一个这样的内核可以并发(concurrent but interleaved)执行4条指令流(instruction streams), 32个并发程序片元(fragment). 概念GPU 复制16个上述的处理单元, 得到一个GPU. 实际肯定没有这么简单的, 所以说是概念GPU. 这个GPU含16个处理单元, 128个ALU, 64组执行环境(Execution context), 512个并发程序片元(fragment).祭出n多年前的卡皇GTX 480, 有480个CUDA核(也就是ALU), 内存带宽177.4GB/s. 而GTX 980 Ti有2816个CUDA核, 内存带宽336.5GB/s.但是带宽依旧是瓶颈, 虽然比CPU带宽高了一个数量级, 但是可以看到, GTX 980 Ti的带宽也就是多年前GTX 480的两倍左右. GPU线程与SM 由于目前还没有完全依靠GPU运行得机器, 一般来说, 都是异构的, CPU+GPU. 这一点是要特别注意的, 也就是Host与Device. GPU线程 在CUDA架构下, 显示芯片执行时的最小单位是thread. 数个thread可以组成一个block. 一个block中的thread能存取同一块共享的内存, 而且可以快速进行同步的动作. 不同block中的thread无法存取同一个共享的内存, 因此无法直接互通或进行同步. 因此, 不同block中的thread能合作的程度是比较低的. 上图: 然后依据thread, block和grid, 有着不同的存储. 核心就是thread. 可以结合下图进行理解: 每个处理器上有一组本地32位寄存器(Registers); 并行数据缓存或共享存储器(Shared Memory), 由所有标量处理器核心共享, 共享存储器空间就位于此处; 只读固定缓存(Constant Cache), 由所有标量处理器核心共享, 可加速从固定存储器空间进行的读取操作(这是设备存储器的一个只读区域); 一个只读纹理缓存(Texture Cache), 由所有标量处理器核心共享, 加速从纹理存储器空间进行的读取操作(这是设备存储器的一个只读区域), 每个多处理器都会通过实现不同寻址模型和数据过滤的纹理单元访问纹理缓存. SM 看到上图, GPU硬件的一个核心组件是SM, SM是英文名是Streaming Multiprocessor, 翻译过来就是流式多处理器. SM的核心组件包括CUDA核心, (其实就是ALU, 如上图绿色小块就是一个CUDA核心)共享内存, 寄存器等, SM可以并发地执行数百个线程, 并发能力就取决于SM所拥有的资源数. 当一个kernel被执行时, 它的gird中的线程块被分配到SM上, 一个线程块只能在一个SM上被调度. SM一般可以调度多个线程块, 这要看SM本身的能力. 那么有可能一个kernel的各个线程块被分配多个SM, 所以grid只是逻辑层, 而SM才是执行的物理层.下图是我GT 750M的显卡信息: SM采用的是SIMT(Single-Instruction, Multiple-Thread, 单指令多线程)架构, 基本的执行单元是线程束(wraps), 线程束包含32个线程, 这些线程同时执行相同的指令, 但是每个线程都包含自己的指令地址计数器和寄存器状态,也有自己独立的执行路径. 加法 试着用CUDA编程做一个矩阵加法: #include <stdio.h> __global__ void add(float * x, float *y, float * z, int n){ int index = threadIdx.x + blockIdx.x * blockDim.x; int stride = blockDim.x * gridDim.x; for (int i = index; i < n; i += stride){ z[i] = x[i] + y[i]; } } int main(){ int N = 1 << 20; int nBytes = N * sizeof (float); float *x, *y, *z; x = (float*)malloc(nBytes); y = (float*)malloc(nBytes); z = (float*)malloc(nBytes); for (int i = 0; i < N; i++){ x[i] = 10.0; y[i] = 20.0; } float *d_x, *d_y, *d_z; cudaMalloc((void**)&d_x, nBytes); cudaMalloc((void**)&d_y, nBytes); cudaMalloc((void**)&d_z, nBytes); cudaMemcpy((void*)d_x, (void*)x, nBytes, cudaMemcpyHostToDevice); cudaMemcpy((void*)d_y, (void*)y, nBytes, cudaMemcpyHostToDevice); dim3 blockSize(256); // 4096 dim3 gridSize((N + blockSize.x - 1) / blockSize.x); add << < gridSize, blockSize >> >(d_x, d_y, d_z, N); cudaMemcpy((void*)z, (void*)d_z, nBytes, cudaMemcpyDeviceToHost); float maxError = 0.0; for (int i = 0; i < N; i++){ maxError = fmax(maxError, (float)(fabs(z[i] - 30.0))); } printf ("max default: %.4f\n", maxError); cudaFree(d_x); cudaFree(d_y); cudaFree(d_z); free(x); free(y); free(z); return 0; } 由于我是用mac ssh访问linux主机的, 所以看中文都是乱码的, 就没打注释. 简单说下. 逻辑部分:申请1M的float, 放入10.0. 申请1M的float, 放入20.0, 然后加起来. 但是我们不存在直接看结果的. 就循环计算误差值, 输出最大的那个误差值. 最后看到是0就代表全部计算正确了. CUDA部分:cudaMalloc((void**)&d_x, nBytes);是很抢眼的, 意思也很简单, 在GPU中申请空间, 而不是CPU. 用cudaMemcpy((void*)d_x, (void*)x, nBytes, cudaMemcpyHostToDevice);将CPU中的数据放入到GPU, 注意第二个是源数据, 第三个是方向.dim3 blockSize(256);猜猜也知道了, 就是申请256个block. dim3 gridSize()同理.最后cudaMemcpy((void*)z, (void*)d_z, nBytes, cudaMemcpyDeviceToHost);从GPU中把结果拷贝回CPU, 注意第三个参数和之前的不同.记得释放申请的空间. 统一内存 时不我待. 在高版本的CUDA中, 升级了申请空间的操作. CUDA 6.x引入统一内存(Unified Memory). 具体内容建议查阅我给出的链接, 说的非常细致. 简单来说, 就是申请一次就好, 不用先CPU后GPU, 再拷贝来拷贝去, 太傻. #include <stdio.h> __global__ void add(float * x, float *y, float * z, int n){ // 同之前, 略 } int main() { int N = 1 << 20; int nBytes = N * sizeof(float); float *x, *y, *z; cudaMallocManaged((void**)&x, nBytes); cudaMallocManaged((void**)&y, nBytes); cudaMallocManaged((void**)&z, nBytes); for (int i = 0; i < N; ++i) { x[i] = 10.0; y[i] = 20.0; } dim3 blockSize(256); // 4096 dim3 gridSize((N + blockSize.x - 1) / blockSize.x); add << < gridSize, blockSize >> >(x, y, z, N); cudaDeviceSynchronize(); float maxError = 0.0; for (int i = 0; i < N; i++){ maxError = fmax(maxError, (float)(fabs(z[i] - 30.0))); } printf ("max default: %.4f\n", maxError); cudaFree(x); cudaFree(y); cudaFree(z); return 0; } 之前看起来有多蠢, 现在就有多简洁(手动滑稽). 注意到cudaMallocManaged((void**)&x, nBytes);, 这样的申请就无需再拷贝操作了.cudaDeviceSynchronize();同步device, 保证结果能正确访问. 具体原理请参看之前链接. 乘法 学过线性代数的都知道矩阵乘法是那种不难单头疼的工作, 原来可能用MATLAB偷懒, 现在可以考虑CUDA了(手动滑稽).先定义一个Matrix: struct Matrix { int width; int height; float *elements; }; 定义矩阵操作函数和乘法函数: __device__ float getElement(Matrix *A, int row, int col) { return A->elements[row * A->width + col]; } __device__ void setElement(Matrix *A, int row, int col, float value) { A->elements[row * A->width + col] = value; } __global__ void matMulKernel(Matrix *A, Matrix *B, Matrix *C) { float Cvalue = 0.0; int row = threadIdx.y + blockIdx.y * blockDim.y; int col = threadIdx.x + blockIdx.x * blockDim.x; for (int i = 0; i < A->width; ++i) { Cvalue += getElement(A, row, i) * getElement(B, i, col); } setElement(C, row, col, Cvalue); } 主函数并不难写, 主要是理解调度原理: int main() { int width = 1 << 10; int height = 1 << 10; Matrix *A, *B, *C; cudaMallocManaged((void**)&A, sizeof(Matrix)); cudaMallocManaged((void**)&B, sizeof(Matrix)); cudaMallocManaged((void**)&C, sizeof(Matrix)); int nBytes = width * height * sizeof(float); cudaMallocManaged((void**)&A->elements, nBytes); cudaMallocManaged((void**)&B->elements, nBytes); cudaMallocManaged((void**)&C->elements, nBytes); A->height = height; A->width = width; B->height = height; B->width = width; C->height = height; C->width = width; for (int i = 0; i < width * height; ++i) { A->elements[i] = 1.0; B->elements[i] = 2.0; } dim3 blockSize(32, 32); dim3 gridSize((width + blockSize.x - 1) / blockSize.x, (height + blockSize.y - 1) / blockSize.y); matMulKernel << < gridSize, blockSize >> >(A, B, C); cudaDeviceSynchronize(); float maxError = 0.0; for (int i = 0; i < width * height; ++i) maxError = fmax(maxError, fabs(C->elements[i] - 2 * width)); printf ("max fault: %f\n", maxError); return 0; } 和加法差不多, 就是多些矩阵操作. 不多说了~然后矩阵计算代码参考这篇文章. 最后 CUDA近期就先更到这里, 下次一更新应该是内核必须懂部分的. 喜欢记得点赞哦, 有意见或者建议评论区见~
目录 前言 开发环境一览 显卡驱动安装 下载驱动 禁用nouveau 安装驱动 安装CUDA8.0 第一个CUDA程序 向世界问好 最后 前言 在Linux下安装驱动真的不是一件简单的事情, 尤其是显卡驱动, 一失败直接进不去系统都是很可能的. 我在经历了无数折磨之后终于搭起了CUDA编程环境. 我是很心水老黄的, 但是, 我还是想说"So, Nvidia: FUCK YOU!"(Linux之父原话)(手动滑稽). 2007年6月23日 NVIDIA公司发布了CUDA, 透过这个技术, 用户可利用NVIDIA的GeForce 8以后的GPU和较新的Quadro GPU进行计算. 亦是首次可以利用GPU作为C-编译器的开发环境. 近年来, GPU最成功的一个应用就是深度学习领域, 基于GPU的并行计算已经成为训练深度学习模型的标配. GPU并不是一个独立运行的计算平台, 而需要与CPU协同工作, 可以看成是CPU的协处理器, 因此当我们在说GPU并行计算时, 其实是指的基于CPU + GPU的异构计算架构. 在异构计算架构中, GPU与CPU通过PCIe总线连接在一起来协同工作, CPU所在位置称为为主机端(host), 而GPU所在位置称为设备端(device), 如下图所示: CUDA技术有下列几个优点: 分散读取--代码可以从存储器的任意地址读取 统一虚拟内存(CUDA 4) 共享存储器--CUDA公开一个快速的共享存储区域(每个处理器48K), 使之在多个进程之间共享.其作为一个用户管理的高速缓存,比使用纹理查找可以得到更大的有效带宽. 与GPU之间更快的下载与回读 全面支持整型与位操作, 包括整型纹理查找 开发环境一览 GPU: NVIDIA GT 750M OS: UBUNTU 14.04.1LTS 64位(之后会说为什么选这个版本, 16.04流程基本类似, 但是要注意内核版本) 用指令看下英伟达显卡: lspci | grep -i nvidia 当你搭建完成环境之后, 可以用代码查看硬件信息, 自己写或者官方的例子, 我的NVIDIA GT 750M信息显示如下图, 当然可以直接到英伟达官网查看显卡信息. 显卡驱动安装 千万不要用UBUNTU附加驱动里提供的显卡驱动!!!千万不要用UBUNTU附加驱动里提供的显卡驱动!!!千万不要用UBUNTU附加驱动里提供的显卡驱动!!!一般来说, 你会遇到一些奇怪的问题, 当然, 锦鲤是不会出问题的(手动滑稽).这是第一个坑点, 大体有三种展现方式: 装完重启进不去系统, 卡住ubuntu加载页面; 无限登录; 装好了, 进入了系统, 然后输入nvidia-smi指令没有任何反应. 正常情况会弹出一张表, 如下所示: 下载驱动 行了, 来说说我的实操:首先到官网下载显卡驱动, 比方说我是GT 750M, 操作系统是64位Linux, 我就找对应的版本进行下载. 删掉以往的驱动. 注意, 就算你啥都没装, 这步也是无害的. sudo apt-get remove --purge nvidia* 更新并安装一些需要的库, 先装这么多, 之后装CUDA还有一波. sudo apt-get update sudo apt-get install dkms build-essential linux-headers-generic 禁用nouveau 打开blacklist.conf, 在最后加入禁用nouveau的设置, 这是一个开源驱动, 如图所示: sudo vim /etc/modprobe.d/blacklist.conf blacklist nouveau blacklist lbm-nouveau options nouveau modeset=0 alias nouveau off alias lbm-nouveau off 禁用nouveau内核模块 echo options nouveau modeset=0 sudo update-initramfs -u 重启. 如果运行如下指令没用打印出任何内容, 恭喜你, 禁用nouveau成功了. lsmod | grep nouveau 安装驱动 来到tty1(快捷键ctrl + alt + f1,如果没反应就f1-f7一个个试, 不同Linux, 按键会略有不同). 运行如下指令关闭图形界面. sudo service lightdm stop 安装驱动, 注意有坑, 一定要加-no-opengl-files, 不加这个就算安装成功, 也会出现无限登录问题. sudo chmod u+x NVIDIA-Linux-x86_64-390.87.run sudo ./NVIDIA-Linux-x86_64-390.87.run –no-opengl-files 如果你已经装了, 但是没有加-no-opengl-files, 按照如下操作可以救一下. sudo ./NVIDIA-Linux-x86_64-390.87.run –uninstall sudo ./NVIDIA-Linux-x86_64-390.87.run –no-opengl-files 重启. 用nvidia-smi指令试一下, 如果看到类似下图, 恭喜你, 驱动安装成功. 或者看到附加驱动显示继续使用手动安装的驱动. 安装CUDA8.0 先来补库. sudo apt-get install freeglut3-dev libx11-dev libxmu-dev libxi-dev libgl1-mesa-glx libglu1-mesa libglu1-mesa-dev 到官网下载要的CUDA版本, 我这里是8.0第二个版本, 下载runfile(local)版本, 如下图所示: md5检测一下, 不合格要重新下载. 下图是我的检测结果: md5sum cuda_8.0.61_375.26_linux.run 再次关闭图形界面 sudo service lightdm stop 安装时候依旧要加-no-opengl-files参数, 之后一路默认就好. sudo sh cuda_8.0.61_375.26_linux.run –no-opengl-files 然后会看到三个installed. 开启图形界面 sudo service lightdm start 这里又有一个坑, 如果是14.04.5, 我到这一步就开启不了了, 后来换成14.04.1就可以了, 总之内核也要小心选择. 运行如下命令, 如果显示如图3个文件夹, 恭喜你. ls /dev/nvidia* 如果少了或者都找不到, 还有一些操作要做. 用vim创建一个xxx.sh(名字随意), 输入以下内容: #!/bin/bash /sbin/modprobe nvidia if [ "$?" -eq 0 ]; then # Count the number of NVIDIA controllers found. NVDEVS=`lspci | grep -i NVIDIA` N3D=`echo "$NVDEVS" | grep "3D controller" | wc -l` NVGA=`echo "$NVDEVS" | grep "VGA compatible controller" | wc -l` N=`expr $N3D + $NVGA - 1` for i in `seq 0 $N`; do mknod -m 666 /dev/nvidia$i c 195 $i done mknod -m 666 /dev/nvidiactl c 195 255 else exit 1 fi /sbin/modprobe nvidia-uvm if [ "$?" -eq 0 ]; then # Find out the major device number used by the nvidia-uvm driver D=`grep nvidia-uvm /proc/devices | awk '{print $1}'` mknod -m 666 /dev/nvidia-uvm c $D 0 else exit 1 fi 运行后就会看到3个文件夹. sudo chmod +x xxx.sh sudo ./xxx.sh ls /dev/nvidia* /dev/nvidia0 /dev/nvidiactl /dev/nvidia-uvm 然后写入rc.local中, 如图所示: sudo vim /etc/rc.local 打开环境配置文件 sudo vim /etc/profile 最后写入: export PATH=/usr/local/cuda-8.0/bin:$PATH export LD_LIBRARY_PATH=/usr/local/cuda-8.0/lib64:$LD_LIBRARY_PATH 保存退出, 并其生效. source /etc/profile 然后就能看到了. 运行一些检测命令, 如果和我显示的类似, 恭喜你, 环境配置完成. cat /proc/driver/nvidia/version nvcc -V 当然, 你可以编译NVIDIA Samples中的例子, 然后运行. 最常见的就是deviceQuery了. 走两步: 第一个CUDA程序 之前在开发环境部分展示过一个小栗子, 来看看具体代码吧~ vim Device.cu #include <stdio.h> int main() { int nDevices; cudaGetDeviceCount(&nDevices); for (int i = 0; i < nDevices; i++) { cudaDeviceProp prop; cudaGetDeviceProperties(&prop, i); printf("Device Num: %d\n", i); printf("Device name: %s\n", prop.name); printf("Device SM Num: %d\n", prop.multiProcessorCount); printf("Share Mem Per Block: %.2fKB\n", prop.sharedMemPerBlock / 1024.0); printf("Max Thread Per Block: %d\n", prop.maxThreadsPerBlock); printf("Memory Clock Rate (KHz): %d\n", prop.memoryClockRate); printf("Memory Bus Width (bits): %d\n", prop.memoryBusWidth); printf("Peak Memory Bandwidth (GB/s): %.2f\n\n", 2.0 * prop.memoryClockRate * (prop.memoryBusWidth / 8) / 1.0e6); } return 0; } nvcc Device.cu -o Device.o ./Device.o 向世界问好 没有Hello, world!怎么行? 具体说明要到下期了哦~ #include <stdio.h> __global__ void helloFromGPU () { printf("Hello, world! from GPU!\n"); } int main() { printf("Hello, world! from CPU!\n"); helloFromGPU <<<2, 10>>>(); cudaDeviceReset(); return 0; } 最后 事实上, 由于没有以GPU为单独核心的终端, 所以GPU编程其实都是CPU + GPU结构的, CPU负责复杂的串行, GPU负责并行计算. 好了, 喜欢记得点赞哦, 有意见或者建议评论区见~
目录 前言 文件系统结构 新建文件和inode 文件创建过程 inode解析 打开文件 参考 最后 前言 这次来说文件系统. 文件系统是非常重要的, 提高磁盘使用率, 减小磁盘磨损等等都是文件系统要解决的问题. 市面上的文件系统也是数不胜数, 比较常用的像ext4, xfs以及ntfs等等, 国内的像鹅厂的tfs, 然后还有sun号称"last word in file system"的ZFS, 学习ZFS而来的btrfs. 下面上一张Linux文件系统组件的体系结构图, 是我整合了多方文献并结合自己的经验画出来的. 可以看出, 最重要的就是vfs, 正是因为它, 才让Linux可以同时支持多种的文件系统. 举个例子, 比如你装了双系统mint+windows, 在mint中, 你可以看到windows的ntfs磁盘, 但是返回了windows, 你就看不到mint的磁盘了. 那Linux支持哪些文件系统呢? 来到源码的fs文件夹, Linux支持的文件系统可多了去了, 注意看蓝色的. 文件系统结构 磁盘扇区什么的就不多说了. 也许会出一篇谈存储介质的文章, 说说ssd结构啥的. 直接跳过硬件从文件系统结构开始. 注意, 我说的是通用模型, 每个fs的具体实现有差异, 而且差异蛮大的. ext家族是Linux默认的fs了, 事实上ext2/ext3和ext4差异也很大. superblock: 记录此fs的整体信息, 包括inode/block的总量、使用量、剩余量, 以及文件系统的格式与相关信息等; inode table: superblock之后就是inode table, 存储了全部inode. data block: inode table之后就是data block. 文件的内容保存在这个区域. 磁盘上所有块的大小都一样. inode: 记录文件的属性, 同时记录此文件的数据所在的block号码. 每个inode对应一个文件/目录的结构, 这个结构它包含了一个文件的长度、创建及修改时间、权限、所属关系、磁盘中的位置等信息. block: 实际记录文件的内容. 一个较大的文件很容易分布上千个独产的磁盘块中, 而且, 一般都不连续, 太散会导致读写性能急剧下降. 好, 我猜你和我一样是右脑思维, 上图就好: 可以看出来, 这是多层索引结构的文件系统. 用b+树是最佳解决方案, 比如btrfs. inode table指向inode, inode指向一个或者多个block, 注意, 图中还是直接指向, 后面还会讲述多层指向. 最怕的就是inode指向的block太散. 一个比较好的解决办法就是在文件末尾不断添加数据, 而不是新建文件. 新建文件和inode 新建一个文件和文件夹, 用stat指令查看文件信息. touch hello stat hello mkdir hellodir stat hellodir 可以看到一些信息. 例如一个目录初始大小就是4KB, 8个block, 一个扇区就是512B, 一个io block是4KB, 对应第一幅图的General Block Device Layer层. 这些其实不看也知道, 前提是这是常规的fs. 文件创建过程 创建成功一个文件有4步: 存储属性: 也就是文件属性的存储, 内核先找到一块空的inode. 例如, 之前的1049143. 内核把文件的信息记录其中. 如文件的大小、文件所有者、和创建时间等, 用stat指令都可以看到. 存储数据: 即文件内容的存储, 比方建立一个1B的文件, 那一个block, 8个扇区, 内核把数据放到一个空闲逻辑块中也就是空闲block中. 很明显, 碎片化的问题已经呈现在这里了. 1B它也要用4K对吧. 记录分配情况: 假如数据保存到了3个block中, 位置要记录到inode的磁盘序号列表中. 这3个编号分别放在最开始的3个位置. 然后读的时候会一次性读, 可以看我的第二张图. 当然了fat就没有inode, 它在一个块中放了下一个块的位置, 形成链, u盘就是这种fs. 添加文件名到目录: 文件名和inode之间的对应关系将文件名和文件以及文件的内容属性连接起来, 找到文件名就找到文件的inode,通过inode就能找到文件的属性和内容. 换句话说, 就是机器看的和人看的做衔接, 例如网址和ip. 当然, 如果你看inode就能区分文件, 第四步可以不要(手动滑稽). 目录的话, 就是多了.文件(指向自己), ..文件(指向上级目录). 然后添加自己的inode到上级目录. 看图就秒懂了. inode解析 用df指令可以看inode的总数和使用量. df -i dumpe2fs打开指定磁盘可以看inode的大小, 这里是256. inode如何记录文件并且最大是多少呢? inode记录block号码的区域定义为12个直接, 一个间接, 一个双间接与一个三间接记录区. 一个inode是4B, 这样用4K的block可以有1K的inode. 直接: 12 * 4K 间接: (4K / 4) * 4K 双间接: (4K / 4) (4K / 4) 4K 三间接: (4K / 4) (4K / 4) (4K / 4) * 4K 所以的话, 4T, are you OK? 算归算, fs在不断发展, 这是过时的大小了. ext4的话单个文件可以到达16TB, fs可达1EB. 但是注意, ext4的作者都说了, ext4只是过渡, btrfs会更棒, 那事实上, cent os用的xfs也很很棒. 打开文件 创建之后当然要打开了, 打开文件也是有一系列过程的. 先来看看两个指令: sysctl -a | grep fs.file-max ulimit -n 第一个指令查看os最大打开数, 这是系统级限制. 第二个指令查看单进程最大打开数, 这是用户级限制. 进程描述符(task_struct): 为了管理进程, 操作系统要对每个进程所做的事情进行清楚地描述, 为此, 操作系统使用数据结构来代表处理不同的实体, 这个数据结构就是通常所说的进程描述符或进程控制块(PCB). 通俗来讲就是操作系统中描述进程的结构体叫做PCB. Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程, 这个结构体包含了一个进程所需的所有信息. 它定义在include/linux/sched.h文件中. 这不是这次的重点, 但是这个task_struct结构体确实很重要, 也很复杂. 每个进程都会被分配一个task_struct结构, 它包含了这个进程的所有信息, 在任何时候操作系统都能跟踪这个结构的信息. 文件描述符表(file_struct): 该表记录进程打开的文件. 它的表项里面有一个指针, 指向存放在内核空间的文件表中的一个表项. 它向用户提供一个简单的文件描述符(fd), 使得用户可以通过方便地访问一个文件. 例如, 当进程使用open打开一个文件时, 内核就会在这个表中添加一个表项. 如果对同一个文件打开多次, 那么将有多个表项. 使用dup时, 也会增加一个表项. 文件表: 文件表保存了进程对文件读写的偏移量. 该表还保存了进程对文件的存取权限等等. 比如, 进程以O_RDONLY方式打开文件, 这将记录到对应的文件表表项中. 然后每个表有一个指向inode table中inode的指针. 结合之前的图片看, 所有结构就联系起来了, 所以inode是核心点. 上图上图: 在进程A中, 文件描述符1和2都指向了同一个打开的文件表A. 这可能是通过调用dup()、dup2()、fcntl()或者对同一个文件多次调用了open()函数而形成的. 进程A的文件描述符0和进程B的文件描述符2都指向了同一个打开的文件表A. 这种情形可能是在调用fork()后出现的(即, 进程A、B是父子进程关系), 或者当某进程通过UNIX域套接字将一个打开的文件描述符传递给另一个进程时, 也会发生. 再者是不同的进程独自去调用open函数打开了同一个文件, 此时进程内部的描述符正好分配到与其他进程打开该文件的描述符一样. 此外, 进程A的描述符0和进程B的描述符255分别指向不同的打开文件表, 但这些文件表均指向inode table的相同条目(假设), 也就是指向同一个文件. 发生这种情况是因为每个进程各自对同一个文件发起了open()调用。同一个进程两次打开同一个文件, 也会发生类似情况. 为什么要说这些情况呢? 因为如果没有理解清楚这些, 在做多进程多线程read和write的时候很有可能会导致读取和写入混乱. 参考 看了非常多很棒的文章, 这里也分享给大家. https://blog.csdn.net/yuexiaxiaoxi27172319/article/details/45241923 https://www.ibm.com/developerworks/cn/linux/l-linux-filesystem/index.html http://c.biancheng.net/cpp/html/2780.html http://www.ruanyifeng.com/blog/2011/12/inode.html https://www.cnblogs.com/xiaojiang1025/p/6363626.html https://blog.csdn.net/cywosp/article/details/38965239 https://blog.csdn.net/luotuo44/article/details/17474099 https://blog.csdn.net/MBuger/article/details/72123692 https://blog.csdn.net/u014379540/article/details/53456070 https://zhuanlan.zhihu.com/p/34280875 https://blog.csdn.net/gatieme/article/details/51383272 http://gityuan.com/2017/07/30/linux-process/ 最后 这次从结构上逐步往内解剖文件系统, inode是核心点. 当然还有两篇甚至更多的后续文章, 最后会写个简单的用户态文件系统, 喜欢记得点个赞或者关注我哦~
目录 前言 模块与系统调用 用模块打印Hello, world! 用模块添加自定义系统调用 top指令 关闭Linux图形界面 重编内核添加系统调用 解压系统源代码 撰写自定义系统调用 编译内核 测试新内核 最后 前言 要自定义系统调用, 常规的两个方法是模块和重编内核, 一起来看看吧. 更新:在64位ubuntu12.04.5上也成功运行.解决了14.04, 16.04, 18.04上的问题. 模块与系统调用 用模块打印Hello, world! 首先看下系统版本和内核版本. 我用的是32位的ubuntu12.04.5LTS, 我很不喜欢用这么旧的版本, 但是, 其它版本都出现各种问题, 之后我会给大家展示我掉过的坑, 如果你能帮助我解决, 请评论区, 提前感谢~~ uname -a cat /proc/version uname -r 我是在mac端用ssh访问Linux的, 这样是有很多好处的, 比如直接复制粘贴, 不需要改键盘映射等等. 先来写一个test.c. #include<linux/kernel.h> #include<linux/init.h> #include<linux/module.h> MODULE_LICENSE("Dual BSD/GPL"); static int __init hello_init(void) { printk("Hello, world! Written by Sorrower\n"); return 0; } static void __exit hello_exit(void) { printk("Exit, world! Written by Sorrower\n"); } module_init(hello_init); module_exit(hello_exit); 然后写Makefile. 注意看, 如果你用的vim, make前面如果是空格不是TAB, vim是会提示你的. 我这里就是TAB, 所以没有提示. obj-m:=test.o CURRENT_PATH :=$(shell pwd) VERSION_NUM :=$(shell uname -r) LINUX_PATH :=/usr/src/linux-headers-$(VERSION_NUM) all : make -C $(LINUX_PATH) M=$(CURRENT_PATH) modules clean : make -C $(LINUX_PATH) M=$(CURRENT_PATH) clean 输入make指令. 会生成一些文件, 要用的是.ko文件. 你可以用lsmod指令看下有什么模块. 然后插入刚才生成的test.ko. sudo insmod test.ko 然后看下打印了消息没. dmesg | grep "sorrower" 可以再用lsmod看一下. 然后卸载模块. 用dmesg | grep "Sorrower"查看. sudo rmmod test 用模块添加自定义系统调用 注意, 题目是用系统调用打印Hello, world!, 之前的只是熟悉一下模块的使用, 还不是系统调用打印出来的.来到/usr/include/i386-linux-gnu/asm, 查看unistd_32.h, 注意这是32位ubutnu12.04.5中的位置, 不代表其他版本其他位数的. 看到223了吗, 这很明显就是拿来自定义的. 然后来到/boot, 要查看sys_call_table的内存位置, 注意, 要管理员权限. 然后用vim搜索sys_call_table. 我特意把行号标出来了, 你要是想手动找到, 祝你好运了. 开始写syscall.c. 这段代码不是我写的, 来自这篇文章, 写得很棒. 然后请原谅我不要脸地在自定义系统调用里面加了自己的Hello, world!(手动滑稽) #include <linux/kernel.h> #include <linux/init.h> #include <linux/module.h> #include <linux/unistd.h> #include <linux/sched.h> MODULE_LICENSE("Dual BSD/GPL"); #define SYS_CALL_TABLE_ADDRESS 0xc1697140 //sys_call_table对应的地址 #define NUM 223 //系统调用号为223 int orig_cr0; //用来存储cr0寄存器原来的值 unsigned long *sys_call_table_my=0; static int(*anything_saved)(void); //定义一个函数指针,用来保存一个系统调用 static int clear_cr0(void) //使cr0寄存器的第17位设置为0(内核空间可写) { unsigned int cr0=0; unsigned int ret; asm volatile("movl %%cr0,%%eax":"=a"(cr0));//将cr0寄存器的值移动到eax寄存器中,同时输出到cr0变量中 ret=cr0; cr0&=0xfffeffff;//将cr0变量值中的第17位清0,将修改后的值写入cr0寄存器 asm volatile("movl %%eax,%%cr0"::"a"(cr0));//将cr0变量的值作为输入,输入到寄存器eax中,同时移动到寄存器cr0中 return ret; } static void setback_cr0(int val) //使cr0寄存器设置为内核不可写 { asm volatile("movl %%eax,%%cr0"::"a"(val)); } asmlinkage long sys_mycall(void) //定义自己的系统调用 { printk("Hello, world! Written by Sorrower\n"); printk("模块系统调用-当前pid:%d,当前comm:%s\n",current->pid,current->comm); return current->pid; } static int __init call_init(void) { sys_call_table_my=(unsigned long*)(SYS_CALL_TABLE_ADDRESS); printk("call_init......\n"); anything_saved=(int(*)(void))(sys_call_table_my[NUM]);//保存系统调用表中的NUM位置上的系统调用 orig_cr0=clear_cr0();//使内核地址空间可写 sys_call_table_my[NUM]=(unsigned long) &sys_mycall;//用自己的系统调用替换NUM位置上的系统调用 setback_cr0(orig_cr0);//使内核地址空间不可写 return 0; } static void __exit call_exit(void) { printk("call_exit......\n"); orig_cr0=clear_cr0(); sys_call_table_my[NUM]=(unsigned long)anything_saved;//将系统调用恢复 setback_cr0(orig_cr0); } module_init(call_init); module_exit(call_exit); MODULE_AUTHOR("25"); MODULE_VERSION("BETA 1.0"); MODULE_DESCRIPTION("a module for replace a syscall"); Makefile文件和之前差不多, 改下生成的.o文件名字就好.然后要写一个用户态的程序来测试了. 什么是用户态, 来快速解释一下. cpu有用户态和核心态, 系统调用以及中断和异常都会由用户态变成核心态. 上一张进程转换图(或者叫状态机?), 图片来自网络, 我觉得画得一般, 但是我不想再手动画一张了. 好了, 不皮了. 来写test.c吧. 简单粗暴, 就一个系统223调用. #include<stdio.h> #include<stdlib.h> int main() { syscall(223); return 0; } gcc一下, 然后dmesg一下. 这下真的就结束这一部分了. top指令 中途休息一下, 来说些小技巧和指令.mac下的top指令非常好用. 你输入top, 然后输入?, 就显示全部后续操作了. 比如这里top下输入o, 在输入cpu回车. 就是cpu占有排序. 关闭Linux图形界面 我没有很讨厌Linux的图形界面, 但是用了ssh之后, 你就发现确实用不到了. 我知道大家都会切换到tty的. mac是fn+ctrl+option+f3(当然了, 根据版本不同, fx有效范围不同, 12.04是f1-f6, f7图形界面, 测测就知道了) 但是还不够彻底, 要让它开机直接字符界面. 关闭/开启. 当然了, 12.04似乎不吃这个指令. 要再高版本一些. sudo systemctl set-default multi-user.target sudo reboot sudo systemctl set-default graphical.target sudo reboot 重编内核添加系统调用 接下来这个就很简单了, 主要难度在找文件位置以及cpu. 这里切换回18.04LTS. cpu不好的, 可能2h+了, 好的cpu编个18.04LTS怎么20min也要吧. cpu核数两位数的麻烦关闭页面, 不在一个频道了(手动滑稽). 那顺带一提, 之前说的彻底关闭图形界面在18.04LTS就生效了. 解压系统源代码 你可以使用指令下载源码, 也可以手动下载. 总之, 下完之后, 解压文件. 看图片, 我就是用指令下载, 然后再解压压缩包, 所以有两个同名目录. sudo apt-get install linux-source sudo tar -jxvf linux-source-4.15.0.tar.bz2 撰写自定义系统调用 关键是三个文件sys.c, syscalls.h, syscall_32.tbl. 都在很要命的地方呢. sys.c在/usr/src/linux-source-4.15.0/linux-source-4.15.0/kernel下 syscalls.h在/usr/src/linux-source-4.15.0/linux-source-4.15.0/arch/x86/include/asm下 syscall_32.tbl在/usr/src/linux-source-4.15.0/linux-source-4.15.0/arch/x86/entry/syscalls下 对着调用编号就是666(手动滑稽). 打开sys.c写自定义函数, 注意函数名对应. 申明函数, 还是注意名称对应. 编译内核 需要先补下库. sudo apt-get install libncurses5-dev 然后你可以设置编译参数, 如果你知道自己在干嘛的话. sudo make menuconfig 然后就是cpu测试时间了. 编译好了, 装下重启就完事了. 我就不重做了. sudo make sudo make modules make modules_install make install 测试新内核 上几张之前实验时候截的效果图, 测试函数还是之前的test.c, 改下调用号就可以了. 最后 先来几个坑, 求人救救孩子~~这是14.04.5中的, 说什么Invalid module format, StackOverFlow说是内核版本不一致, 但是我Makefile中是用'uname -r'的, 怎么会不一致呢. 问题已经解决, 如果出现上述错误, 只需要使用:sudo apt-get install linux-source-(uname -r得到的内核号)即可. 例如: sudo apt-get install linux-source-4.15.0 之后使用如下指令, 可能会提示补库: sudo make bzImage sudo make modules sudo make modules_install reboot之后问题迎刃而解. 然后看一下默认的18.04, 不是我改过内核的那个. 也在google和StackOverFlow看了解决方案, 还是解决不能. 解决方案除了重编内核, 就是重新安装镜像, 目前我新装的18.04.1测试没问题. 这次也是新开一个篇章, 和以往分享操作不同, 文章更偏向探索, 去学习更深的知识. 喜欢记得点赞, 有意见或者建议评论区见, 暗中关注我也是可以的~
目录 前言 虚拟机设置 安装系统 VMware Fusion中体验 mac使用ssh访问Linux 最后 前言 我发现云栖搬运的文章质量有些问题, 所以有些更新我就手动重发一下了. 今天也是第一时间下载了Ubuntu18.04LTS, 作为一个使用了16.04蛮久时间的程序员, 我表示还是有些激动的. 当然, 没有我拿到mac那么激动(滑稽脸). 好, 我就化身搞事boy, 带大家走一波~顺带推荐下我的老文章不美翻怎么开发!Ubuntu 16.04 LTS深度美化!.更新了用VMware Fusion体验Ubuntu18.04.1LTS的部分.更新了mac用ssh访问虚拟机部分. 虚拟机设置 先去官网下个镜像, 然后直接往虚拟机里面拖. 勾选下面的两个按钮, 第一个是生成一个桌面图标, 第二个是进行一些设置. 尽可能少得建立mac和linux的关联, 如果你能明白每个选项的具体作用, 并且会用到, 另当别论了. 安装系统 非常熟悉的安装界面和一点点都不熟悉的操作系统界面, 这是Ubuntu?(捂脸), 除了越来越红, 我还真是认不得了.(尴尬脸) 安装好之后第一次进入会看到如下: VMware Fusion中体验 其实我用VM主要的原因是简洁, PD总会生成一些奇怪又没什么用的东西. 安装部分就不说了, 装好了VMware Tools之后, 我选择使用Retina显示, 然后这里需要回到Ubuntu设置显示器. 之后你就可以体验到界面精美的Ubuntu了. 效果图: mac使用ssh访问Linux 先上效果图, 再说实现: 可以看到我已经成功访问到ubuntu了. ip是Linux对应的ip. 首先你需要在Linux下安装ssh, 然后ps查看一下, 有sshd就对了. 如果没有就手动开启. sudo apt-get install openssh-server ps -e | grep ssh sudo /etc/init.d/ssh start 然后就可以在mac上访问了. ssh so@172.16.97.130 最后 好了, 就体验到这里了, 整体来看改动真的大, 不过很多地方还是很有意思的. 不过最近看一篇paper的时候, 发现华为的工程师还在用14.04进行网络测试. 所以呀, Linux真的要用, 没准还是旧版本更好呢. 喜欢记得点赞或者关注我哦~
目录 前言 稳定匹配 不稳定对 Propose-And-Reject Algorithm 最后 前言 文章内容取自 http://www.cs.cmu.edu/~arielpro/15896s16/slides/896s16-16.pdf 并有所修改, 如有侵权等问题, 请提示删除(手动感谢). 这次主要是开个系列分享分享有趣的算法. 稳定匹配(The Stable Matching Problem) 不稳定对(Unstable pair) 如果: 男生x相比现有配对更喜欢女生y 女生y相比现有配对更喜欢男生x 这就是一个不稳定对, 很好理解吧. 那么稳定匹配就是不存在不稳定对的匹配.举个小栗子, 现在给出联谊上3男3女的喜欢表, 提什么男男/女女的, 一对多的, 麻烦关闭页面(手动无奈). 男生喜欢 1st 2nd 3rd X A B C Y B A C Z A B C 女生喜欢 1st 2nd 3rd A Y X Z B X Y Z C X Y Z 这里我们尝试给出一个解, 看是不是稳定匹配. X-C, Y-B, Z-A. 很明显, X, A, B是可能出事情的. 如果X-A, 就出事了, 因为A比起现在匹配的Z更喜欢X; 同理, X-B也出事了, B可是最喜欢X的. 那么GG, 红线崩了. 男生喜欢 1st 2nd 3rd X A B C Y B A C Z A B C 女生喜欢 1st 2nd 3rd A Y X Z B X Y Z C X Y Z X-A, Y-B, Z-C如何呢? 发现是稳定的, 因为A, B都最不喜欢Z. 男生喜欢 1st 2nd 3rd X A B C Y B A C Z A B C 女生喜欢 1st 2nd 3rd A Y X Z B X Y Z C X Y Z 但是不是每次都这么好运能找到稳定匹配的, 所以需要算法帮助解决问题. Propose-And-Reject Algorithm Propose-And-Reject Algorithm(Gale-Shapley 1962)可以解决问题. 贴一下伪代码, 这个算法原本是解决求婚问题的, 但是当成联谊看比较合适, 哪有人日常换未婚夫的(手动滑稽). Initialize each person to be free. while (some man is free and hasn't proposed to every woman) { Choose such a man m w = 1st woman on m's list to whom m has not yet proposed if (w is free) assign m and w to be engaged else if (w prefers m to her fiancé m') assign m and w to be engaged, and m' to be free else w rejects m } 那么上面那一题就很快得出答案了, 就是我给的稳定匹配.但是需要证明一下, 当然我引用的文献证明, 我的证明能力就很佛系了. 完整性证明 全部男女都找到了另一半 假设Z凉凉, 没有匹配. 那么由于一对一的限制, 某个妹子A也没人爱. 换句话说, 没人向A表白. 但是Z必须要向全部妹子表白才结束循环. 所以反证成功. 稳定性证明 没有不稳定对 假设A-Z不稳定, 要搞事. 情况1: Z没有向A表白 说明Z更喜欢现有伴侣而不是A. A-Z稳定. 情况2: Z有向A表白 A拒绝了Z(立刻或者之后甩了). 那说明A更喜欢现有伴侣. A-Z稳定. 反证成功. 最后 想要代码实现你要加上数据结构, 复杂度是n的平方, 毕竟程序 = 数据结构 + 算法. 然后的话, 所以想要获得幸福, 要主动出击以及保持好形象(手动捂脸). 还有还有, 我不是标题党哈, 你试试跑个10男10女的匹配, 这个算法绝对渣出天际. 喜欢记得点赞, 有意见或者建议评论区见~
更新nasm mac自带了nasm, 但是, 但是, 但是, 老得一塌糊涂. brew install nasm 安装完之后重启. 撰写编译运行 之后就是和写c差不多了. 但是, 我们要讲究一些, 写个32, 写个64, 好吧. 64: nasm -f macho64 -o helloworld.o helloworld.asm ld -o helloworld -e _main helloworld.o ./helloworld 32: 最后 我汇编很佛系的, 最近也是要开始认真学了. 喜欢记得点赞哦, 有意见或者建议评论区~ 参考文章: http://gaoryrt.com/2015/11-18-assembly/https://blog.csdn.net/u011987514/article/details/72615406
我的博客即将入驻“云栖社区”,诚邀技术同仁一同入驻。 同时你可以到以下网站找到我:慕课网掘金csdn简书腾讯云+社区 除此之外, 应该非我本人账号.
github传送门 目录 前言 效果图 快速上手 CollapsingToolbarLayout折叠模式 AppBarLayout滚动方式 CoordinatorLayout配合Snackbar 自定义伸缩头部 最后 前言 之前也是写了RecyclerView的内容, 这次再补充伸缩头部的实现. 港真, 伸缩头部是那种看到第一眼就会爱上的视图效果, 好看又简洁. 本文内容较多, 需要10分钟以上阅读时间, 请合理安排, 收藏也是可以的哦~ 多图预警, 转载请说明出处, 感谢~ 效果图 先上案例的效果图, 有兴趣再看下去: case1 case2 快速上手 先来实操一下, 看看从默认的滚动模板(Scrolling Activity)到效果图要几步. 选择模板 首先, 在Toolbar上面加入ImageView, 参数之后再说明. <android.support.design.widget.CollapsingToolbarLayout android:id="@+id/toolbar_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" app:contentScrim="?attr/colorPrimary" app:layout_scrollFlags="scroll|exitUntilCollapsed" app:toolbarId="@+id/toolbar"> <ImageView android:id="@+id/iv_main" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:contentDescription="@string/desc" android:scaleType="centerCrop" app:layout_collapseMode="pin" /> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" /> </android.support.design.widget.CollapsingToolbarLayout> 然后在java代码中使用Glide加载图片. 导包: implementation 'com.github.bumptech.glide:glide:3.7.0' // 加载图片 ImageView ivMain = (ImageView) findViewById(R.id.iv_main); Glide.with(this).load(R.drawable.p5).into(ivMain); 看下效果: 阶段效果图 发现两个问题, 由于背景是白色, 标题栏字体颜色要变成黑色, 默认就是黑色, 所以就是删除xml中的主题设置. 当然, 如果你是深色背景, 这里就无需动它. 然后标题栏需要变成透明的. 将标题栏设置透明色 那由于5.0之前是不能变的, 将styles.xml从5.0区分开, 5.0之前什么都不做, 之后版本设置标题栏为透明色. 现在styles.xml中写入: <style name="MyTheme" parent="AppTheme" /> 然后复制styles.xml: 复制styles.xml 删除重复部分: <resources> <style name="MyTheme" parent="AppTheme"> <item name="android:statusBarColor">@android:color/transparent</item> </style> </resources> 然后在配置文件中设置新主题, 顺带改下标题名称, 再次运行看下效果: // 设置标题 ActionBar supportActionBar = getSupportActionBar(); if (supportActionBar != null) { supportActionBar.setTitle(UIUtil.getString(R.string.p5)); } 修正后效果图 这样就完成了. CollapsingToolbarLayout折叠模式 app:layout_collapseMode="parallax" app:layout_collapseMode="pin" app:layout_collapseMode="none" 从xml中的参数说吧, 来看CollapsingToolbarLayout的折叠模式. 下面我也是截了一段官方文档内容. COLLAPSE_MODE_OFF int COLLAPSE_MODE_OFF The view will act as normal with no collapsing behavior. Constant Value: 0 (0x00000000) COLLAPSE_MODE_PARALLAX int COLLAPSE_MODE_PARALLAX The view will scroll in a parallax fashion. See setParallaxMultiplier(float) to change the multiplier used. Constant Value: 2 (0x00000002) COLLAPSE_MODE_PIN int COLLAPSE_MODE_PIN The view will pin in place until it reaches the bottom of the CollapsingToolbarLayout. Constant Value: 1 (0x00000001) 列个表再看下: 参数 效果 none 视图将正常运行, 没有折叠行为 pin 视图将固定到位, 直到它到达CollapsingToolbarLayout的底部 parallax 视图将以视差方式滚动 是不是该怎么懵还是怎么懵, 来看效果图: parallax模式 pin模式 注意看人物的脚, parallax模式下人物最终滑动到身体部位消失. pin模式下, 人物滑到脚部位消失. 也就是说, pin模式下, 下面的滚动视图和图片是同步滑动的, 但是这样的观感其实不好. parallax则改进了这一点, 看起来很和谐, 尽管两者不再同步, 这就是翻译后说的以视差方式滚动了. AppBarLayout滚动方式 滚动方式主要依靠参数组合(scroll必须要), 列个表再看下效果图, 官方文档就不截了. 参数 效果 scroll 视图将滚动与滚动事件直接相关. 需要设置此标志才能使任何其他标志生效. 如果在此之前的任何兄弟视图没有此标志, 则此值无效. exitUntilCollapsed 退出(滚动屏幕)时, 视图将滚动直到“折叠”. 折叠高度由视图的最小高度定义。 snap 在滚动结束时, 如果视图仅部分可见, 则它将被捕捉并滚动到其最近的边缘. enterAlways 当进入(在屏幕上滚动)时, 无论滚动视图是否也在滚动, 视图都将滚动任何向下滚动事件. 这通常被称为“快速返回”模式. enterAlwaysCollapsed 'enterAlways'的另一个标志, 它修改返回的视图, 最初只回滚到它的折叠高度. 一旦滚动视图到达其滚动范围的末尾, 该视图的其余部分将滚动到视图中. 折叠高度由视图的最小高度定义. 看看单scroll的情况: app:layout_scrollFlags="scroll" scroll 可以看到整个滚上去了, 没有保留Toolbar. 那我现在用的是app:layout_scrollFlags="scroll|exitUntilCollapsed", 效果大家也见过了. 喜闻乐见的吸附效果, app:layout_scrollFlags="scroll|snap", 例如, 还剩下25%没滚完, 松手就自己滚出去; 如果还有75%没滚完, 松手直接全部显示. 但是我感觉体验不好, 会让人有着操作不灵敏的错觉. snap 快速返回, 就是把滚出去的部分快速显示出来, 可以对比之前的返回速度来看: app:layout_scrollFlags="scroll|enterAlways" enterAlways 对比快速返回来看, 这个相对柔和一些, 可以理解为二段式的快速返回, 总之就是返回没有enterAlways那么迅速: app:layout_scrollFlags="scroll|enterAlwaysCollapsed" enterAlwaysCollapsed CoordinatorLayout配合Snackbar 先来看看自带的点击悬浮按钮的效果: 默认效果 不让悬浮按钮吸附在Toolbar上, 将它放置到底部, 再看下效果: android:layout_gravity="end|bottom" 自动上移 如果不是CoordinatorLayout, 可就没有这种效果了哦. 自定义伸缩头部 再来看一个改动更大, 更自定义的. 先上效果图: 效果图 相比于之前的, 最大的变化在于对滚动幅度的监听. 依据滚动幅度变化Toolbar内容. 布局文件 先来看下主布局文件的变化, Toolbar包含了两个布局文件, 相互切换. 然后展开部分由之前的ImageView变成了一个布局文件, 这里要注意app:contentInsetLeft="0dp", app:contentInsetStart="0dp", 这个就像html的默认边距一样, 需要清零. 不写的话左侧有默认的边距. <android.support.design.widget.CollapsingToolbarLayout android:id="@+id/toolbar_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" app:layout_scrollFlags="scroll|exitUntilCollapsed" app:toolbarId="@+id/toolbar"> <include layout="@layout/open_content" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="?attr/actionBarSize" app:layout_collapseMode="parallax" /> <android.support.v7.widget.Toolbar android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:contentInsetLeft="0dp" app:contentInsetStart="0dp" app:layout_collapseMode="pin"> <include android:id="@+id/toolbar_open" layout="@layout/toolbar_open" /> <include android:id="@+id/toolbar_close" layout="@layout/toolbar_close" /> </android.support.v7.widget.Toolbar> </android.support.design.widget.CollapsingToolbarLayout> 三个小的布局代码我就贴一个做栗子: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@color/colorPrimary"> <ImageView android:id="@+id/iv_first" android:layout_width="@dimen/twenty_four_dp" android:layout_height="@dimen/twenty_four_dp" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_centerVertical="true" android:layout_marginLeft="@dimen/twenty_dp" android:layout_marginStart="@dimen/twenty_dp" android:contentDescription="@string/desc" android:src="@android:drawable/btn_star_big_on" /> <ImageView android:id="@+id/iv_second" android:layout_width="@dimen/twenty_four_dp" android:contentDescription="@string/desc" android:layout_height="@dimen/twenty_four_dp" android:layout_centerVertical="true" android:layout_marginLeft="@dimen/twenty_dp" android:layout_marginStart="@dimen/twenty_dp" android:layout_toEndOf="@+id/iv_first" android:layout_toRightOf="@+id/iv_first" android:src="@android:drawable/btn_star_big_on" /> <ImageView android:id="@+id/iv_third" android:layout_width="@dimen/twenty_four_dp" android:layout_height="@dimen/twenty_four_dp" android:contentDescription="@string/desc" android:layout_centerVertical="true" android:layout_marginLeft="@dimen/twenty_dp" android:layout_marginStart="@dimen/twenty_dp" android:layout_toEndOf="@+id/iv_second" android:layout_toRightOf="@+id/iv_second" android:src="@android:drawable/btn_star_big_on" /> <ImageView android:id="@+id/iv_forth" android:layout_width="@dimen/twenty_four_dp" android:contentDescription="@string/desc" android:layout_height="@dimen/twenty_four_dp" android:layout_centerVertical="true" android:layout_marginLeft="@dimen/twenty_dp" android:layout_marginStart="@dimen/twenty_dp" android:layout_toEndOf="@+id/iv_third" android:layout_toRightOf="@+id/iv_third" android:src="@android:drawable/btn_star_big_on" /> <ImageView android:layout_width="@dimen/twenty_four_dp" android:layout_height="@dimen/twenty_four_dp" android:layout_centerVertical="true" android:layout_marginLeft="@dimen/twenty_dp" android:contentDescription="@string/desc" android:layout_marginStart="@dimen/twenty_dp" android:layout_toEndOf="@+id/iv_forth" android:layout_toRightOf="@+id/iv_forth" android:src="@android:drawable/btn_star_big_on" /> <View android:id="@+id/toolbar_close_mask" android:layout_width="match_parent" android:layout_height="match_parent" /> </RelativeLayout> 要点在最后的View, 这是一个遮罩, 依据滚动幅度变化其透明度起到遮罩效果. AppBarLayout.OnOffsetChangedListener 官方文档写的很简单, 使用起来也不难. 添加implements AppBarLayout.OnOffsetChangedListener. 实现public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset)方法. @Override public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) { int offset = Math.abs(verticalOffset); int scrollRange = appBarLayout.getTotalScrollRange(); if (offset <= scrollRange / 2) { mToolbarOpen.setVisibility(View.VISIBLE); mToolbarClose.setVisibility(View.GONE); float openPer = (float) offset / (scrollRange / 2); int openAlpha = (int) (255 * openPer); mToolbarOpenMask.setBackgroundColor(Color.argb(openAlpha, 48, 63, 159)); } else { mToolbarClose.setVisibility(View.VISIBLE); mToolbarOpen.setVisibility(View.GONE); float closePer = (float) (scrollRange - offset) / (scrollRange / 2); int closeAlpha = (int) (255 * closePer); mToolbarCloseMask.setBackgroundColor(Color.argb(closeAlpha, 48, 63, 159)); } float per = (float) offset / scrollRange; int alpha = (int) (255 * per); mContentMask.setBackgroundColor(Color.argb(alpha, 48, 63, 159)); } 前面也说了, 就是变化遮罩透明度, 这个颜色是对应了布局中设置的颜色的, 否则过渡效果就不对了. 可以用下PS将colors.xml中6位颜色变成rgb填入. 最后 看到这里也很不容易啦(手动比心). 喜欢记得点赞, 有意见或者建议评论区见, 暗中关注我也是可以的哦~ 顺带一提, 腾讯云+社区也将同步我的文章了, 目前还在审核中:我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=cji0h4jns3lw github传送门
目录 前言 OpenEmu简介 OpenEmu实际体验 OpenEmu操作 最后 前言 这次的文章和以往不太一样, 不谈技术, 来谈谈情怀. 记得那是我小学二三年级的时候吧, 我妈给我买了一个Game Boy, 价格记不清了, 反正不贵, 而且是黑白的那种, 应该就是初代Game Boy吧. 然后还配了一张口袋妖怪青的卡. 在那个魔域啊, 传奇啊, 或者什么类似网游盛行的年代, 我被这台Game Boy带上了另外一条不归路(手动无奈). 而且要知道, 就算有些Steam游戏有Mac版, 大多是兼容的, 体验不如4k商务本, 当然除了15寸带A卡版, 但是绝大多数Mac是没有独显的. 所以干脆换个角度, 曲线娱乐. 更新了操作部分 OpenEmu简介 OpenEmu官网OpenEmu github主页 那今天, 我要推荐的这款软件可就厉害了, 看看这张列表, 你能认全且都见过, 那真是实打实骨灰级玩家了, 要知道乔布斯年轻时候可是在雅达利任过职的, 这上面挂着的几个雅达利主机我真是只在传说中听过. 除此之外, 更奇怪的是它只适用于mac, 要知道mac没有像DirectX那样的驱动的, 基本用的OpenGL, 所以mac游戏性能差是多方面的原因, 不过对付下任天堂游戏还是妥的. 模拟器列表 OpenEmu使用 来说说使用, 拿精灵宝可梦心金为例, 这本来是Game Boy上的口袋妖怪金, 在nds上使用新的游戏引擎进行了复刻, 虽然是冷饭, 作为一个曾经几百上千小时的老玩家, 我想说, 真香. 打开google, 输入nds 精灵宝可梦心金 rom这三个关键词你很容易就找到对应资源了. 下载解压, 你会找到一个.nds的文件, 将它拖入到nds标签下的游戏库即可. 然后双击就可以玩了. 开始游戏 说句实话, 比在真机上玩还要爽. 甚至它可以帮助你快速连接和修改手柄, 设置按键 设置按键 不过还是需要先装下手柄驱动的. 我的是北通和微软的手柄, 如果你是大法的, 就自己咕果一下吧. 驱动 从兼容xbox360的北通到微软比较新的xbox one s蓝牙手柄, 都能快速链接. 而且用键盘也行, 不爱折腾就别折腾呗(手动滑稽). 我这里上几张我的外设图: 外设展示 外设展示 外设展示 外设展示 说个很搞笑的事情, win7无论如何都无法通过蓝牙连上xobx one s手柄, 必须要用线才能维持的了生活(手动滑稽). OpenEmu实际体验 那么我们就可以开始游戏体验体验, 它能保持完美形象吗? 我在玩Game Boy Advance(以下简称GBA)游戏, 和大法PSP游戏的时候卡死过, 系统并没有死, 就是游戏死了. 在玩nds的时候出现过人物花掉, 而且十分频繁. 但是比起游戏死掉, 注意, 这里如果没有手动保存, 模拟器的自动保存也会崩溃. 人物花了只要重新进入某个房间刷新视图就会恢复, 算是可以忍受的bug. nds并没有出现过死掉的情况. 人物花屏 所以模拟器并没有很完美, 但是瑕不掩瑜, 依旧可以畅玩很久. 只要养成手动保存的好习惯. 再说说画质, 任天堂的游戏是没什么太高硬件要求的, 索尼的psp可是有很多ps3游戏的便携版的, 那体验如何, 我只能说, 放弃这个psp标签吧, 画质渣到几乎无法忍受, 而且这种渣可能源自psp游戏便携版本身, 因为psp的屏幕很小, 大法肯定也有底层优化, 但是放到Mac上, 怎么都没法看着舒服了. 就比如小岛老师的合金装备硬生生变成了育碧出品的画质. 怪物猎人也就cg能看, 唯一像样的就是像女神异闻录这样的游戏, 像我正在体验的女神异闻录3便携版, 整体体验还是可以的, 可以认为类似的JRPG都是可以的. 但是很遗憾psp的主要卖点就是动作游戏. 所以除了怀旧的GBA游戏, nds游戏是比较棒的选择了, 当然还有很多主机我真不认识, 想来佳作也有限. OpenEmu操作 在体验了一段时间之后, 发现需要加一段操作部分. OpenEmu提供实时存储, 所以可以不去管游戏自身的存储, 但是随着游戏的进行, 存储文档会原来越多, 它是不覆盖的, 那这里就教大家如何手动整理存档. 进入到软件目录: 软件目录 如果你要删除模拟器的实时存档, 进入Save States目录, 找到对应模拟器->对应游戏, 删除多余存档: 模拟器存档目录 如果你要删除游戏自身存档, 就有多个地方了, mGBA是GBA游戏存档; 而DeSmuME目录下放着nds游戏存档. GBA游戏自带存档目录 nds游戏自带存档目录 模拟器提供了截图快捷键, command+T, 就在Screenshots目录下, 你可以使用指令打开图形界面的目录, 浏览截图: open . 截图目录 图形界面浏览 最后 一直到今天我都依旧痴迷主机游戏而不是手游页游或者网游. 当然了, 如果不是2014年的主机解禁令, 到现在你可能都只能用用任天堂的掌上游戏主机而不是像现在这样索尼微软会提供专门的国行主机, 并且鼎力支持国内优秀开发者. 大家也知道最近某鹅因为游戏收益不达标导致股价大跌. 我一直不看好某鹅的游戏, 不管是代理的还是自研的. 原因嘛, 吸金强无敌, 但是真谈不上什么游戏性. 我不是针对某款游戏, 我是说在座全部的鹅厂游戏. 好了, 喜欢记得点赞, 有意见或者建议评论区见, 暗中关注我也是可以的哦~
目录 前言 基础使用 分隔线 点击监听 搭配CardView 更丰富的条目 增删条目 快速添加视图 让RecyclerView支持复杂视图 最后 前言 RecyclerView在Android界面开发当中是很重要的, 那掌握它也是很必要的. 但是有些时候会觉得它很厚重, 这里就从RecyclerView的基础一直说到扩展, 让你把RecyclerView学薄了. RecyclerView官方文档也是非常厚重. 这篇文章融合了自己原来的多篇文章, 并进行了修正和改进, 而且添加了很多很有趣的内容. 本文需要20分钟以上的阅读时间, 请合理安排. 多图预警, 转载请注明出处! 基础使用 要使用RecyclerView在Android Studio 2.x(以下简称AS), 要这样: compile 'com.android.support:cardview-v7:25.3.1' compile 'com.android.support:recyclerview-v7:25.3.1' 到了AS 3.x, 要这样: implementation 'com.android.support:cardview-v7:26.1.0' implementation 'com.android.support:recyclerview-v7:26.1.0' 之后在布局文件中写入如下代码就引入了RecyclerView了. <android.support.v7.widget.RecyclerView android:id="@+id/rv_main" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical" /> 接下来说说介绍些各种布局. 可以看RecyclerView.LayoutManager官方文档. 布局类 效果 LinearLayoutManager 以垂直或水平滚动列表方式显示项目 GridLayoutManager 在网格中显示项目 StaggeredGridLayoutManager 在分散对齐网格中显示项目 mRvMain = (RecyclerView) findViewById(R.id.rv_main); // 设置布局 LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); mRvMain.setLayoutManager(linearLayoutManager); 最关键的还是适配器的撰写. 但是理解起来不是很难, 你只要将ListView的适配器写法带入理解就好. 这里把全部代码贴出来, 因为后面要在这个基础上不断扩充. public class MyRVAdapter2 extends RecyclerView.Adapter<MyRVAdapter2.MyTVHolder> { private final LayoutInflater mLayoutInflater; private final Context mContext; private final ArrayList<String> mData; public MyRVAdapter2(Context context) { mLayoutInflater = LayoutInflater.from(context); mContext = context; mData = new ArrayList<>(); for (int i = 0; i < 40; i++) { mData.add("hello " + i); } } @Override public MyRVAdapter2.MyTVHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new MyRVAdapter2.MyTVHolder(mLayoutInflater.inflate(R.layout.rv_txt_item, parent, false)); } @Override public void onBindViewHolder(final MyRVAdapter2.MyTVHolder holder, int pos) { holder.mTextView.setText(mData.get(pos)); } @Override public int getItemCount() { return mData == null ? 0 : mData.size(); } class MyTVHolder extends RecyclerView.ViewHolder { TextView mTextView; MyTVHolder(View itemView) { super(itemView); mTextView = (TextView) itemView.findViewById(R.id.tv_txt); } } } 然后写个最基础的TextView条目. 让它跑起来看看效果. <?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="wrap_content" android:orientation="horizontal"> <TextView android:id="@+id/tv_txt" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" android:padding="@dimen/eight_dp" android:text="@string/tmp" android:textSize="@dimen/thirty_sp" /> </LinearLayout> 基础 分隔线 前面的部分已经是基础的RecyclerView使用了. 那比起ListView是不是没有了分隔线. 这里上一个简单好用的开源库RecyclerView-FlexibleDivider. 引入: implementation 'com.yqritc:recyclerview-flexibledivider:1.4.0' 使用: mRvMain.addItemDecoration( new HorizontalDividerItemDecoration.Builder(this).build()); 看效果就达到了吧. 分隔线 觉得不好看, 还可以自定义, 更多写法可以参见文档内容. mRvMain.addItemDecoration( new HorizontalDividerItemDecoration.Builder(this) .color(Color.BLUE) .sizeResId(R.dimen.two_dp) .marginResId(R.dimen.eight_dp, R.dimen.eight_dp) .build()); 自定义分隔线 而且而且, 竖着的分隔线也大丈夫哦. GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 2); mRvMain.setLayoutManager(gridLayoutManager); mRvMain.addItemDecoration( new VerticalDividerItemDecoration.Builder(this).build()); 竖线 点击监听 再回忆一下在天国的ListView, 还有item的点击吧, 这个也要自己写. 适配器中: public interface OnItemClickListener { void onItemClick(View view, int position); void onItemLongClick(View view, int position); } private MyRVAdapter2.OnItemClickListener mOnItemClickListener; public void setOnItemClickListener(MyRVAdapter2.OnItemClickListener mOnItemClickListener) { this.mOnItemClickListener = mOnItemClickListener; } onBindViewHolder中设置点击监听. @Override public void onBindViewHolder(final MyRVAdapter2.MyTVHolder holder, int pos) { holder.mTextView.setText(mData.get(pos)); if (mOnItemClickListener != null) { holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int pos = holder.getLayoutPosition(); mOnItemClickListener.onItemClick(holder.itemView, pos); } }); holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { int pos = holder.getLayoutPosition(); mOnItemClickListener.onItemLongClick(holder.itemView, pos); return false; } }); } } 使用监听: mAdapter.setOnItemClickListener(new MyRVAdapter2.OnItemClickListener() { @Override public void onItemClick(View view, int position) { Toast.makeText(UIUtil.getContext(), "click" + position, Toast.LENGTH_SHORT).show(); } @Override public void onItemLongClick(View view, int position) { Toast.makeText(UIUtil.getContext(), "long click" + position, Toast.LENGTH_SHORT).show(); } }); 点击 搭配CardView 是不是这个点击看着没啥感觉, 没事, 我们换上CardView再来一次. 布局文件: <?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:card_view="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="@dimen/eight_dp" android:foreground="@drawable/card_foreground" card_view:cardCornerRadius="@dimen/four_dp"> <TextView android:id="@+id/tv_txt" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" android:padding="@dimen/eight_dp" android:text="@string/tmp" android:textSize="@dimen/thirty_sp" /> </android.support.v7.widget.CardView> cardview 给CardView加上水波纹点击特效: <?xml version="1.0" encoding="utf-8"?> <ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/colorPrimary" /> 波纹点击 在老版本就只能用选择器了, 其实效果也还好: <?xml version="1.0" encoding="utf-8"?> <inset xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/card_foreground_selector" android:insetBottom="@dimen/four_dp" android:insetLeft="@dimen/three_dp" android:insetRight="@dimen/three_dp" android:insetTop="@dimen/four_dp" /> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true"> <shape android:shape="rectangle"> <solid android:color="@color/colorPrimaryTran" /> <corners android:radius="@dimen/four_dp" /> </shape> </item> <item android:state_enabled="true" android:state_focused="true"> <shape android:shape="rectangle"> <solid android:color="#0f000000" /> <corners android:radius="@dimen/four_dp" /> </shape> </item> </selector> 低版本兼容 更丰富的条目 大家应该都知道TextView可以设置图标吧, 这里来看下效果图, 顺带感受下android界面设计语言的变化. <?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="wrap_content" android:orientation="horizontal"> <TextView android:id="@+id/tv_txt" android:layout_width="match_parent" android:layout_height="wrap_content" android:drawableLeft="@mipmap/ic_launcher" android:drawablePadding="@dimen/sixteen_dp" android:drawableStart="@mipmap/ic_launcher" android:gravity="center_vertical" android:padding="@dimen/eight_dp" android:text="@string/tmp" android:textSize="@dimen/thirty_sp" /> </LinearLayout> 4.x 8.x 让GridLayoutManager展示不同宽度的条目 方的是4.x上的, 圆的是8.x上的, 可以看到, 变化还是很大的. 我们回正题. GridLayoutManager布局是可以设置宽度的, 不一定都是一样大的, 来看下实现. // 指定item宽度 gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { if (position == 0 || position == (mAdapter.getItemCount() - 1) / 2 || position == (mAdapter.getItemCount() - 1)) { return gridLayoutManager.getSpanCount(); } else { return 1; } } }); 来看效果图, 发现我们的分隔线崩了是吧, 如果真想用这个分隔线也还是要自己动手修补修补, 改动改动, 开源库再棒也猜不到你的项目需求呀. 分隔线异常 设置宽度 当然了, 我还是很喜欢这个分隔线的, 我们来看看横着滚动的效果. 布局文件要改动: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="match_parent" android:orientation="horizontal"> <TextView android:id="@+id/tv_txt" android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="center_vertical" android:text="@string/tmp" android:textSize="@dimen/thirty_sp" /> </LinearLayout> gridLayoutManager.setOrientation(GridLayoutManager.HORIZONTAL); 横滑 展示不同布局 之前变化宽度其实还是相同条目, 现在要展示不同条目: 写一个图的条目: <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="@dimen/eight_dp"> <ImageView android:id="@+id/iv_img" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:src="@mipmap/ic_launcher" /> </RelativeLayout> public enum ITEM_TYPE { ITEM_TYPE_IMAGE, ITEM_TYPE_TEXT } 这里多了判断条目类型, 还要注意返回值的变化, 用了更基类的RecyclerView.ViewHolder. @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == ITEM_TYPE.ITEM_TYPE_IMAGE.ordinal()) { return new MyRVAdapter2.MyIVHolder(mLayoutInflater.inflate(R.layout.rv_img_item, parent, false)); } else { return new MyRVAdapter2.MyTVHolder(mLayoutInflater.inflate(R.layout.rv_txt_item, parent, false)); } } 类继承上面也要变成RecyclerView.ViewHolder, 这些都是要对应的. extends RecyclerView.Adapter<RecyclerView.ViewHolder> 当然了, holder也是不能少的. public class MyIVHolder extends RecyclerView.ViewHolder { ImageView mImageView; MyIVHolder(View view) { super(view); mImageView = (ImageView) view.findViewById(R.id.iv_img); } } @Override public void onBindViewHolder(final RecyclerView.ViewHolder holder, int pos) { if (holder instanceof MyRVAdapter2.MyTVHolder) { ((MyRVAdapter2.MyTVHolder) holder).mTextView.setText(mData.get(pos)); } else if (holder instanceof MyRVAdapter2.MyIVHolder) { ((MyRVAdapter2.MyIVHolder) holder).mImageView.setImageDrawable(UIUtil.getDrawable(R.mipmap.ic_launcher)); } // 点击监听 ... } 顺带的, 我们把之前放宽的条目变成不同的视图, 也就是对应起来: @Override public int getItemViewType(int position) { if (position == 0 || position == (getItemCount() - 1) / 2 || position == (getItemCount() - 1)) { return ITEM_TYPE.ITEM_TYPE_IMAGE.ordinal(); } else { return ITEM_TYPE.ITEM_TYPE_TEXT.ordinal(); } } 看看效果: 不同布局 它还能继续地复杂, 试试瀑布流StaggeredGridLayoutManager: <?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:card_view="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="@dimen/eight_dp" card_view:cardCornerRadius="@dimen/four_dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <ImageView android:id="@+id/iv_img" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:src="@mipmap/ic_launcher" /> <TextView android:id="@+id/tv_txt" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="@string/tmp" android:textSize="@dimen/thirty_sp" /> </LinearLayout> </android.support.v7.widget.CardView> StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL); mRvMain.setLayoutManager(staggeredGridLayoutManager); 瀑布流 分割线又崩了, 嘿嘿, 其实用上了CardView, 分割线没什么必要再用了. 分隔线异常 增删条目 现在适配器中添加增删方法: public void addData(int position) { mData.add(position, "hello x"); notifyItemInserted(position); } public void removeData(int position) { mData.remove(position); notifyItemRemoved(position); } 再写入点击事件中, 点击增加, 长按删除: mAdapter.setOnItemClickListener(new MyRVAdapter2.OnItemClickListener() { @Override public void onItemClick(View view, int position) { mAdapter.addData(position); } @Override public void onItemLongClick(View view, int position) { mAdapter.removeData(position); } }); 增删条目 增删条目开源库 这里再上一个开源库recyclerview-animators, 可以修改增删动画, 种类也很丰富, 还能在它基础上自定义: 分类 动画类名 Cool LandingAnimator Scale ScaleInAnimator, ScaleInTopAnimator, ScaleInBottomAnimator, ScaleInLeftAnimator, ScaleInRightAnimator Fade FadeInAnimator, FadeInDownAnimator, FadeInUpAnimator, FadeInLeftAnimator, FadeInRightAnimator Flip FlipInTopXAnimator, FlipInBottomXAnimator, FlipInLeftYAnimator, FlipInRightYAnimator Slide SlideInLeftAnimator, SlideInRightAnimator, OvershootInLeftAnimator, OvershootInRightAnimator, SlideInUpAnimator, SlideInDownAnimator 引入: implementation 'jp.wasabeef:recyclerview-animators:2.3.0' 使用: mRvMain.setItemAnimator(new SlideInLeftAnimator()); 这里给大家展示两种效果, 其它的自己尝试吧. 增删动画 mRvMain.setItemAnimator(new LandingAnimator()); 增删动画 快速添加视图 还有像Header, Foot这样的视图, 自己写也还是要费些功夫的, 这里推荐Android大神的库baseAdapter 引入: implementation 'com.zhy:base-rvadapter:3.0.3' 添加头尾视图 HeaderAndFooterWrapper mHeaderAndFooterWrapper = new HeaderAndFooterWrapper(mAdapter); TextView t1 = new TextView(this); t1.setText("Header 1"); t1.setTextSize(30); TextView t2 = new TextView(this); t2.setText("Foot 1"); t2.setTextSize(30); mHeaderAndFooterWrapper.addHeaderView(t1); mHeaderAndFooterWrapper.addFootView(t2); mRvMain.setAdapter(mHeaderAndFooterWrapper); 头尾 添加更多视图 LoadMoreWrapper mLoadMoreWrapper = new LoadMoreWrapper(mAdapter); mLoadMoreWrapper.setLoadMoreView(R.layout.rv_cv_img_txt_item); mLoadMoreWrapper.setOnLoadMoreListener(new LoadMoreWrapper.OnLoadMoreListener() { @Override public void onLoadMoreRequested() { } }); mRvMain.setAdapter(mLoadMoreWrapper); 更多 是不是感觉特别爽, 那看看更爽的, 在不写适配器的情况下快速添加条目: final ArrayList<String> mData = new ArrayList<>(); for (int i = 0; i < 40; i++) { mData.add("hello " + i); } mRvMain.setAdapter(new CommonAdapter<String>(this, R.layout.rv_cv_txt_item, mData) { @Override protected void convert(ViewHolder holder, String s, int position) { holder.setText(R.id.tv_txt, mData.get(position)); } }); 快速添加条目 是不是感觉省了一万个小时呢. 让RecyclerView支持复杂视图 每次加入新的视图都要对适配器进行比较大程度的改动, 这样是很容易出错的. 这里引入一个非常棒的开源库-AdapterDelegates, 降低下代码耦合性. 引入: implementation 'com.hannesdorfmann:adapterdelegates3:3.0.1' 先不说使用细节, 来看看实现后想加入不同视图有多简单吧: ArrayList<Base> data = new ArrayList<>(); for (int i = 0; i < 10; i++) { data.add(new B("b " + i)); } for (int i = 0; i < 10; i++) { data.add(new A("a " + i)); } BaseAdapter animalAdapter = new BaseAdapter(this, data); mRvMain.setAdapter(animalAdapter); 添加复杂条目 是不是惊了, 也就是说, 你只要实现了A, B这些视图类, 直接新建放入数组就完事了. 需要写基础适配器: public class BaseAdapter extends RecyclerView.Adapter { private AdapterDelegatesManager<List<Base>> delegatesManager; private List<Base> items; public BaseAdapter(Activity activity, List<Base> items) { this.items = items; delegatesManager = new AdapterDelegatesManager<>(); delegatesManager.addDelegate(new AAdapterDelegate(activity)) .addDelegate(new BAdapterDelegate(activity)); } @Override public int getItemViewType(int position) { return delegatesManager.getItemViewType(items, position); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return delegatesManager.onCreateViewHolder(parent, viewType); } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { delegatesManager.onBindViewHolder(items, position, holder); } @Override public int getItemCount() { return items.size(); } } 需要对每个类进行进行具体设置, 这里以A为例. public class AAdapterDelegate extends AdapterDelegate<List<Base>> { private LayoutInflater inflater; public AAdapterDelegate(Activity activity) { inflater = activity.getLayoutInflater(); } @Override public boolean isForViewType(@NonNull List<Base> items, int position) { return items.get(position) instanceof A; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) { return new CatViewHolder(inflater.inflate(R.layout.rv_cv_img_txt_item, parent, false)); } @Override public void onBindViewHolder(@NonNull List<Base> items, int position, @NonNull RecyclerView.ViewHolder holder, @Nullable List<Object> payloads) { CatViewHolder vh = (CatViewHolder) holder; A cat = (A) items.get(position); vh.name.setText(cat.getName()); } static class CatViewHolder extends RecyclerView.ViewHolder { public TextView name; public ImageView img; public CatViewHolder(View itemView) { super(itemView); name = (TextView) itemView.findViewById(R.id.tv_txt); img = (ImageView) itemView.findViewById(R.id.iv_img); } } } 最后 看完这篇应该是对RecyclerView有个大体认识了, 多练习练习就会得心应手起来了. 那还是有一点, 就像分隔线库的几次不理想表现, 具体项目要求还是要具体对待, 开源库也不是万能的. 最近不是又有什么开源项目套壳事件了嘛, 别人一开源就说自己有自主产权了真的好吗? 喜欢记得点赞或者关注我哦, 有意见或者建议评论区见~
界面无小事(一):RecyclerView+CardView了解一下界面无小事(二):让RecyclerView展示更多不同视图界面无小事(三):用RecyclerView + Toolbar做个文件选择器界面无小事(四):来写个滚动选择器吧!界面无小事(五):自定义TextView界面无小事(六):来做个好看得侧拉菜单!界面无小事(七):使用代码动态增删布局 目录 前言 增删item 自定义增删动画 最后 前言 之前写过一篇代码动态增删布局的, 对比下这次的RecyclerView增删item, 说句实话, 代码动态增删布局基本可以退群了. 增删item 当然首先你可以按照第一篇-界面无小事(一): RecyclerView+CardView了解一下建立基础的RecyclerView. 这次的关键是在适配器代码中加入增删item的操作. 要注意一点, 刷新和原来在ListView的操作是不一样的. 你可以直接看官方文档, 大致有这几个: notifyItemInserted() notifyItemRemoved() notifyItemMoved() notifyItemChanged() 在适配器中加入如下代码: public void addData(int position) { mData.add(position, "hello python"); notifyItemInserted(position); } public void removeData(int position) { mData.remove(position); notifyItemRemoved(position); } 然后我们在toolbar中加上add和del按钮, 对应这两个方法. <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/add" android:icon="@mipmap/ic_launcher" android:title="@string/add" app:showAsAction="never" /> <item android:id="@+id/del" android:icon="@mipmap/ic_launcher" android:title="@string/del" app:showAsAction="never" /> </menu> 并且设置长按为删除操作, 点击是增加操作. 好了, 上效果图: 效果图 不单单是我演示的这种布局, 还有线型的, 横向的, 瀑布流都可以有这种类似效果. 可参考界面无小事(二): 让RecyclerView展示更多不同视图. 那既然有默认动画, 肯定就能自定义动画了. 在这之前, 先说说几个方法. 用来设置动画的具体状态的持续时间. rvTest.getItemAnimator().setAddDuration(400); rvTest.getItemAnimator().setRemoveDuration(400); rvTest.getItemAnimator().setMoveDuration(400); rvTest.getItemAnimator().setChangeDuration(400); 自定义增删动画 这里借助开源项目. 因为这个项目真的足够地棒. 自带动画个数都已经足够用了, 见下. 你还可以继续在这基础上自定义. 是不是感觉三生万物了. Animators 分类 动画类名 Cool LandingAnimator Scale ScaleInAnimator, ScaleInTopAnimator, ScaleInBottomAnimator, ScaleInLeftAnimator, ScaleInRightAnimator Fade FadeInAnimator, FadeInDownAnimator, FadeInUpAnimator, FadeInLeftAnimator, FadeInRightAnimator Flip FlipInTopXAnimator, FlipInBottomXAnimator, FlipInLeftYAnimator, FlipInRightYAnimator Slide SlideInLeftAnimator, SlideInRightAnimator, OvershootInLeftAnimator, OvershootInRightAnimator, SlideInUpAnimator, SlideInDownAnimator 而且非常难得的是使用方法还很简单, 需要几个步骤 在Module的build.gradle中写入 dependencies { implementation 'jp.wasabeef:recyclerview-animators:2.3.0' } 在Project的build.gradle中写入 repositories { google() jcenter() } 具体使用部分只要用动画类名替换之前默认的动画类名即可. 例如: rvTest.setAdapter(mAdapter); rvTest.setItemAnimator(new SlideInLeftAnimator()); 好了, 接下来看几个效果图吧: SlideInLeftAnimator ScaleInAnimator LandingAnimator 最后 喜欢记得点赞哦, 有意见或者建议评论区见, 暗中关注我也是可以的.
Android小知识10则(上)Android小知识10则(下)Android用5种方式实现自定义计时器, 哪种才是你的菜?github传送门 目录 前言 Chronometer的使用 CountDownTimer的使用 最后 前言 之前在Android用5种方式实现自定义计时器, 哪种才是你的菜?的文章中我提到了Chronometer和CountDownTimer计时器, 但是很奇怪, 好像被忽略了, 所以这次单独拎出来发一次好了. Android也是提供了计时器的, 虽然功能比较简单, 但是有些场景下也还是够用的...吗?(手动滑稽) CountDownTimer是倒计时计时器. Chronometer的话, 看怎么用了, 正着倒着都行...吗?(再次滑稽) Chronometer的使用 礼貌性给下官方文档. 然后上效果图: Chronometer的使用 mTimer.setBase(-60000 + SystemClock.elapsedRealtime()); mTimer.setCountDown(false); mTimer.start(); 我们以+1m(也就是从1分钟开始计时)为例: 先看xml代码, android:format="%s"是要点, 后面会说. 然后它继承自TextView, 属性设置什么的就很简单了: <Chronometer android:id="@+id/timer" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_margin="@dimen/sixteen_dp" android:format="%s" android:textColor="@android:color/darker_gray" android:textSize="@dimen/thirty_sp" /> (-60000 + SystemClock.elapsedRealtime())的出现会让你一下子懵了, 所以先说SystemClock.elapsedRealtime(). Chronometer实例是需要设置基线的, 然后用SystemClock.elapsedRealtime()减去你设置的基线值, 换句话说, 如果你写mTimer.setBase(SystemClock.elapsedRealtime());就意味着从零开始. 然后单位是ms, 一分钟就是60000ms, 所以想从一分钟开始就是(-60000 + SystemClock.elapsedRealtime())了. mTimer.setCountDown();代表是倒计时还是正常计时, false就是正常计时, true计时倒计时. 你可能会提问, 为什么我没有格式化字符串它也正常显示了. 看xml中的android:format="%s", 这就是代表用默认的格式. 官方文档有这么一段: By default it will display the current timer value in the form "MM:SS" or "H:MM:SS", or you can use setFormat(String) to format the timer value into an arbitrary string. 也就是说默认"MM:SS", 超过1小时"H:MM:SS", 你可以用setFormat(String)设置你的style儿(手动滑稽). 然后mTimer.start();是开始. mTimer.stop();是停止. 这很好理解了. 也许你会觉得它还挺好用, 但事实很残酷, 倒计时的功能要7.0才能使用, 其它的倒是兼容低版本, 但是废了一半了不是. 但是配合CountDownTimer, 意外地解决了麻烦. CountDownTimer的使用 效果图: CountDownTimer的使用 这个倒计时类异常好用. 构造函数第一个参数是总时长, 第二个是间隔. onTick是每次变化要执行的动作, onFinish是结束后要执行的动作. mCountDownTimer.start();是开始. mCountDownTimer.cancel();是停止. 完事了, 就这么多内容, 不信去看看官方文档. private CountDownTimer mCountDownTimer = new CountDownTimer(10000, 1000) { @Override public void onTick(long millisUntilFinished) { String str = "剩余" + (millisUntilFinished / 1000) + "秒"; mTvTime.setText(str); } @Override public void onFinish() { mTvTime.setEnabled(true); mTvTime.setText("倒计时结束"); } }; 最后 喜欢记得点赞哦, 有意见或者建议评论区见, 暗中关注我也是可以的~
Android绘制(一):来用shape绘出想要的图形吧!Android绘制(二):来用Path绘出想要的图形吧! 目录 效果图 前言 绘制 属性动画 最后 效果图 不废话, 直接上效果图, 感兴趣再看下去. 其实不单单是效果图演示的, 运用熟练的话各种图标之间都是可以切换的. 暂停到终止 暂停到播放 前言 之前的文章也说了, path还是很有潜力的. 但是很遗憾, 我本人不太擅长用贝塞尔曲线画东西, 所以只能演示一些简单的变化(手动无奈). 来看看是如何实现的吧. 绘制 想要绘制矩形很简单啦, 移动到左上角, 然后逆时针画一圈, 或者顺时针画一圈. 那其实暂停和终止就是两个矩形, 播放就是两个三角形. 所以稍微改变下path绘制的位置就解决问题啦. mLPath.moveTo(left, top); mLPath.lineTo(left, bottom); mLPath.lineTo(right, bottom); mLPath.lineTo(right, top); mLPath.close(); 但是需要弄清绘制区域. 首先要测出设定视图宽高, 再以此画一个圆, 然后设置一个内边距, 然后再绘制图标. 来张图: 绘制 属性动画 其实这里的属性动画的使用部分是最简单的使用, 就是值变化, 从0到1或者从1到0. ValueAnimator valueAnimator = ValueAnimator.ofFloat(isPlaying ? 1 : 0, isPlaying ? 0 : 1); valueAnimator.setDuration(mAnimDuration); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mProgress = (float) animation.getAnimatedValue(); invalidate(); } }); 然后依据mProgress的值变化图形. 那要怎么构建这个变化呢? 注意看我的注释部分, 变化的值就那几个, 你将0时的值和1时的值先写好, 然后推算函数, 就是初中数学的难度, 大概(天知道现在小学生变成什么样了)(手动滑稽). // 暂停间距(0: mMidSpace 1: 0) float pauseDis = mMidSpace * (1 - mProgress); // 暂停单条宽(0: mRectWidth / 3 1: mRectWidth / 2) float pauseWidth = (mRectWidth - pauseDis) / 2; // 左暂停左上(0: 0 1: mRectWidth / 2) float pauseLLT = pauseWidth * mProgress; // 右暂停左上(0: mRectWidth / 3 * 2 1: mRectWidth / 2) float pauseRLT = pauseWidth + pauseDis; // 右暂停右上(0: mRectWidth 1: mRectWidth) float pauseRRT = pauseWidth * 2 + pauseDis; // 右暂停右下(0: mRectWidth 1: mRectWidth / 2) float pauseRRB = pauseRRT - pauseWidth * mProgress; 最后 之后实现应该都不太难了, 不管是监听还是自定义属性. 那自定义视图可以看这篇-界面无小事(五):自定义TextView, 属性动画可以看这篇-动画必须有(一): 属性动画浅谈. 当然了, 要是前一篇没看的, 建议看下Android绘制(二):来用Path绘出想要的图形吧!. 喜欢记得点赞哦, 有意见或者建议评论区见, 暗中关注我也是可以的哦~
Android绘制(一):来用shape绘出想要的图形吧! 目录 前言 绘制线 绘制图形 绘制弧 绘制文字 组合 贝塞尔曲线 最后 前言 之前有一篇用shape进行绘制的, 但是那个偏向静态, path结合属性动画可以动起来哦~ path是什么? 来看看官方的介绍: The Path class encapsulates compound (multiple contour) geometric paths consisting of straight line segments, quadratic curves, and cubic curves. It can be drawn with canvas.drawPath(path, paint), either filled or stroked (based on the paint's Style), or it can be used for clipping or to draw text on a path. 咕果翻译一下就是: Path类封装了由直线段,二次曲线和三次曲线组成的复合(多个轮廓)几何路径。 它可以使用canvas.drawPath(path,paint)绘制,填充或描边(基于绘制的样式),或者它可以用于剪切或在路径上绘制文本。 可能你更加一脸懵b了, 没事我们来看方法. 绘制线 来看段代码和效果图. 首先用moveTo移动点, lineTo绘制线到某个位置, rLineTo在当前基础上加上某数值. 然后用close闭环. mPath.moveTo(400, 400); mPath.lineTo(600, 400); mPath.lineTo(600, 600); mPath.rLineTo(-200, 0); mPath.close(); 绘制线 但是就是不想好好画矩形, 皮一下. setLastPoint将上一个变化lineTo(600, 600)的作用完全覆盖了, 导致了结果出现了变化. mPath.moveTo(400, 400); mPath.lineTo(600, 400); mPath.lineTo(600, 600); mPath.setLastPoint(400, 800); mPath.rLineTo(-200, 0); mPath.close(); 修改点位置 绘制图形 我们用同一个矩形来绘制矩形, 椭圆以及圆角矩形. 观察下圆的位置, 你会发现, 绘制圆的x和y是指圆心, 而不是左上角. mPath.addCircle(200, 400, 100, Path.Direction.CCW); // 绘制矩形 RectF rect = new RectF(200, 400, 800, 600); mPath.addRect(rect, Path.Direction.CW); // 绘制椭圆 mPath.addOval(rect, Path.Direction.CW); // 绘制圆角矩形 mPath.addRoundRect(rect, 60, 60, Path.Direction.CW); 绘制图形 绘制弧 绘制弧也是基于矩形, 万物基于矩形(手动滑稽). addArc的后两个参数就是度数, 从0到300. arcTo最后有个boolean类型参数, 设置成true见图一, 设置成false见图二. 看出来了吧, 就是moveTo和lineTo的差别, 说专业点就是是否要forceMoveTo. RectF rect = new RectF(200, 400, 800, 600); RectF rect2 = new RectF(200, 600, 800, 800); mPath.addRect(rect, Path.Direction.CW); mPath.addRect(rect2, Path.Direction.CW); mPath.addArc(rect, 0, 300); mPath.arcTo(rect2, 0, 90, true); true false 绘制文字 我们顺时针(Path.Direction.CW)绘制圆, 写在上面的文字也是顺时针. 逆时针(Path.Direction.CCW)绘制圆, 文字也是逆时针. 闭合方向 标志 顺时针 Path.Direction.CW 逆时针 Path.Direction.CCW mPath.addCircle(400, 400, 300, Path.Direction.CW); mPaint.setStrokeWidth(4f); mPaint.setTextSize(40f); canvas.drawTextOnPath(getResources().getString(R.string.test_str), mPath, 0, 0, mPaint); 顺时针 mPath.addCircle(400, 400, 300, Path.Direction.CCW); mPaint.setStrokeWidth(4f); mPaint.setTextSize(40f); canvas.drawTextOnPath(getResources().getString(R.string.test_str), mPath, 逆时针 组合 想将线或者图形组合起来, op方法满足你. 第二个参数是关键, 有 Path.Op.DIFFERENCE Path.Op.INTERSECT Path.Op.REVERSE_DIFFERENCE Path.Op.UNION Path.Op.XOR 标志 说明 DIFFERENCE 从第一个路径中减去第二个路径 INTERSECT 两条路径相交部分 REVERSE_DIFFERENCE 从第二条路径中减去第一条路径 UNION 联结两条路径 XOR 独立两条路径 我们按这个排列顺序来看. mPath.addCircle(400, 400, 200, Path.Direction.CCW); mPath2.addRect(500, 300, 700, 500, Path.Direction.CCW); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { mPath.op(mPath2, Path.Op.DIFFERENCE); } Path.Op.DIFFERENCE Path.Op.INTERSECT Path.Op.REVERSE_DIFFERENCE Path.Op.UNION 最后一个想看出效果需要将画笔改成FILL或者FILL_AND_STROKE. 这个可以理解成用并集减去交集剩下部分. 但是最好还是从Path来理解而不是从集合的角度. 图二是STROKE下效果. mPaint.setStyle(Paint.Style.FILL_AND_STROKE); Path.Op.XOR和FILL画笔 Path.Op.XOR和STROKE画笔 贝塞尔曲线 单点控 moveTo是第一个点, 如果不moveTo, 默认(0, 0). (500, 0)是控制点, (600, 400)是结束点. mPath.moveTo(400, 400); mPath.quadTo(500, 0, 600, 400); 单点控 双点控 (600, 800)是第二点控. mPath.moveTo(400, 400); mPath.cubicTo(500, 0, 600, 800, 700, 400); 双点控 最后 你可以看出Path是很有潜力的. 喜欢记得点赞哦, 有意见或者建议评论区见, 暗中关注我也是可以的~
Chronometer和CountDownTimer计时器github传送门 目录 效果图 前言 Timer + TimerTask + Handler Timer + TimerTask Handler Thread + Handler Handler + Runnable 最后 效果图 看下效果图, 这是五种不同的方式演示计时器. 当然不看源码是看不出差别的. 效果图 前言 这次的文章不知道能不能帮助大家, 但是对我自己的帮助还是蛮大的, 才知道自己原来用的方法不是最优而且也不是最简. 然后我之前有一篇文章是用官方控件和类实现的, 有兴趣可以看一下Chronometer和CountDownTimer计时器. Timer + TimerTask + Handler 在TimerTask实例的run方法中用Handler实例发送消息, 用Timer实例启动计时器, 从0ms开始, 间隔1000ms. case R.id.cv_start1: if (mTimer1 == null && mTask1 == null) { mTimer1 = new Timer(); mTask1 = new TimerTask() { @Override public void run() { Message message = mHandler.obtainMessage(1); mHandler.sendMessage(message); } }; mTimer1.schedule(mTask1, 0, 1000); } break; case R.id.cv_stop1: if (mTimer1 != null) { mTimer1.cancel(); mTimer1 = null; } if (mTask1 != null) { mTask1.cancel(); mTask1 = null; } break; Timer + TimerTask 比起第一种, 这种更Java, 但是可以少一个Handler实例. if (mTimer2 == null && mTask2 == null) { mTimer2 = new Timer(); mTask2 = new TimerTask() { @Override public void run() { runOnUiThread(new Runnable() { @Override public void run() { mTvTime2.setText(getTime()); } }); } }; mTimer2.schedule(mTask2, 0, 1000); } Handler 这种方法就直接告别Timer和TimerTask了, 通过Handler的发消息延迟sendMessageDelayed以及不同的消息内容(就是what值)解决问题. case R.id.cv_start3: { Message message = mHandler.obtainMessage(2); mHandler.sendMessage(message); } break; case R.id.cv_stop3: { Message message = mHandler.obtainMessage(3); mHandler.sendMessage(message); } case 2: { mTvTime3.setText(getTime()); this.removeMessages(2); Message message = this.obtainMessage(2); this.sendMessageDelayed(message, 1000); } break; case 3: this.removeMessages(2); break; Thread + Handler 老朋友Thread + Handler, 原来经常这么写, 现在不了(手动滑稽). /** * 启动线程 */ private void startThread() { if (mThread == null) { mThread = new MyThread(); mThread.start(); } } /** * 停止线程 */ private void stopThread() { if (mThread != null) { mThread.stop = true; mThread = null; Message message = mHandler.obtainMessage(5); mHandler.sendMessage(message); } } Handler + Runnable 最后一种我目前最喜欢. 你连what值都无需处理, 直接postDelayed设置时延, 然后交给实现Runnable接口的实例的run方法来做. case R.id.cv_start5: { if (mRunnable == null) { mRunnable = new MyRunnable(); mHandler.postDelayed(mRunnable, 0); } } break; case R.id.cv_stop5: { mHandler.removeCallbacks(mRunnable); mRunnable = null; } break; private class MyRunnable implements Runnable { @Override public void run() { mTvTime5.setText(getTime()); mHandler.postDelayed(this, 1000); } } 最后 总之一点, 用第三种Handler和第五种Handler + Runnable肯定比其它的消耗少得多, 所以很推荐. 或者你还有更棒的方法可以评论区告诉我下. 喜欢记得点赞, 暗中关注我也是可以的哦~
界面无小事(一):RecyclerView+CardView了解一下界面无小事(二):让RecyclerView展示更多不同视图界面无小事(三):用RecyclerView + Toolbar做个文件选择器界面无小事(四):来写个滚动选择器吧!界面无小事(五):自定义TextView界面无小事(六):来做个好看得侧拉菜单!github传送门 目录 效果图 前言 布局文件 实现 最后 效果图 不多废话, 先上图, 有兴趣再看下去: 效果图 前言 这篇是之前的一篇旧文改的, 也是想将这篇放入自己的界面无小事专题, 所以当成新篇章来写, 绝对不是为了什么日更之类的事情哦(手动滑稽)~. 布局文件 先来看看布局文件, 不是很复杂, 但是涉及到之后java部分的代码, 所以必须都贴出来. 不过你可以看下预览图就好: 布局预览图 <?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.so.moreview.ui.activity.MainActivity"> <LinearLayout android:id="@+id/ll_content" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="@dimen/eight_dp"> <LinearLayout android:id="@+id/ll_item" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/darker_gray" android:orientation="vertical" android:padding="@dimen/eight_dp" tools:ignore="UselessParent"> <EditText android:id="@+id/et_item" android:layout_width="match_parent" android:layout_height="@dimen/forty_dp" android:background="@android:color/white" android:gravity="center_vertical" android:inputType="textMultiLine" android:textSize="@dimen/sixteen_sp" tools:ignore="LabelFor" /> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/eight_dp"> <ImageButton android:id="@+id/ib_add" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentEnd="true" android:background="@drawable/add" android:contentDescription="" tools:ignore="ContentDescription" /> </RelativeLayout> </LinearLayout> </LinearLayout> </ScrollView> 实现 LinkedList<ImageButton> mAddList; mAddList.add(curView, btAdd); LinkedList<ImageButton> mDelList; mDelList.add(curView, btDel); 这里我使用LinkedList<ImageButton>实例存储ImageButton, 就是为了让增删的时候方便一些. 最关键的是增删按钮的代码: 添加条目 /** * @param v 添加一个新条目 */ private void addItem(View v) { if (v == null) { return; } // 1. 根据传入的v, 判断是mListAddBtn中的哪一个 int curView = -1; for (int i = 0; i < mAddList.size(); i++) { if (mAddList.get(i) == v) { curView = i; break; } } // 2. 根据获取的值添加控件 if (curView >= 0) { curView++; // ll_item LinearLayout ll = new LinearLayout(MainActivity.this); LinearLayout.LayoutParams llParams = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); llParams.setMargins(0, UIUtil.dp2px(8), 0, 0); ll.setLayoutParams(llParams); ll.setBackgroundColor(ContextCompat.getColor(this, android.R.color.darker_gray)); ll.setPadding(UIUtil.dp2px(8), UIUtil.dp2px(8), UIUtil.dp2px(8), UIUtil.dp2px(8)); ll.setOrientation(LinearLayout.VERTICAL); // et_item EditText et = new EditText(MainActivity.this); LinearLayout.LayoutParams etParams = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, mEtHeight); et.setLayoutParams(etParams); et.setBackgroundColor(ContextCompat.getColor(this, android.R.color.white)); et.setGravity(Gravity.CENTER_VERTICAL); et.setInputType(InputType.TYPE_TEXT_FLAG_MULTI_LINE); et.setTextSize(16); et.setId(mEtId); ll.addView(et); RelativeLayout rl = new RelativeLayout(MainActivity.this); RelativeLayout.LayoutParams rlParams = new RelativeLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); rlParams.setMargins(0, UIUtil.dp2px(8), 0, 0); rl.setLayoutParams(rlParams); // ib_add ImageButton btAdd = new ImageButton(MainActivity.this); RelativeLayout.LayoutParams btAddParams = new RelativeLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); btAddParams.addRule(RelativeLayout.ALIGN_PARENT_END); btAdd.setLayoutParams(btAddParams); btAdd.setBackgroundResource(R.drawable.add); btAdd.setId(mBtnAddId); btAdd.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { addItem(v); } }); rl.addView(btAdd); mAddList.add(curView, btAdd); // ib_del ImageButton btDel = new ImageButton(MainActivity.this); RelativeLayout.LayoutParams btDelParams = new RelativeLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); btDelParams.setMargins(0, 0, UIUtil.dp2px(8), 0); btDelParams.addRule(RelativeLayout.LEFT_OF, mBtnAddId); btDel.setLayoutParams(btDelParams); btDel.setBackgroundResource(R.drawable.del); btDel.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { delItem(v); } }); rl.addView(btDel); mDelList.add(curView, btDel); ll.addView(rl); mLlContent.addView(ll, curView); mBtnAddId++; mEtId++; } } 看起来有些长, 但是对照布局文件来看, 就非常简单了, 就是用java代码把布局文件里写的再写一遍. 删除条目 /** * @param v 删除一个新的EditText条目 */ private void delItem(View v) { if (v == null) { return; } int curView = -1; for (int i = 0; i < mDelList.size(); i++) { if (mDelList.get(i) == v) { curView = i; break; } } if (curView >= 0) { mAddList.remove(curView); mDelList.remove(curView); mLlContent.removeViewAt(curView); } } 删除就很简单了, 先弄清点击的是哪个按钮, 然后把相关的一股脑删了即可. 最后 其实这样改动视图还是比较过时的, 之后会准备一篇RecyclerView增删条目的文章. 到时候一对比就可以看到效果了. 但是在某些场合用用还是可以的, 比如弹窗中微调布局之类的. 喜欢记得点赞哦, 暗中关注我也是可以的~
Android小知识10则(上)github传送门 注: 在目录中点击可以跳转到具体代码页 目录 Chronometer和CountDownTimer计时器 Chronometer的使用 CountDownTimer的使用 正则表达式 动态数组 shape绘制 矩形 椭圆 线 环 用shape绘制SeekBar 最后 Chronometer和CountDownTimer计时器 Android也是提供了计时器的, 虽然功能比较简单, 但是有些场景下也还是够用的...吗?(手动滑稽) CountDownTimer是倒计时计时器. Chronometer的话, 看怎么用了, 正着倒着都行...吗?(再次滑稽) Chronometer的使用 礼貌性给下官方文档. 然后上效果图: Chronometer的使用 mTimer.setBase(-60000 + SystemClock.elapsedRealtime()); mTimer.setCountDown(false); mTimer.start(); 我们以+1m(也就是从1分钟开始计时)为例: 先看xml代码, android:format="%s"是要点, 后面会说. 然后它继承自TextView, 属性设置什么的就很简单了: <Chronometer android:id="@+id/timer" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_margin="@dimen/sixteen_dp" android:format="%s" android:textColor="@android:color/darker_gray" android:textSize="@dimen/thirty_sp" /> (-60000 + SystemClock.elapsedRealtime())的出现会让你一下子懵了, 所以先说SystemClock.elapsedRealtime(). Chronometer实例是需要设置基线的, 然后用SystemClock.elapsedRealtime()减去你设置的基线值, 换句话说, 如果你写mTimer.setBase(SystemClock.elapsedRealtime());就意味着从零开始. 然后单位是ms, 一分钟就是60000ms, 所以想从一分钟开始就是(-60000 + SystemClock.elapsedRealtime())了. mTimer.setCountDown();代表是倒计时还是正常计时, false就是正常计时, true计时倒计时. 你可能会提问, 为什么我没有格式化字符串它也正常显示了. 看xml中的android:format="%s", 这就是代表用默认的格式. 官方文档有这么一段: By default it will display the current timer value in the form "MM:SS" or "H:MM:SS", or you can use setFormat(String) to format the timer value into an arbitrary string. 也就是说默认"MM:SS", 超过1小时"H:MM:SS", 你可以用setFormat(String)设置你的style儿(手动滑稽). 然后mTimer.start();是开始. mTimer.stop();是停止. 这很好理解了. 也许你会觉得它还挺好用, 但事实很残酷, 倒计时的功能要7.0才能使用, 其它的倒是兼容低版本, 但是废了一半了不是. 但是配合CountDownTimer, 意外地解决了麻烦. CountDownTimer的使用 效果图: CountDownTimer的使用 这个倒计时类异常好用. 构造函数第一个参数是总时长, 第二个是间隔. onTick是每次变化要执行的动作, onFinish是结束后要执行的动作. mCountDownTimer.start();是开始. mCountDownTimer.cancel();是停止. 完事了, 就这么多内容, 不信去看看官方文档. private CountDownTimer mCountDownTimer = new CountDownTimer(10000, 1000) { @Override public void onTick(long millisUntilFinished) { String str = "剩余" + (millisUntilFinished / 1000) + "秒"; mTvTime.setText(str); } @Override public void onFinish() { mTvTime.setEnabled(true); mTvTime.setText("倒计时结束"); } }; 正则表达式 正则表达式是很通用的东西了, 不论写什么都会用到的, 看看应用中展现的部分正则表达式的功能吧: 正则表达式 规则 只要知道了规则, 几乎没有正则表达式匹配不了的串(手动滑稽). 这里有个推荐的网站. 里面写的很细. 接下来展示如何在Android中实现的. 匹配 Pattern p = Pattern.compile("\\d+"); Matcher m = p.matcher("abcd1234ABCD5678"); 上面两行是关键语句, compile方法中的字符串就是正则表达式, 这里是"\\d+"(注意多一个\是转义符). matcher方法中的字符串就是要匹配的字符串, 这里是"abcd1234ABCD5678". 然后有4种匹配方式, 我在效果图中展示的是find()和matches(): 序号 方法 说明 1 public boolean lookingAt() 尝试将从区域开头开始的输入序列与该模式匹配。 2 public boolean find() 尝试查找与该模式匹配的输入序列的下一个子序列。 3 public boolean find(int start) 重置此匹配器,然后尝试查找匹配该模式、从指定索引开始的输入序列的下一个子序列。 4 public boolean matches() 尝试将整个区域与模式匹配。 while (m.find()) { Log.i("sorrower", m.group()); } 上面的代码可以打印符合正则表达式的子序列结果. 当然你可以使用m.group(x)获取第x个匹配的子序列. 注意从1开始. 用m.start()和m.end()就可以获取到子序列的起始位置和结束位置后面一个位置了. matches()的返回值表示整个匹配是否成功. 替换 除开匹配, 用正则表达式替换也是没问题的哦. 序号 方法 说明 1 public Matcher appendReplacement(StringBuffer sb, String replacement) 实现非终端添加和替换步骤。 2 public StringBuffer appendTail(StringBuffer sb) 实现终端添加和替换步骤。 3 public String replaceAll(String replacement) 替换模式与给定替换字符串相匹配的输入序列的每个子序列。 4 public String replaceFirst(String replacement) 替换模式与给定替换字符串匹配的输入序列的第一个子序列。 5 public static String quoteReplacement(String s) 返回指定字符串的字面替换字符串。这个方法返回一个字符串,就像传递给Matcher类的appendReplacement 方法一个字面字符串一样工作。 动态数组 来看看效果先吧! 动态数组 转变为静态数组 首先是ArrayList转变为静态数组, 这个算是个小知识点吧, toArray方法中的参数要写对. 代码如下: ArrayList<String> list = new ArrayList<>(); list.add("java"); list.add("c"); list.add("c++"); String[] strings = list.toArray(new String[0]); Log.i("tag", Arrays.toString(strings)); 使用 使用起来也比较简单, 可以看官方文档, 或者看下我的源码, 改动改动体验下. shape绘制 在没有UI设计师的时候, 或者是想简单看下效果的时候, 用shape进行快速绘制是极好的! 大家如果之前有关注我, 会知道这是之前一个单独的篇章, 当然不是为了凑数放在这里的, 和下一个知识点有关. 官方文档 一共有四种shape: rectangle, oval, line, ring. 矩形 我们一个一个来看, 首先是矩形: 矩形例子 <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <!-- 尺寸 --> <size android:width="160dp" android:height="80dp" /> <!-- 颜色 --> <!--<solid android:color="@color/colorPrimary" />--> <!-- 内间距 --> <padding android:bottom="8dp" android:left="8dp" android:right="8dp" android:top="8dp" /> <!-- 渐变 --> <gradient android:angle="45" android:endColor="@color/colorPrimary" android:startColor="@color/colorAccent" android:type="linear" /> <!-- 圆角 --> <!--<corners android:radius="200dp" />--> <!-- 圆角单独设置 --> <corners android:bottomLeftRadius="0dp" android:bottomRightRadius="0dp" android:topLeftRadius="40dp" android:topRightRadius="40dp" /> <!-- 描边 --> <stroke android:width="2dp" android:color="#666" android:dashGap="4dp" android:dashWidth="4dp" /> </shape> 渐变gradient是会覆盖颜色的, 如果你想要纯色, 直接设置颜色值即可, 就是设置solid中的color. 顺带一提, solid只有color一个参数. 如果你没有渐变gradient, 也不写solid, 那么将会是空心的. 渐变gradient的type参数有3个: linear 线性渐变 sweep 扫描渐变 radial 放射渐变, 需要配合参数gradientRadius 圆角corners可以直接设置radius, 也可以一个一个指定. 描边stroke的话不写dashGap, dashWidth就会是实线, dashWidth代表虚线宽度, dashGap代表虚线间隔. 内间距padding和尺寸size就不提了, 大家都懂的. 椭圆 椭圆例子 <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <!-- 尺寸 --> <size android:width="160dp" android:height="80dp" /> <!-- 颜色 --> <!--<solid android:color="@color/colorPrimary" />--> <!-- 内间距 --> <padding android:bottom="8dp" android:left="8dp" android:right="8dp" android:top="8dp" /> <!-- 渐变 --> <gradient android:centerColor="@color/colorPrimary" android:endColor="@color/colorPrimaryDark" android:startColor="@color/colorAccent" android:type="sweep" /> <!-- 描边 --> <stroke android:width="1dp" android:color="#333" /> </shape> 渐变是最多可以设置三种颜色, 意思一看便知了: startColor centerColor endColor 一般椭圆都会用来绘制实心的小圆点. 线 线就很简单了: 线例子 <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="line"> <!-- 描边 --> <stroke android:width="8dp" android:color="@color/colorPrimary" android:dashGap="8dp" android:dashWidth="6dp" /> </shape> 环 最后来看环, 它有些特有属性: 环例子B <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:innerRadiusRatio="4" android:shape="ring" android:thicknessRatio="100" android:useLevel="false"> <!-- 尺寸 --> <size android:width="200dp" android:height="200dp" /> <!-- 渐变 --> <gradient android:angle="0" android:centerColor="@color/colorPrimaryDark" android:endColor="@color/colorPrimary" android:startColor="@color/colorAccent" android:type="sweep" /> <!-- 描边 --> <stroke android:width="1dp" android:color="#777" android:dashGap="4dp" android:dashWidth="4dp" /> </shape> thicknessRatio 指的是环厚度百分比, 默认是9, 比如说这里宽度是200dp, thicknessRatio是100, 环厚度就是200dp / 100 = 2dp. 当然, 你可以直接用thickness设置厚度. innerRadiusRatio 是内环百分比, 默认是3, 就是指用宽度 / 百分比得到的值就是内环半径. 同样可以用innerRadius直接设置. 用shape绘制SeekBar 我知道有很多非常好看的自定义进度条, 但是我写这个SeekBar是想补充下shape的使用, 用非常少量的代码实现自定义进度条. 来看看效果图: 用shape绘制SeekBar 实现 <SeekBar android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="@dimen/eight_dp" android:max="200" android:maxHeight="@dimen/eight_dp" android:minHeight="@dimen/eight_dp" android:progressDrawable="@drawable/layout_progress" android:thumb="@drawable/shape_circle" /> 简单解释下几个要点属性: max代表进度条最大的值. maxHeight, minHeight可以设置进度条宽度, 我喜欢稍微宽一点的. thumb设置滑块, 可以是图片, 可以是shape写的设置. progressDrawable代表进度条的外观, 可以是图片, 可以是shape写的设置. 再来看看滑块和进度条外观具体代码, 进度条可以设置背景, 进度, 和第二进度. 滑块的话, 你想画成什么样都行. <?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@android:id/background"> <shape> <corners android:radius="@dimen/four_dp" /> <solid android:color="@android:color/darker_gray" /> </shape> </item> <item android:id="@android:id/secondaryProgress"> <clip> <shape> <corners android:radius="@dimen/four_dp" /> <solid android:color="@color/colorAccent" /> </shape> </clip> </item> <item android:id="@android:id/progress"> <clip> <shape> <corners android:radius="@dimen/four_dp" /> <solid android:color="@android:color/holo_blue_light" /> </shape> </clip> </item> </layer-list> <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="@android:color/holo_blue_light" /> <stroke android:width="@dimen/one_dp" android:color="@android:color/holo_blue_light" /> <size android:width="@dimen/sixteen_dp" android:height="@dimen/sixteen_dp" /> </shape> java部分的话, 用Handler实例postDelayed方法让进度条跑起来就可以看到效果了. 这里设定50ms发一次消息. findViewById(R.id.cv_start).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mRunnable == null) { mRunnable = new MyRunnable(); mHandler.postDelayed(mRunnable, 0); } } }); findViewById(R.id.cv_stop).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mHandler.removeCallbacks(mRunnable); mRunnable = null; } }); private class MyRunnable implements Runnable { @Override public void run() { int progress = mSbTest.getProgress(); mTvProgress.setText(String.valueOf(progress)); mSbTest.setProgress(++progress); mSbTest.setSecondaryProgress(progress + 10); int progress2 = mSbTest2.getProgress(); mTvProgress2.setText(String.valueOf(progress2)); mSbTest2.setProgress(++progress2); mSbTest2.setSecondaryProgress(progress2 + 20); mHandler.postDelayed(this, 50); } } 最后 这样就写完10个知识点了, 这样之后很多文章扩展起来就会很方便了. 喜欢记得点赞, 有意见或者建议评论区见, 暗中关注我也是可以的哦~
界面无小事(一): RecyclerView+CardView了解一下界面无小事(二): 让RecyclerView展示更多不同视图界面无小事(三):用RecyclerView + Toolbar做个文件选择器界面无小事(四):来写个滚动选择器吧!界面无小事(五):自定义TextView界面无小事(六):来做个好看得侧拉菜单!github传送门 目录 效果图 前言 DrawerLayout Toolbar fragment NavigationView CircleImageView 最后 效果图 不多废话, 来看效果图, 喜欢再看源码: 效果图 前言 这次来说说侧拉菜单. 虽然现在手机越来越大, 但也不至于说直接把侧拉菜单全部展示出来, 因为很多时候, 它没有展示的必要. 这次会涉及的内容是DrawerLayout, Toolbar, NavigationView, 都是与material design相关的. DrawerLayout 看下主视图布局代码: <?xml version="1.0" encoding="utf-8"?> <android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/dl_main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.so.knowledge.ui.activity.DrawerLayout.DrawerActivity"> <RelativeLayout android:id="@+id/ll_content" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.Toolbar android:id="@+id/tb_main" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@color/colorPrimary" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> <TextView android:id="@+id/tv_content" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="@string/username" android:textColor="@android:color/holo_blue_dark" android:textSize="@dimen/thirty_sp" /> </RelativeLayout> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="start" android:background="@android:color/white"> <android.support.v7.widget.RecyclerView android:id="@+id/rv_fun_list" android:layout_width="match_parent" android:layout_height="wrap_content" android:scrollbars="vertical" /> </RelativeLayout> <android.support.design.widget.NavigationView android:id="@+id/nav_user_info" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="end" app:headerLayout="@layout/nav_header" app:menu="@menu/nav_menu" /> </android.support.v4.widget.DrawerLayout> 这里在DrawerLayout中塞进了三个布局, android:layout_gravity="end"代表右侧拉布局, android:layout_gravity="start"代表左侧拉布局, 没写代表主界面布局. 具体细节后面再说, 记得导包: compile 'com.android.support:design:25.3.1' Toolbar Toolbar我是很喜欢用的, 可以放置很多按钮, 通过设置隐藏等, 看起来也依然简洁.我在第三篇就写过Toolbar的使用. 然后在效果图中, 点击Toolbar的左侧按钮, 会展开左侧的菜单. 菜单内容就是我在第一篇中写的, 具体代码就是mDlMain.openDrawer(GravityCompat.START);. 点击右侧按钮, 会展开右侧菜单, 代码是mDlMain.openDrawer(GravityCompat.END);, 右侧菜单我们后面再说. @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: mDlMain.openDrawer(GravityCompat.START); break; case R.id.username: mDlMain.openDrawer(GravityCompat.END); break; } return true; } fragment 仔细观察的同学会发现点击左侧菜单的第一个和第二个按钮会切换主界面字符串的颜色, 其实不单单是切换颜色, 我重新放置了fragment. 当然了, 切换fragment不是什么难事. myRVAdapter.setOnItemClickListener(new MyRVAdapter.OnItemClickListener() { @Override public void onItemClick(View view, int position) { Toast.makeText(getApplicationContext(), "click: " + position, Toast.LENGTH_SHORT).show(); FragmentTransaction ft = fm.beginTransaction(); switch (position) { case 0: ft.replace(R.id.ll_content, new Fragment1()); break; case 1: ft.replace(R.id.ll_content, new Fragment2()); break; default: break; } ft.commit(); mDlMain.closeDrawer(GravityCompat.START); } @Override public void onItemLongClick(View view, int position) { Toast.makeText(getApplicationContext(), "long click: " + position, Toast.LENGTH_SHORT).show(); } }); 我最想说的一点就是, 即使切换了fragment, 但是Toolbar依旧是存在的, 这点要注意. NavigationView 官方文档 这是用来实现右侧菜单的. 主要要实现两个部分, 就是布局文件中写的header和menu部分. header部分是布局代码, 而menu部分是menu代码. 关于CircleImageView部分, 后面有说. 这里要说的是菜单部分, 将两个按钮设置成单选条目组, 就和单选按钮组是一样的了. <android.support.design.widget.NavigationView android:id="@+id/nav_user_info" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="end" app:headerLayout="@layout/nav_header" app:menu="@menu/nav_menu" /> <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="@dimen/hundred_eighty_dp" android:background="@color/colorPrimary" android:padding="@dimen/sixteen_dp"> <de.hdodenhof.circleimageview.CircleImageView xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/civ_avatar" android:layout_width="@dimen/sixty_four_dp" android:layout_height="@dimen/sixty_four_dp" android:layout_centerInParent="true" android:src="@drawable/avatar" app:civ_border_color="@android:color/white" app:civ_border_width="@dimen/two_dp" /> <TextView android:id="@+id/tv_email" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:text="@string/email" android:textColor="@android:color/white" /> <TextView android:id="@+id/tv_username" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_above="@id/tv_email" android:text="@string/username" android:textColor="@android:color/white" /> </RelativeLayout> <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <group android:checkableBehavior="single"> <item android:id="@+id/nav_share" android:icon="@mipmap/ic_launcher" android:title="@string/share" /> <item android:id="@+id/nav_loc" android:icon="@mipmap/ic_launcher" android:title="@string/loc" /> </group> </menu> CircleImageView 这是个异常实用的开源项目, 使用起来也很简单, 目的就是把普通图片变成圆形图片, 还可以加个白框或者黑框. 从效果图来看, 还是很不错的. 圆形图片 最后 这次的很简单, 就是融合了之前的内容, 并把google提供的侧拉面板和菜单面板的使用学会, 感谢google, 自己实现就可麻烦了. 喜欢记得点赞, 有意见或者建议评论区见, 暗中关注我也是可以的~
动画必须有(一): 属性动画浅谈githhub传送门 目录 前言 效果图 FloatingActionButton基础 FloatingActionButton实例 最后 前言 悬浮按钮是我非常喜欢的, 可以把最关键的功能放入到悬浮按钮中. 比如日记app里的新建日记, 阅读类app里的喜欢. 稍微处理一下可以将悬浮按钮扩展成悬浮菜单, 来看下实现吧! github直接看源码 效果图 废话不多说, 先看图, 感兴趣再往下看! 悬浮菜单 FloatingActionButton基础 记得导包. compile 'com.android.support:design:26.+' 可以看看谷歌官方介绍. 你会被瞬间圈粉. 然后是官方文档, 这个文档说了如何调用. 搭配Snackbar 官方推荐配合Snackbar来使用, 这都不多说了. 配合Snackbar 显示和隐藏 然后还有就是悬浮按钮的隐藏和显示函数. Button btHide = (Button) findViewById(R.id.bt_hide); btHide.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { fab.hide(); } }); Button btShow = (Button) findViewById(R.id.bt_show); btShow.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { fab.show(); } }); 隐藏和显示 颜色 可以设置点击颜色app:rippleColor, 以及背景颜色app:backgroundTint. 我将背景色改成蓝色, 点击水波纹扩散变为紫色, 效果图如下: 设置颜色 注意看颜色 位置 当然了, 位置可以随便改, 甚至可以吸附在某个控件之上. android:layout_gravity="bottom|left" 设置位置 吸附效果如下, 即使滚动也会保持相对的位置: app:layout_anchor="@id/toolbar" app:layout_anchorGravity="center|bottom" 吸附并设置位置 FloatingActionButton实例 来看看效果图是如何实现的吧. 布局文件 布局文件是个要点, 里面塞进了两个菜单, 你选一个喜欢的用就好. 一个是扇型的, 一个是线型的. <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <FrameLayout android:id="@+id/fl_fan_menu" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone"> <android.support.design.widget.FloatingActionButton android:id="@+id/fab_left" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_marginBottom="@dimen/twenty_dp" android:layout_marginEnd="@dimen/twenty_dp" android:src="@mipmap/ic_launcher" app:backgroundTint="@color/colorAccent" app:elevation="@dimen/zero_dp" app:fabSize="mini" /> <android.support.design.widget.FloatingActionButton android:id="@+id/fab_left_top" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_marginBottom="@dimen/twenty_dp" android:layout_marginEnd="@dimen/twenty_dp" android:src="@mipmap/ic_launcher" app:backgroundTint="@color/colorAccent" app:elevation="@dimen/zero_dp" app:fabSize="mini" /> <android.support.design.widget.FloatingActionButton android:id="@+id/fab_top" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_marginBottom="@dimen/twenty_dp" android:layout_marginEnd="@dimen/twenty_dp" android:src="@mipmap/ic_launcher" app:backgroundTint="@color/colorAccent" app:elevation="@dimen/zero_dp" app:fabSize="mini" /> <android.support.design.widget.FloatingActionButton android:id="@+id/fab_origin" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_marginBottom="@dimen/twenty_dp" android:layout_marginEnd="@dimen/twenty_dp" android:src="@drawable/ic_add" app:backgroundTint="@color/colorPrimary" app:fabSize="normal" /> </FrameLayout> <RelativeLayout android:id="@+id/rl_line_menu" android:layout_width="match_parent" android:layout_height="match_parent"> <RelativeLayout android:id="@+id/rl_menu_content" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone"> <LinearLayout android:id="@+id/ll_fun1" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_marginBottom="@dimen/hundred_dp" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_toLeftOf="@+id/fab_mini1" android:layout_weight="1" android:gravity="right" android:paddingRight="@dimen/eight_dp" android:text="1" android:textColor="@android:color/white" android:textSize="@dimen/sixteen_sp" /> <android.support.design.widget.FloatingActionButton android:id="@+id/fab_mini1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/twenty_six_dp" android:src="@mipmap/ic_launcher" app:backgroundTint="@color/colorPrimary" app:fabSize="mini" /> </LinearLayout> <LinearLayout android:id="@+id/ll_fun2" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_above="@+id/ll_fun1" android:layout_marginBottom="@dimen/twenty_dp" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_toLeftOf="@+id/fab_mini2" android:layout_weight="1" android:gravity="right" android:paddingRight="@dimen/eight_dp" android:text="2" android:textColor="@android:color/white" android:textSize="@dimen/sixteen_sp" /> <android.support.design.widget.FloatingActionButton android:id="@+id/fab_mini2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/twenty_six_dp" android:src="@mipmap/ic_launcher" app:backgroundTint="@color/colorPrimary" app:fabSize="mini" /> </LinearLayout> <LinearLayout android:id="@+id/ll_fun3" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_above="@+id/ll_fun2" android:layout_marginBottom="@dimen/twenty_dp" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_toLeftOf="@+id/fab_mini2" android:layout_weight="1" android:gravity="right" android:paddingRight="@dimen/eight_dp" android:text="3" android:textColor="@android:color/white" android:textSize="@dimen/sixteen_sp" /> <android.support.design.widget.FloatingActionButton android:id="@+id/fab_mini3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/twenty_six_dp" android:src="@mipmap/ic_launcher" app:backgroundTint="@color/colorPrimary" app:fabSize="mini" /> </LinearLayout> <LinearLayout android:id="@+id/ll_fun4" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_above="@+id/ll_fun3" android:layout_marginBottom="@dimen/twenty_dp" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_toLeftOf="@+id/fab_mini2" android:layout_weight="1" android:gravity="right" android:paddingRight="@dimen/eight_dp" android:text="4" android:textColor="@android:color/white" android:textSize="@dimen/sixteen_sp" /> <android.support.design.widget.FloatingActionButton android:id="@+id/fab_mini4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/twenty_six_dp" android:src="@mipmap/ic_launcher" app:backgroundTint="@color/colorPrimary" app:fabSize="mini" /> </LinearLayout> </RelativeLayout> <android.support.design.widget.FloatingActionButton android:id="@+id/fab_add" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:layout_marginBottom="@dimen/twenty_dp" android:layout_marginEnd="@dimen/twenty_dp" android:backgroundTint="@color/colorAccent" android:src="@drawable/ic_add" app:fabSize="normal" app:rippleColor="@color/colorPrimaryDark" /> </RelativeLayout> <Button android:id="@+id/bt_switch" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="@string/switch_menu" /> </RelativeLayout> normal与mini 悬浮按钮有两种尺寸, normal和mini. 在xml中加入app:fabSize="mini"就变成mini尺寸的了. 所以在设置动画和位置的时候不是将按钮全部放置在同一位置, 需要修正位置. 修正距离就是(normal-mini)/2, 应该很好理解. // 计算偏移值 int w = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); int h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); mFabOrigin.measure(w, h); mFabLeft.measure(w, h); mOffset = (mFabOrigin.getMeasuredHeight() - mFabLeft.getMeasuredHeight()) / 2; // 修正位置 FrameLayout.LayoutParams lParams = (FrameLayout.LayoutParams) mFabLeft.getLayoutParams(); lParams.setMargins(0, 0, UIUtil.dp2px(20) + mOffset, UIUtil.dp2px(20) + mOffset); mFabLeft.setLayoutParams(lParams); FrameLayout.LayoutParams ltParams = (FrameLayout.LayoutParams) mFabLeftTop.getLayoutParams(); ltParams.setMargins(0, 0, UIUtil.dp2px(20) + mOffset, UIUtil.dp2px(20) + mOffset); mFabLeftTop.setLayoutParams(ltParams); FrameLayout.LayoutParams tParams = (FrameLayout.LayoutParams) mFabTop.getLayoutParams(); tParams.setMargins(0, 0, UIUtil.dp2px(20) + mOffset, UIUtil.dp2px(20) + mOffset); mFabTop.setLayoutParams(tParams); 关于动画 只要位置算对了, 动画不是特别难, 当然想要像google或者apple的动画那样舒适还是很难的. 没看第一篇的可以回头看看. /** * 显示扇型菜单 */ private void showFanMenu() { // 标识符设置是 mFanMenuOpen = true; // 按钮1向左移动 int x = (int) mFabOrigin.getX(); int y = (int) mFabOrigin.getY(); ValueAnimator va1 = ValueAnimator.ofInt(x, x - ConstantUtil.FAN_OFFSET); va1.setDuration(500).addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int l = (int) animation.getAnimatedValue(); int t = (int) mFabLeft.getY(); int r = mFabLeft.getWidth() + l; int b = mFabLeft.getHeight() + t; mFabLeft.layout(l, t, r, b); } }); // 按钮2向左上移动 ValueAnimator va2x = ValueAnimator.ofInt(x, x - (int) (ConstantUtil.FAN_OFFSET / Math.pow(2, 1.0 / 2))); ValueAnimator va2y = ValueAnimator.ofInt(y, y - (int) (ConstantUtil.FAN_OFFSET / Math.pow(2, 1.0 / 2))); va2x.setDuration(500).addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int l = (int) animation.getAnimatedValue(); int t = (int) mFabLeftTop.getY(); int r = mFabLeftTop.getWidth() + l; int b = mFabLeftTop.getHeight() + t; mFabLeftTop.layout(l, t, r, b); } }); va2y.setDuration(500).addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int l = (int) mFabLeftTop.getX(); int t = (int) animation.getAnimatedValue(); int r = mFabLeftTop.getWidth() + l; int b = mFabLeftTop.getHeight() + t; mFabLeftTop.layout(l, t, r, b); } }); // 按钮3向上移动 ValueAnimator va3 = ValueAnimator.ofInt(y, y - ConstantUtil.FAN_OFFSET); va3.setDuration(500).addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int l = (int) mFabTop.getX(); int t = (int) animation.getAnimatedValue(); int r = mFabTop.getWidth() + l; int b = mFabTop.getHeight() + t; mFabTop.layout(l, t, r, b); } }); // 开始动画 va1.start(); va2x.start(); va2y.start(); va3.start(); } 切换图标 然后就是在不同状态切换悬浮按钮的图标, 使用setImageResource方法即可. mFabAdd.setImageResource(mLineMenuOpen ? R.drawable.ic_add : R.drawable.ic_close); 最后 我本人还是很喜欢google的material design的, 这个悬浮按钮也非常实用. 喜欢记得点赞或者关注我哦, 有意见或者建议评论区见~ 动画必须有(一): 属性动画浅谈githhub传送门
界面无小事(一): RecyclerView+CardView了解一下界面无小事(二): 让RecyclerView展示更多不同视图界面无小事(三):用RecyclerView + Toolbar做个文件选择器界面无小事(四):来写个滚动选择器吧!界面无小事(五):自定义TextView界面无小事(六):来做个好看得侧拉菜单!github传送门 目录 效果图 前言 自定义属性 MeasureSpec类 颜色解析 字号转换 最后 效果图 不多废话, 直接上图, 如果感兴趣再看下去. 效果图 前言 写第四篇滚动选择器的时候, 在自定义视图这里含糊了, 有些地方没说清楚, 这次补上关于自定义视图的部分. 自定义属性 自定义视图的一个要点就是添加自定义属性. 这里我们填上三个常用的, 文本, 颜色, 字号. 然后在布局文件中就可以使用了. 最后在自定义类中获取属性并赋值. <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="MyTextView"> <attr name="text" format="string" /> <attr name="color" format="color" /> <attr name="size" format="dimension" /> </declare-styleable> </resources> xmlns:app="http://schemas.android.com/apk/res-auto" <com.so.mytextview.ui.view.MyTextView android:id="@+id/mtv_test" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" app:color="@color/colorAccent" app:size="60sp" app:text="hello world" /> public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { // 获取自定义属性 TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyTextView); mSize = ta.getDimension(R.styleable.MyTextView_size, 16); mText = ta.getString(R.styleable.MyTextView_text); mColor = ta.getColor(R.styleable.MyTextView_color, Color.BLACK); ta.recycle(); // 设置画笔 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setTextSize(mSize); // 设置背景颜色 mBkColor = Color.BLUE; } MeasureSpec类 MeasureSpec类官方文档 关于onMeasure方法, 最重要的就是就是MeasureSpec类的使用了. 其实主要也就是要算好match_parent和wrap_content. match_parent和具体数值都是EXACTLY. wrap_content是AT_MOST. ScrollView或者是ListView就会是UNSPECIFIED. @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = 0; int height = 0; int specMode = MeasureSpec.getMode(widthMeasureSpec); int specSize = MeasureSpec.getSize(widthMeasureSpec); switch (specMode) { case MeasureSpec.EXACTLY: width = getPaddingLeft() + getPaddingRight() + specSize; break; case MeasureSpec.AT_MOST: case MeasureSpec.UNSPECIFIED: width = (int) (getPaddingLeft() + getPaddingRight() + mPaint.measureText(mText)); break; } specMode = MeasureSpec.getMode(heightMeasureSpec); specSize = MeasureSpec.getSize(heightMeasureSpec); switch (specMode) { case MeasureSpec.EXACTLY: height = getPaddingTop() + getPaddingBottom() + specSize; break; case MeasureSpec.AT_MOST: case MeasureSpec.UNSPECIFIED: Paint.FontMetrics fmi = mPaint.getFontMetrics(); float textHeight = Math.abs(fmi.bottom - fmi.top); height = (int) (getPaddingTop() + getPaddingBottom() + textHeight); break; } setMeasuredDimension(width, height); } 有两个要点, 就是算字符串的宽度和高度, 宽度用Paint实例的measureText方法即可. 高度涉及到我在第四篇写的Paint.FontMetrics类, 就是用底部减去顶部取绝对值. 颜色解析 Color是个要处理的类, 当你用getColor函数获取到函数, 它是一个int值, 如果我们需要重新在原有颜色基础上变化, 就需要解析这个int, 将它还原成RGB. /** * 依据颜色值获取rgb值 * * @param color 颜色值 * @return rgb值 */ public int[] setColor(int color) { int[] rgb = new int[3]; rgb[0] = (color & 0x00ff0000) >> 16; rgb[1] = (color & 0x0000ff00) >> 8; rgb[2] = (color & 0x000000ff); return rgb; } 字号转换 要处理好字号问题, 最重要的就是转换, 代码中都是用px的, 但是布局文件一般用sp. /** * sp转px */ public static int sp2px(float spVal) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spVal, getContext().getResources().getDisplayMetrics()); } /** * px转sp */ public static float px2sp(float pxVal) { return (pxVal / getContext().getResources().getDisplayMetrics().scaledDensity); } 最后 这样可以自定义一些简单的视图类了, 如果要更复杂的, 还需要去处理更多的参数, 特别是构造方法那个四参数的. 有意见或者建议评论区见, 喜欢记得点赞或者关注我哦~
界面无小事(一): RecyclerView+CardView了解一下界面无小事(二): 让RecyclerView展示更多不同视图界面无小事(三):用RecyclerView + Toolbar做个文件选择器界面无小事(四):来写个滚动选择器吧!界面无小事(五):自定义TextView界面无小事(六):来做个好看得侧拉菜单!去github看源码 目录 效果图 前言 Paint类 计时器 基线baseline 滚动选择器实现 最后 效果图 不废话, 先上效果图. 觉得有趣再往下看吧.去github看源码 效果图 前言 在pc时代, 输入一般都依靠键盘. 对于像选时间这种操作, win一般会列出全部日期, 然后让你点击选择. 说句实话, 土爆了. 当然了, 滚动选时间也土爆了(手动尴尬), 但是比win的操作方式已经有趣不少了. 而且滚动选择器我觉得还是有很多不错的应用场景的, 所以这次就写一个分享给大家. Paint类 官方文档 Paint还是很值得熟悉的一个类, 大部分函数都是set方法, 去文档看就好了. 本文有两个Paint实例, 一个是绘制文本用, 一个是绘线. mPaint = new Paint(); // 设置抗锯齿 mPaint.setFlags(Paint.ANTI_ALIAS_FLAG); // 设置文本对齐方式 mPaint.setTextAlign(Paint.Align.CENTER); // 设置画笔颜色 mPaint.setColor(UIUtil.getColor(R.color.colorText)); mLinePaint = new Paint(); mLinePaint.setColor(UIUtil.getColor(R.color.colorPrimaryTrans)); 后续代码中, Paint实例会依据曲线设置文本字号以及透明度. 所以, 我们需要自己设置最小最大字号, 最小最大透明度, 这样就可以在范围内依据函数曲线变化. 差不多就是下图, 但是y是大于0的. 变化曲线 // 依据曲线设置字号 float scale = gradient(mMax, mMoveLen); float size = (mMax - mMin) * scale + mMin; mPaint.setTextSize(size); // 依据曲线设置透明度 mPaint.setAlpha((int) ((mMaxAlpha - mMinAlpha) * scale + mMinAlpha)); 计时器 计时器是经常用到的, Android里面会用Timer, TimerTask, Handler三个组合使用. 思路就是Timer实例使用schedule函数, 传入TimerTask实例以及时间参数. 然后在TimerTask实例的run方法中让Handler实例调用sendMessage方法发送消息. 最后在handleMessage方法中处理. 代码如下: mTask = new MyTimerTask(mHandler); mTimer.schedule(mTask, 0, 10); private class MyTimerTask extends TimerTask { Handler handler; public MyTimerTask(Handler handler) { this.handler = handler; } @Override public void run() { handler.sendMessage(handler.obtainMessage()); } } Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { // 逐步回滚, 直到小于指定值, 选中目标 if (Math.abs(mMoveLen) < BACK_SPEED) { // 选中 mMoveLen = 0; if (mTask != null) { mTask.cancel(); mTask = null; select(); } } else { // 滚动 mMoveLen = mMoveLen - mMoveLen / Math.abs(mMoveLen) * BACK_SPEED; } invalidate(); } }; 基线baseline 最后来谈谈玄学, 基线. Android的绘制是基于基线的. 那什么是基线, 来看两张图片: 玄学图1 玄学图2 也就是说, 想要把某个文本垂直居中, 除了要获取View的高度, 还要获取文本的高度. 这里就需要Paint.FontMetrics类了, 里面有我们要的参数. 官方文档 这里有两种思路, 依靠top和bottom算出文本高度, 或者依靠ascent和descent. 你对上面哪张图更理解, 就用哪个. 还有一点要说的就是, 所有参数都是相对于baseline的, 比方说top就可能是-100, bottom就会是30. float baseline = y - (fmi.top + fmi.bottom) / 2.0f; float baseline = y - (fmi.ascent + fmi.descent) / 2.0f; 滚动选择器实现 要想实现滚动选择器, 肯定还是要处理触摸操作的. 如果对自定义视图不熟悉的, 可以看看我之前的文章. 或者google一下. 要点就是抬手时候开启计时器, 点下记录位置, 移动重绘. 然后为了头尾衔接, 需要在到顶和到底的时候处理下List中的内容. @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: { if (mTask != null) { mTask.cancel(); mTask = null; } mLastDownY = event.getY(); } break; case MotionEvent.ACTION_MOVE: { mMoveLen += (event.getY() - mLastDownY); if (mMoveLen > DIS * mMin / 2) { tailToHead(); mMoveLen = mMoveLen - DIS * mMin; } else if (mMoveLen < -DIS * mMin / 2) { headToTail(); mMoveLen = mMoveLen + DIS * mMin; } mLastDownY = event.getY(); invalidate(); } break; case MotionEvent.ACTION_UP: { // 移动过小就不移动 if (Math.abs(mMoveLen) < 0.001) { mMoveLen = 0; break; } if (mTask != null) { mTask.cancel(); mTask = null; } mTask = new MyTimerTask(mHandler); mTimer.schedule(mTask, 0, 10); } break; } return true; } private void headToTail() { String head = mData.get(0); mData.remove(0); mData.add(head); } private void tailToHead() { String tail = mData.get(mData.size() - 1); mData.remove(mData.size() - 1); mData.add(0, tail); } 最后 有段时间没写文章了, 也是忙一些乱七八糟的事情去了. 写得有点找不到感觉, 之后会努力写出有趣的分享文章来的. 喜欢可以点赞或者关注我哦~
界面无小事(一): RecyclerView+CardView了解一下界面无小事(二): 让RecyclerView展示更多不同视图界面无小事(三):用RecyclerView + Toolbar做个文件选择器界面无小事(四):来写个滚动选择器吧!界面无小事(五):自定义TextView界面无小事(六):来做个好看得侧拉菜单! 目录 前言 最终效果演示 布局文件 RecyclerView适配器 Toolbar的使用 填充RecyclerView条目 悬浮按钮 最后 前言 github传送门 在之前两期也是说了很多RecyclerView的使用, 这期打算来个实操性质的. 用RecyclerView制作一个文件管理器, 并且可以进行文件的多选, 应该是蛮实用的. 最终效果展示 最终效果展示 布局文件 还是先从最简单的布局文件开始看. 可以看到, 三个字符串和一个图标. 图标依据是文件夹或者文件进行显示, 当然了, 之后会做得更细, 例如依据文件类型进行图标变换, mp3就显示为音乐, mp4就是显示视频. 上方字符串是文件或者文件夹名称. 下方字符串的话, 见下面的展示图, 依据类型进行显示: 布局文件 文件夹 文件 RecyclerView适配器 具体的使用在之前文章里面也细说过了. 这里来看两个关键函数. 我们的填充内容主要是当前目录下全部的files, 存放在ArrayList当中. 每当用户展开新的一层, 就会调用refreshData函数进行刷新. 如果是单选或者是多选, 就会调用refreshSelect函数进行对应的处理. 整体也比较简单, 不多赘述. public void refreshData(ArrayList<File> files) { mFiles = files; this.notifyDataSetChanged(); } public int refreshSelect(int pos) { if (pos < 0) { if (pos == -1) { // 全不选 mSelectList.clear(); } else if (pos == -2) { // 全选 for (int i = 0; i < mFiles.size(); i++) { if (mFiles.get(i).isFile() && !mSelectList.contains((Integer) i)) { mSelectList.add((Integer) i); } } } } else { // 单选 if (!mSelectList.contains((Integer) pos)) { mSelectList.add((Integer) pos); } else { mSelectList.remove((Integer) pos); } } this.notifyDataSetChanged(); return mSelectList.size(); } Toolbar的使用 Toolbar是个好东西. 你可以看看官方文档. 反正我自从会用了之后, 几乎没有不用的时候. Toolbar使用细节的文章就太多了, 我也不多说了. 但是app:layout_scrollFlags="scroll|enterAlways|snap"这行还是很重要的, 作用就是让Toolbar在上拉RecyclerView的时候隐藏, 下拉的时候显示. 来张效果图: <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:layout_scrollFlags="scroll|enterAlways|snap" app:popupTheme="@style/AppTheme.PopupOverlay" /> Toolbar隐藏和显示 然后使用setTitle函数可以修改Toolbar中标题内容, 关于变化内容的字符串使用可以看我之前的文章. getSupportActionBar().setTitle( String.format(getResources().getString( R.string.selected_str), mSelectCount)); 如果你要在Toolbar上添加按钮: <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/all" android:icon="@drawable/all" android:orderInCategory="100" android:title="@string/all" app:showAsAction="ifRoom" /> </menu> @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } 添加对应按钮的点击监听的话: @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: break; case R.id.all: break; } return true; } android.R.id.home是系统自带的一个返回按钮, 和ios的返回类似, 你懂的~. 当然了, 一般是不显示出来的, 你需要如下代码: ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } 填充RecyclerView条目 既然要使用RecyclerView, 条目填充就很重要了. 思路就是使用Stack进行当前路径存储, 后续每点击一个文件夹就添加一层, 每返回一层, 就弹出一个. // 获取sdcard目录 mSdcardPath = Environment.getExternalStorageDirectory().toString(); mSelectPath = new ArrayList<>(); mCurPathStack = new Stack<>(); mCurFileList = new ArrayList<>(); File[] files = Environment.getExternalStorageDirectory().listFiles(); if (files != null) { for (File f : files) { mCurFileList.add(f); } } mCurPathStack.push(mSdcardPath); 具体细节肯定要查看下源码的, 这里看到mCurPathStack就是处理路径的. mCurFileList用来存储当前展开文件夹的内容. mSelectPath用来存储勾选的文件. mFMAdapter.setOnItemClickListener(new FMAdapter.OnItemClickListener() { @Override public void onItemClick(View view, int position) { final File file = mCurFileList.get(position); if (file.isDirectory()) { // 是文件夹 mCurPathStack.push("/" + file.getName()); // 根据路径刷新数据 refreshData(getCurPath()); } else { // 是文件 mSelectCount = mFMAdapter.refreshSelect(position); getSupportActionBar() .setTitle(String.format(getResources() .getString(R.string.selected_str), mSelectCount)); // 将选中的文件加入文件路径数组 if (!mSelectPath.contains(file.getAbsolutePath())) { mSelectPath.add(file.getAbsolutePath()); } else { mSelectPath.remove(file.getAbsolutePath()); } } } @Override public void onItemLongClick(View view, int position) { } }); 然后对每一个条目添加点击事件, 长按事件的话, 大家可以按照自己的喜欢处理, 这里不多写了. 主要是单击事件. 如果是点击文件夹, 就将点击文件夹加入栈, 然后刷新视图. 如果是文件, 就是单选文件, 需要将位置传给适配器函数refreshSelect, 这个之前也说过了. 一个比较重要的就是, 在当前的mSelectPath中需要进行确认, 如果已经存在就删除这个选择, 如果不存在, 就选择这个文件, 这个逻辑也是很好理解的. 悬浮按钮 这个也是非常常用的一个视图类. 如果你点击了悬浮按钮, 就会弹出确认窗口, 关于弹窗, 可以查看我之前的文章. 这里就上一张效果图了. 悬浮按钮 点击演示 最后 好了, 就写到这里了, 喜欢记得点赞或者关注我哦, 有意见或者建议评论区见哦. 然后点击这里查看源码, 听说github要被巨硬收购, 瑟瑟发抖.
目录 前言 虚拟机设置 安装系统 最后 前言 今天也是第一时间下载了Ubuntu18.04LTS, 作为一个使用了16.04蛮久时间的程序员, 我表示还是有些激动的. 当然, 没有我拿到mac那么激动(滑稽脸). 好, 我就化身搞事boy, 带大家走一波~ 顺带推荐下我的老文章不美翻怎么开发!Ubuntu 16.04 LTS深度美化!(捂脸) 虚拟机设置 先去官网下个镜像, 然后直接往虚拟机里面拖. 打开虚拟机 拖动镜像 识别镜像 勾选下面的两个按钮, 第一个是生成一个桌面图标, 第二个是进行一些设置. 安装前勾选 尽可能少得建立mac和linux的关联, 如果你能明白每个选项的具体作用, 并且会用到, 另当别论了. 设置 设置 安装系统 非常熟悉的安装界面和一点点都不熟悉的操作系统界面, 这是Ubuntu?(捂脸), 除了越来越红, 我还真是认不得了.(尴尬脸) 安装系统 安装好之后第一次进入会看到如下: 初次进入 最后 好了, 就体验到这里了, 整体来看改动真的大, 不过很多地方还是很有意思的. 如果之后还有特别有意思的地方没准还会加篇文章细说, 但是现在, 我说句实话, 还没找到怎么调分辨率(捂脸). 喜欢记得点赞或者关注我哦~
界面无小事(一): RecyclerView+CardView了解一下界面无小事(二): 让RecyclerView展示更多不同视图界面无小事(三):用RecyclerView + Toolbar做个文件选择器界面无小事(四):来写个滚动选择器吧!界面无小事(五):自定义TextView界面无小事(六):来做个好看得侧拉菜单! 目录 前言 GridLayoutManager的使用 Glide加载图片 让RecyclerView支持更多不同布局 来看看横向滚动 还有瀑布流 最后 前言 之前设置布局的时候用了最简单的LinearLayoutManager, 而且是单一布局, 这次来感受下GridLayoutManager和瀑布流以及多布局. GridLayoutManager的使用 比起LinearLayoutManager, GridLayoutManager可以适用的场景就更多了. 来看一段代码: RecyclerView rvTest = (RecyclerView) findViewById(R.id.rv_test); //rvTest.setLayoutManager(new LinearLayoutManager(this)); final GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 2); rvTest.setLayoutManager(gridLayoutManager); final MyRVAdapter myRVAdapter = new MyRVAdapter(this); if (gridLayoutManager != null) { gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { if (position == 0 || position == 4 || position == (myRVAdapter.getItemCount() - 1)) { return gridLayoutManager.getSpanCount(); } else { return 1; } } }); } rvTest.setAdapter(myRVAdapter); 将原本的LinearLayoutManager替换为GridLayoutManager, 并将0和4以及最后一个条目设置为填充父容器. 来看看效果图: 效果图 Glide加载图片 这是谷歌推荐的一个图片加载库. 我个人的评价就是, 异常强大, 可以满足各种花式加载. 而这里我们只是简单用一下, 不细说. 在构建当中加入: compile 'com.github.bumptech.glide:glide:3.7.0' 使用类似:Glide.with(context).load(R.drawable.pic).centerCrop().into(imageView);加载即可. 第一个参数是上下文, 第二个参数是图片资源, 第三个参数是ImageView控件. 让RecyclerView支持更多不同布局 快速写一个带图布局: <?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:card_view="http://schemas.android.com/apk/res-auto" android:id="@+id/cv_test" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="8dp" android:foreground="@drawable/card_foreground" card_view:cardCornerRadius="4dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <ImageView android:id="@+id/iv_test" android:layout_width="match_parent" android:layout_height="100dp" android:scaleType="centerCrop" /> <TextView android:id="@+id/tv_test" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="20dp" /> </LinearLayout> </android.support.v7.widget.CardView> getItemViewType用来设置视图的类型. 这里我们把0, 4, 和最后一个设置为图片型. 和之前在GridLayoutManager中设置填充父容器的position一样. public enum ITEM_TYPE { ITEM_TYPE_IMAGE, ITEM_TYPE_TEXT } private int[] mImageId = {R.drawable.test, R.drawable.test2, R.drawable.test3}; @Override public int getItemViewType(int position) { if (position == 0 || position == 4 || position == (getItemCount() - 1)) { return ITEM_TYPE.ITEM_TYPE_IMAGE.ordinal(); } else { return ITEM_TYPE.ITEM_TYPE_TEXT.ordinal(); } } 添加一个新的viewHolder: public static class MyTVHolder extends RecyclerView.ViewHolder { TextView mTextView; public MyTVHolder(View itemView) { super(itemView); mTextView = (TextView) itemView.findViewById(R.id.tv_test); } } public static class MyIVHolder extends RecyclerView.ViewHolder { TextView mTextView; ImageView mImageView; MyIVHolder(View view) { super(view); mTextView = (TextView) view.findViewById(R.id.tv_test); mImageView = (ImageView) view.findViewById(R.id.iv_test); } } 然后就是修改onCreateViewHolder和onBindViewHolder部分, 区别处理文字item和带图item, 顺带一提, 类上继承的RecyclerView.Adapter的泛型要变更, public class MyRVAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>: @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == ITEM_TYPE.ITEM_TYPE_IMAGE.ordinal()) { return new MyIVHolder(mLayoutInflater.inflate(R.layout.rv_image_item, parent, false)); } else { return new MyTVHolder(mLayoutInflater.inflate(R.layout.rv_item, parent, false)); } } @Override public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) { if (holder instanceof MyTVHolder) { ((MyTVHolder) holder).mTextView.setText(mArray[position]); } else if (holder instanceof MyIVHolder) { Glide.with(mContext) .load(mImageId[position % 3]) .centerCrop() .into(((MyIVHolder) holder).mImageView); ((MyIVHolder) holder).mTextView.setText(mArray[position]); } if (mOnItemClickListener != null) { holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int pos = holder.getLayoutPosition(); mOnItemClickListener.onItemClick(holder.itemView, pos); } }); holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { int pos = holder.getLayoutPosition(); mOnItemClickListener.onItemLongClick(holder.itemView, pos); return false; } }); } } 其实目的就是根据getItemViewType的设置加载不同布局. 来看看效果图: 不同布局加载 来看看横向滚动 一行代码足矣:gridLayoutManager.setOrientation(GridLayoutManager.HORIZONTAL); 横着滚 还有瀑布流 对, 还有瀑布流. 记得注释掉GridLayoutManager设置宽度那部分. StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL); rvTest.setLayoutManager(layoutManager); 瀑布流 最后 这是第二篇的全部内容, 感兴趣的还有第三篇哦, 或者你还没有看第一篇如果喜欢记得点赞或者关注我哦.
目录 前言 shape绘制 矩形 椭圆 线 环 用shape绘制SeekBar 最后 前言 在没有UI设计师的时候, 或者是想简单看下效果的时候, 用shape进行快速绘制是极好的! 官方文档. shape绘制 一共有四种shape: rectangle, oval, line, ring. 矩形 我们一个一个来看, 首先是矩形: 矩形例子 <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <!-- 尺寸 --> <size android:width="160dp" android:height="80dp" /> <!-- 颜色 --> <!--<solid android:color="@color/colorPrimary" />--> <!-- 内间距 --> <padding android:bottom="8dp" android:left="8dp" android:right="8dp" android:top="8dp" /> <!-- 渐变 --> <gradient android:angle="45" android:endColor="@color/colorPrimary" android:startColor="@color/colorAccent" android:type="linear" /> <!-- 圆角 --> <!--<corners android:radius="200dp" />--> <!-- 圆角单独设置 --> <corners android:bottomLeftRadius="0dp" android:bottomRightRadius="0dp" android:topLeftRadius="40dp" android:topRightRadius="40dp" /> <!-- 描边 --> <stroke android:width="2dp" android:color="#666" android:dashGap="4dp" android:dashWidth="4dp" /> </shape> 渐变gradient是会覆盖颜色的, 如果你想要纯色, 直接设置颜色值即可, 就是设置solid中的color. 顺带一提, solid只有color一个参数. 如果你没有渐变gradient, 也不写solid, 那么将会是空心的. 渐变gradient的type参数有3个: linear 线性渐变 sweep 扫描渐变 radial 放射渐变, 需要配合参数gradientRadius 圆角corners可以直接设置radius, 也可以一个一个指定. 描边stroke的话不写dashGap, dashWidth就会是实线, dashWidth代表虚线宽度, dashGap代表虚线间隔. 内间距padding和尺寸size就不提了, 大家都懂的. 椭圆 椭圆例子 <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <!-- 尺寸 --> <size android:width="160dp" android:height="80dp" /> <!-- 颜色 --> <!--<solid android:color="@color/colorPrimary" />--> <!-- 内间距 --> <padding android:bottom="8dp" android:left="8dp" android:right="8dp" android:top="8dp" /> <!-- 渐变 --> <gradient android:centerColor="@color/colorPrimary" android:endColor="@color/colorPrimaryDark" android:startColor="@color/colorAccent" android:type="sweep" /> <!-- 描边 --> <stroke android:width="1dp" android:color="#333" /> </shape> 渐变是最多可以设置三种颜色, 意思一看便知了: startColor centerColor endColor 一般椭圆都会用来绘制实心的小圆点. 线 线就很简单了: 线例子 <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="line"> <!-- 描边 --> <stroke android:width="8dp" android:color="@color/colorPrimary" android:dashGap="8dp" android:dashWidth="6dp" /> </shape> 环 最后来看环, 它有些特有属性: 环例子B <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:innerRadiusRatio="4" android:shape="ring" android:thicknessRatio="100" android:useLevel="false"> <!-- 尺寸 --> <size android:width="200dp" android:height="200dp" /> <!-- 渐变 --> <gradient android:angle="0" android:centerColor="@color/colorPrimaryDark" android:endColor="@color/colorPrimary" android:startColor="@color/colorAccent" android:type="sweep" /> <!-- 描边 --> <stroke android:width="1dp" android:color="#777" android:dashGap="4dp" android:dashWidth="4dp" /> </shape> thicknessRatio 指的是环厚度百分比, 默认是9, 比如说这里宽度是200dp, thicknessRatio是100, 环厚度就是200dp / 100 = 2dp. 当然, 你可以直接用thickness设置厚度. innerRadiusRatio 是内环百分比, 默认是3, 就是指用宽度 / 百分比得到的值就是内环半径. 同样可以用innerRadius直接设置. 用shape绘制SeekBar 我知道有很多非常好看的自定义进度条, 但是我写这个SeekBar是想补充下shape的使用, 用非常少量的代码实现自定义进度条. 来看看效果图: 用shape绘制SeekBar 实现 <SeekBar android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="@dimen/eight_dp" android:max="200" android:maxHeight="@dimen/eight_dp" android:minHeight="@dimen/eight_dp" android:progressDrawable="@drawable/layout_progress" android:thumb="@drawable/shape_circle" /> 简单解释下几个要点属性: max代表进度条最大的值. maxHeight, minHeight可以设置进度条宽度, 我喜欢稍微宽一点的. thumb设置滑块, 可以是图片, 可以是shape写的设置. progressDrawable代表进度条的外观, 可以是图片, 可以是shape写的设置. 再来看看滑块和进度条外观具体代码, 进度条可以设置背景, 进度, 和第二进度. 滑块的话, 你想画成什么样都行. <?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@android:id/background"> <shape> <corners android:radius="@dimen/four_dp" /> <solid android:color="@android:color/darker_gray" /> </shape> </item> <item android:id="@android:id/secondaryProgress"> <clip> <shape> <corners android:radius="@dimen/four_dp" /> <solid android:color="@color/colorAccent" /> </shape> </clip> </item> <item android:id="@android:id/progress"> <clip> <shape> <corners android:radius="@dimen/four_dp" /> <solid android:color="@android:color/holo_blue_light" /> </shape> </clip> </item> </layer-list> <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="@android:color/holo_blue_light" /> <stroke android:width="@dimen/one_dp" android:color="@android:color/holo_blue_light" /> <size android:width="@dimen/sixteen_dp" android:height="@dimen/sixteen_dp" /> </shape> java部分的话, 用Handler实例postDelayed方法让进度条跑起来就可以看到效果了. 这里设定50ms发一次消息. findViewById(R.id.cv_start).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mRunnable == null) { mRunnable = new MyRunnable(); mHandler.postDelayed(mRunnable, 0); } } }); findViewById(R.id.cv_stop).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mHandler.removeCallbacks(mRunnable); mRunnable = null; } }); private class MyRunnable implements Runnable { @Override public void run() { int progress = mSbTest.getProgress(); mTvProgress.setText(String.valueOf(progress)); mSbTest.setProgress(++progress); mSbTest.setSecondaryProgress(progress + 10); int progress2 = mSbTest2.getProgress(); mTvProgress2.setText(String.valueOf(progress2)); mSbTest2.setProgress(++progress2); mSbTest2.setSecondaryProgress(progress2 + 20); mHandler.postDelayed(this, 50); } } 最后 我个人还是偏向用shape绘制的, 图片一出理不好就是内存溢出啊, 形变啊, 还要注意分辨率, 真心头大. 喜欢记得点赞哦, 暗中关注我也是可以的~
目录 前言 初步了解手势操作 六个重写方法 小栗子体验一下 别忘了双击事件 最后 前言 手势操作是Android交互当中至关重要的. 可以说, 如果一个软件没有好的手势操作, 就不能将其定义为移动端的软件. 这里来看下谷歌提供的GestureDetector类. 搭配属性动画使用, 效果更好哦. 初步了解手势操作 先来一段可以快速了解手势操作类的代码. 这里用到了GestureDetector.SimpleOnGestureListener(), 可以暂时理解为是一个适配器, 快速实现GestureDetector.OnGestureListener接口中要实现的方法. 这样的话, 你就可以按需实现, 不用全部重写所有方法. public class MainActivity extends AppCompatActivity { private GestureDetector mGestureDetector; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button btHello = (Button) findViewById(R.id.bt_hello); mGestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { Toast.makeText(MainActivity.this, "onDown", Toast.LENGTH_SHORT).show(); return false; } @Override public void onShowPress(MotionEvent e) { Toast.makeText(MainActivity.this, "onShowPress", Toast.LENGTH_SHORT).show(); } @Override public boolean onSingleTapUp(MotionEvent e) { Toast.makeText(MainActivity.this, "onSingleTapUp", Toast.LENGTH_SHORT).show(); return false; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { Toast.makeText(MainActivity.this, "onScroll", Toast.LENGTH_SHORT).show(); return false; } @Override public void onLongPress(MotionEvent e) { Toast.makeText(MainActivity.this, "onLongPress", Toast.LENGTH_SHORT).show(); } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { Toast.makeText(MainActivity.this, "onFling", Toast.LENGTH_SHORT).show(); return false; } }); btHello.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return mGestureDetector.onTouchEvent(event); } }); } } 可以看到手势操作其实就是丰富了原来的触摸事件. 在触摸操作实现那里进行拦截. 然后有六个可重写的方法. 来分析一下这几个方法. 六个重写方法 onDown 这个很好理解了, 就是按下的操作. onShowPress 这个的意思是按压. 什么是按压, 就是比按下更用力, 更多一点时间. onLongPress 这个就是长按了. 比方说你一直按着按钮, 那么执行顺序就是onDown-->onShowPress-->onLongPress. onSingleTapUp 这个就是单点抬手. 就是你单击按钮抬手的那下. 但是有一点要注意. 如果你点击时长小于onLongPress, 它是会触发的, 如果大于onLongPress, 它就不会触发了. onScroll 拖动相应. 就是拖着某个控件, 这个就会触发. onFling 滑动相应. 就是快速划一下, 它就会触发. 小栗子体验一下 也说了一些, 我们来体验下. 我们拎着按钮绕了一圈, 我们看下后台的打印. 绕圈 onDown onShowPress onScroll onScroll onScroll onScroll onScroll onScroll 然后我们再来一次, 先点击一下, 再绕一圈. 点击时候发生了: onDown onShowPress onSingleTapUp 绕圈时候是: onDown onShowPress onScroll onScroll onScroll onFling 我们先看下点击事件, 点下去就是onDown, 稍微多点一会儿就会触发onShowPress, 然后抬手就是onSingleTapUp. 然后分析第一次绕圈, 点下去onDown, 停留onShowPress, 拖动onScroll, 之后就是多次拖动. 很好理解. 那么再看第二次, 多了一个onFling, 而且是在最后, 就是说, 我们的拖动操作, 最后抬手的时候会触发onFling, 但是不是100%. 所以处理的时候要区分这两种操作, 以免冲突. 别忘了双击事件 在pc上, 双击事件是非常常见的, 可以到了移动端, 这种操作就用的不那么多了. 不过我们还是来看看. 先上栗子: @Override public boolean onDoubleTap(MotionEvent e) { Toast.makeText(MainActivity.this, "onDoubleTap", Toast.LENGTH_SHORT).show(); LogUtil.i("onDoubleTap"); return super.onDoubleTap(e); } 双击按钮之后: onDown onShowPress onSingleTapUp onDoubleTap onDown onShowPress 这里出现了新加入的onDoubleTap, 也就是快速点击被识别到了. 为什么可以直接加入呢, 因为GestureDetector.SimpleOnGestureListener()之中也实现了GestureDetector.OnDoubleTapListener中的内容. 如果你不使用GestureDetector.SimpleOnGestureListener()的话, 可以使用如下代码实现GestureDetector.OnDoubleTapListener接口. class MyDoubleTap implements GestureDetector.OnDoubleTapListener{ @Override public boolean onSingleTapConfirmed(MotionEvent e) { return false; } @Override public boolean onDoubleTap(MotionEvent e) { return false; } @Override public boolean onDoubleTapEvent(MotionEvent e) { return false; } } 所以说GestureDetector.SimpleOnGestureListener()真的很好用啊, 但是如果你有多个对象要使用的话, 还是老实复写吧. 最后 还有很多新的手势操作, 绝对不止我文章中写的这些. 但是这些已经足够入门和大多数手势了. 喜欢记得点赞或者关注我哦.
目录 前言 ObjectAnimator的初步使用 用AnimatorSet进行动画混合 将动画写在xml中 动画监听 ViewPropertyAnimator上手 最后 前言 官方文档传送门 属性动画是非常非常好用的, 谷歌自己都说这是一个强大的框架. 那今天就来了解一下它. ObjectAnimator的初步使用 属性动画最大的特点就是可以让任何Object动起来, 我先给个小栗子, 大家感受一下. TextView tvTest = (TextView) findViewById(R.id.tv_test); float curTranslationY = tvTest.getTranslationY(); ObjectAnimator animator = ObjectAnimator.ofFloat(tvTest, "translationY", curTranslationY, curTranslationY + 100f); animator.setDuration(2000); animator.start(); 栗子 属性动画有个很重要的点就是说, 动画过后, 控件本身真的就变换了, 而不单单是绘制出了效果. 然后这里ofFloat()函数的第一个参数自然是控件了, 第二个参数是可以填入很多的, 比如"alpha", "rotation", 到底有多少, 大家可以移步官方文档. 然后后面的参数根据第二个参数来, 可多个, 这里可能说的不太清晰, 所以我们再来一个小栗子. TextView tvTest = (TextView) findViewById(R.id.tv_test); ObjectAnimator animator = ObjectAnimator.ofFloat(tvTest, "alpha", 1f, 0f, 1f, 0f, 1f, 0f, 1f); animator.setDuration(2000); animator.start(); 又见栗子 用AnimatorSet进行动画混合 一般来说, 让人感觉舒服的动画都不会是单一变换的动画, 肯定要各种动画混合一起, 来达到某种效果. 我这里进行一些混合的尝试, 顺便再展示几种动画. // 垂直移动 float curTranslationY = tvTest.getTranslationY(); ObjectAnimator translationY = ObjectAnimator.ofFloat(tvTest, "translationY", curTranslationY, curTranslationY + 500f); ObjectAnimator scaleY = ObjectAnimator.ofFloat(tvTest, "scaleY", 1f, 5f, 1f); ObjectAnimator scaleX = ObjectAnimator.ofFloat(tvTest, "scaleX", 1f, 5f, 1f); AnimatorSet animSet = new AnimatorSet(); animSet.play(scaleY).with(scaleX).after(translationY); animSet.setDuration(2000); animSet.start(); 混合动画 这里就是将垂直移动动画, 水平缩放和垂直缩放混合在一起, 大家肯定发现了, play(), with(), after()这几个函数. after(Animator anim) after中的动画先执行, 之后才是play中的动画. after(long delay) after中设置时间, 那么play中的动画会根据时间延迟执行. before(Animator anim) before中的动画后执行, play中的先执行. with(Animator anim) play中的动画和with中的一同执行. playTogether() 中间可以放入要一起执行的全部动画, 之后不可接after(), before()这些函数. 来个小栗子. AnimatorSet animSet = new AnimatorSet(); animSet.playTogether(translationY, scaleX, scaleY); animSet.setDuration(2000); animSet.start(); 动画混合 将动画写在xml中 写在xml中的好处不言而喻了, 复用性极强. 直接贴代码了, 很好理解的. <set xmlns:android="http://schemas.android.com/apk/res/android" android:ordering="sequentially"> <objectAnimator android:duration="1000" android:propertyName="translationY" android:valueFrom="0" android:valueTo="500" android:valueType="floatType" /> <set android:ordering="sequentially"> <objectAnimator android:duration="1000" android:propertyName="rotation" android:valueFrom="0" android:valueTo="360" android:valueType="floatType" /> <set android:ordering="together"> <objectAnimator android:duration="1000" android:propertyName="scaleX" android:valueFrom="1" android:valueTo="5" android:valueType="floatType" /> <objectAnimator android:duration="1000" android:propertyName="scaleY" android:valueFrom="1" android:valueTo="5" android:valueType="floatType" /> </set> </set> </set> 然后使用如下代码调用xml动画. AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(getApplicationContext(), R.animator.anim_set); set.setTarget(tvTest); set.start(); 动画监听 动画监听是很有必要知道的, 我们是在做软件, 不是在做电影, 不能让它一个劲播下去. 先看一个比较全面的监听. translationY.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); 这个很麻烦啦, 所以有适配版本的监听. 如果你用Android Studio它会弹出框让你选择. 选择复写的函数 translationY.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); } }); ViewPropertyAnimator上手 属性动画是谷歌在android3.0引入的, 而ViewPropertyAnimator则是3.1引入的, 这个ViewPropertyAnimator绝对可以说是个磨人的小妖精, 它绝对会让你爱上属性动画的, 看个栗子: tvTest.animate().translationY(curTranslationY + 500) .scaleX(5).scaleY(5) .setDuration(1000); ViewPropertyAnimator栗子 用ViewPropertyAnimator的目的就是精简代码以及快速实现, 想要处理一些复杂的动画, 还是要用上一篇说的内容慢慢来的(滑稽脸). 我们稍微修改一点代码, 看看新效果. tvTest.animate().translationYBy(250) .scaleX(5).scaleY(5) .setDuration(1000); By不By 其实可以看函数就看得出来意思了, 不加By代表直接移动某个值, 加了By代表在原有基础上移动某个值. 好, 我们再来看一个及其经典的代码, 可以完美展现出ViewPropertyAnimator的精简好用: view.animate()// 获取ViewPropertyAnimator对象 // 动画持续时间 .setDuration(5000) // 透明度 .alpha(0) .alphaBy(0) // 旋转 .rotation(360) .rotationBy(360) .rotationX(360) .rotationXBy(360) .rotationY(360) .rotationYBy(360) // 缩放 .scaleX(1) .scaleXBy(1) .scaleY(1) .scaleYBy(1) // 平移 .translationX(100) .translationXBy(100) .translationY(100) .translationYBy(100) .translationZ(100) .translationZBy(100) // 更改在屏幕上的坐标 .x(10) .xBy(10) .y(10) .yBy(10) .z(10) .zBy(10) // 插值器 .setInterpolator(new BounceInterpolator())//回弹 .setInterpolator(new AccelerateDecelerateInterpolator())//加速再减速 .setInterpolator(new AccelerateInterpolator())//加速 .setInterpolator(new DecelerateInterpolator())//减速 .setInterpolator(new LinearInterpolator())//线性 // 动画延迟 .setStartDelay(1000) //是否开启硬件加速 .withLayer() // 监听 .setListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }) .setUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { } }) .withEndAction(new Runnable() { @Override public void run() { } }) .withStartAction(new Runnable() { @Override public void run() { } }); 所以日常动画用它就足够了. 最后 有了属性动画, 界面就不会很死板了. 而且由于属性动画的特性, 让它可以完成动画部分的事, 甚至可以完成很多界面交互上的事. 喜欢记得点赞或者关注我哦.
界面无小事(一): RecyclerView+CardView了解一下界面无小事(二): 让RecyclerView展示更多不同视图界面无小事(三):用RecyclerView + Toolbar做个文件选择器界面无小事(四):来写个滚动选择器吧!界面无小事(五):自定义TextView界面无小事(六):来做个好看得侧拉菜单! 目录 前言 RecyclerView使用 CardView使用 RecyclerView.Adapter实现 给RecyclerView.Adapter添加点击事件 给CardView添加点击特效 为CardView添加更多内容 最后 前言 官方文档传送门RecyclerView是Google推荐用来替代ListView的. 整体使用感觉和ListView差不多, 但是比ListView是要多不少优点的. 想要使用它们, 先要添加依赖项哦. 版本看着填, 最好和appcompat-v7保持一致. compile 'com.android.support:cardview-v7:25.3.1' compile 'com.android.support:recyclerview-v7:25.3.1' RecyclerView使用 将RecyclerView添加到布局. <android.support.v7.widget.RecyclerView android:id="@+id/rv_test" android:layout_width="match_parent" android:layout_height="wrap_content" android:scrollbars="vertical" /> 在代码中找到RecyclerView, 添加布局管理器. 之后还要设置适配器, 我们到适配器那部分再说. 先用最简单的LinearLayoutManager. RecyclerView rvTest = (RecyclerView) findViewById(R.id.rv_test); rvTest.setLayoutManager(new LinearLayoutManager(this)); RecyclerView提供这些内置布局管理器:LinearLayoutManager以垂直或水平滚动列表方式显示项目GridLayoutManager在网格中显示项目StaggeredGridLayoutManager在分散对齐网格中显示项目 CardView使用 我们将每一个CardView视为RecyclerView的item, 所以就不加布局, 直接上CardView了. 就像ListView的item一样. 这段布局代码的android:foreground="@drawable/card_foreground"部分我们在给CardView加点击特效部分继续细说. <?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:card_view="http://schemas.android.com/apk/res-auto" android:id="@+id/cv_test" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="8dp" android:foreground="@drawable/card_foreground" card_view:cardCornerRadius="4dp"> <TextView android:id="@+id/tv_test" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="20dp" /> </android.support.v7.widget.CardView> RecyclerView.Adapter实现 RecyclerView.Adapter写起来并不难. 如果你的IDE是AS, 几乎可以依靠AS的提示完成代码. 代码就不分析了, 几乎和写ListView的时候一样. 但是注意, RecyclerView.Adapter这部分还没有完成, 在给RecyclerView.Adapter添加点击事件部分会增加回调代码. 然后要注意onCreateViewHolder的第二个参数viewType, 这里还没有用到. public class MyRVAdapter extends RecyclerView.Adapter<MyRVAdapter.MyTVHolder> { private final String[] mArray; private final LayoutInflater mLayoutInflater; private final Context mContext; public MyRVAdapter(Context context) { mArray = context.getResources().getStringArray(R.array.testArray); mLayoutInflater = LayoutInflater.from(context); mContext = context; } @Override public MyRVAdapter.MyTVHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new MyTVHolder(mLayoutInflater.inflate(R.layout.rv_item, parent, false)); } @Override public void onBindViewHolder(MyRVAdapter.MyTVHolder holder, int position) { holder.mTextView.setText(mArray[position]); } @Override public int getItemCount() { return mArray == null ? 0 : mArray.length; } public class MyTVHolder extends RecyclerView.ViewHolder { TextView mTextView; public MyTVHolder(View itemView) { super(itemView); mTextView = (TextView) itemView.findViewById(R.id.tv_test); } } } 到此, 已经可以运行看看效果了. 来个效果图吧! <?xml version="1.0" encoding="utf-8"?> <resources> <string-array name="testArray"> <item>hello java</item> <item>hello android</item> <item>hello recycler view</item> <item>hello card view</item> <item>hello java</item> <item>hello android</item> <item>hello recycler view</item> <item>hello card view</item> <item>hello java</item> <item>hello android</item> <item>hello recycler view</item> <item>hello card view</item> </string-array> </resources> 效果图 给RecyclerView.Adapter添加点击事件 但是和ListView不同, item的点击事件要我们自己写, 当然, 也没有多麻烦. 分成两个部分, 添加接口, 和设置监听. public interface OnItemClickListener { void onItemClick(View view, int position); void onItemLongClick(View view, int position); } private OnItemClickListener mOnItemClickListener; public void setOnItemClickListener(OnItemClickListener mOnItemClickListener) { this.mOnItemClickListener = mOnItemClickListener; } @Override public void onBindViewHolder(final MyRVAdapter.MyTVHolder holder, int position) { holder.mTextView.setText(mArray[position]); if (mOnItemClickListener != null) { holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int pos = holder.getLayoutPosition(); mOnItemClickListener.onItemClick(holder.itemView, pos); } }); holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { int pos = holder.getLayoutPosition(); mOnItemClickListener.onItemLongClick(holder.itemView, pos); return false; } }); } } 然后就可以和使用ListView时一样对item添加监听了. 在给CardView添加点击特效部分结束之后会给出完整的效果图. MyRVAdapter myRVAdapter = new MyRVAdapter(this); myRVAdapter.setOnItemClickListener(new MyRVAdapter.OnItemClickListener() { @Override public void onItemClick(View view, int position) { Toast.makeText(getApplicationContext(), "click: " + position, Toast.LENGTH_SHORT).show(); } @Override public void onItemLongClick(View view, int position) { Toast.makeText(getApplicationContext(), "long click: " + position, Toast.LENGTH_SHORT).show(); } }); rvTest.setAdapter(myRVAdapter); 给CardView添加点击特效 不像ListView是有个默认点击特效的, RecyclerView是需要自己来完成的. 但是由于要使用的水波纹特效在5.0才引入, 我们需要分开处理. 先是5.0之后的. 5.0后只要设置波纹颜色就好. <?xml version="1.0" encoding="utf-8"?> <ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/colorPrimary" /> 波纹点击 5.0之前就是选择器了, 注定是不好看的. 或者有其他方法可以救一下? <?xml version="1.0" encoding="utf-8"?> <inset xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/card_foreground_selector" android:insetBottom="4dp" android:insetLeft="3dp" android:insetRight="3dp" android:insetTop="4dp" /> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true"> <shape android:shape="rectangle"> <solid android:color="@color/colorPrimaryTrans" /> <corners android:radius="@dimen/card_radius" /> </shape> </item> <item android:state_enabled="true" android:state_focused="true"> <shape android:shape="rectangle"> <solid android:color="#0f000000" /> <corners android:radius="@dimen/card_radius" /> </shape> </item> </selector> 选择器 我再补充一点, 想要不同版本对应不同的点击特效, 要在资源文件目录建文件夹. 比如这里的drawable-v21就代表21及以上使用, 21以下使用drawable. 建包 为CardView添加更多内容 CardView不可能说只能显示文字, 那如何添加图标或者图片呢?我只能说异常简单, 看代码: <?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:card_view="http://schemas.android.com/apk/res-auto" android:id="@+id/cv_test" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="8dp" android:foreground="@drawable/card_foreground" card_view:cardCornerRadius="4dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_vertical" android:orientation="horizontal"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="4dp" android:src="@mipmap/ic_launcher" /> <TextView android:id="@+id/tv_test" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="20dp" android:textSize="16sp" /> </LinearLayout> </android.support.v7.widget.CardView> 再来个效果图: 2018-04-12_20-59-39.gif-468.1kB 最后 基本算是RecyclerView的入门了, 喜欢记得点赞或者关注我, 如果感兴趣还有第二篇哦~~
Android小知识10则(下) 目录 前言 横竖屏锁定 不同分辨率的图标 将字符串写在资源文件中 为AlertDialog设置点击监听 ProgressDialog了解一下 最后 前言 Android的知识还是比较碎的, 日常积累很重要. 我把平常一些小知识点整合整合, 理成了上下两篇文章, 每篇五个知识点. 横竖屏锁定 有两种方法可以实现 使用配置文件 在AndroidManifest.xml的activity标签下添加android:screenOrientation="portrait"即可.默认是android:screenOrientation="unspecified", 也就是根据系统当前的横竖屏状态切换. 使用Java代码 使用setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);是锁定横屏. 使用setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);是锁定竖屏. 个人推荐写在配置文件里面. 来两张图: 配置文件 Java代码 不同分辨率的图标 由于Android的分辨率很不统一, 小到4寸左右, 大到10寸左右. 屏幕适配是很恶心人的问题. 这里就简单说一下如何添加不同分辨率的图标到工程当中. 首先看一下官方的例子, 我们的目标就是让我们的图标按照不同分辨率, 放在一个同名文件夹中. 官方例子 右击mipmap文件夹, 在目录中打开, 我的是mac所以显示finder, win/linux的话好像是文件浏览器之类的名称吧, 位置是差不多的. 右击mipmap文件夹 然后就很简单了, 你只需要把不同分辨率的图标, 由小到大逐一放入到mipmap-hdpi到mipmap-xxxhdpi中即可. 当然了, drawable也是一样, 从drawable-hdpi到drawable-xxxhdpi. 回到Android Studio就会有想要的效果了, 用的时候只要输入图片名称即可, 系统会自动选择最合适的图片显示. 当然说是这么说, 其实布局文件还得按照分辨率来写不是, 没准还要区分版本(手动无奈). 将字符串写在资源文件中 将字符串写在资源文件中好处是不言而喻的, 便于管理, 便于查找, 便于维护, 便于全球化等等, 而且上传github的时候, 它也会死命提示你的(手动滑稽). 那就来看看这个知识点吧. 单个字符串 单个字符串的使用是非常简单的. 将字符串写在strings.xml中, java直接引用就好. 或者用getResources().getString()方法 <string name="single_str">SingleStr</string> TextView tvSingleStr = (TextView) findViewById(R.id.tv_single_str); tvSingleStr.setText(R.string.single_str); getResources().getString(R.string.app_name); 字符串数组 字符串数组的使用稍微麻烦点. 同样在strings.xml中写一个字符串数组. 然后在java里面引用. <string-array name="str_arr"> <item>a</item> <item>b</item> <item>c</item> <item>d</item> </string-array> String[] stringArray = getResources().getStringArray(R.array.str_arr); TextView tvStrArr = (TextView) findViewById(R.id.tv_str_arr); tvStrArr.setText(Arrays.toString(stringArray)); 格式化字符串 这个格式化字符串在c里面是家常便饭, 但是到了java可能就没那么好使了. 总之, 也还是可以用的. 步骤还是一样, 先strings.xml, 后java. <string name="format_str">我买了%1$d个苹果, 花了%2$.2f元, 抽到了%3$s去夏威夷的票, 用手比了一个%4$c</string> String formatStr = String.format(getResources().getString(R.string.format_str), 5, 3.2, "两张", 'v'); TextView tvFormatStr = (TextView) findViewById(R.id.tv_format_str); tvFormatStr.setText(formatStr); 为AlertDialog设置点击监听 为列表和单选列表设置监听 关于AlertDialog的初步使用请参见之前的文章. 然后来看具体添加监听的代码. 列表 直接在setItems后面添加监听函数, 或者新建类实现监听接口都是可以的, 要说的是, which对应点击的条目, 从0开始. 比如说点击"apple"就是0, 点击"boy"就是1. builder.setItems(new String[]{"apple", "boy", "cat"}, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Log.i("sorrower", "which: " + which); } }); 单选列表 单选列表大体和列表相同, 值得一提的就是setSingleChoiceItems的第二个参数如果是-1, 就代表都不选, 如果是0, 代表默认选中第一个, 这里的话就是"apple". builder.setSingleChoiceItems(new String[]{"apple", "boy", "cat"}, -1, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Log.i("sorrower", "which: " + which); } }); 为AlertDialog自带按钮设置监听 每个AlertDialog默认有三个按钮的, 肯定否定和中性. 这里不使用匿名监听, 我们新建一个类实现监听接口. builder.setPositiveButton("positive", new clickListener()); builder.setNegativeButton("negative", new clickListener()); builder.setNeutralButton("neutral", new clickListener()); class clickListener implements DialogInterface.OnClickListener { @Override public void onClick(final DialogInterface dialog, int which) { Log.i("sorrower", "which: " + which); } } 然后得到的结果和猜想的不太一样, 点击NeutralButton, which的值是-3; 点击NegativeButton, which的值是-2; 点击PositiveButton, which的值是-1. 我们可以使用which值区分用户点击. ProgressDialog了解一下 进度条弹窗还是很实用的, 今天就带大家了解一下! 先来张效果图: 效果图 final ProgressDialog pd = new ProgressDialog(this); pd.setIcon(R.mipmap.ic_launcher_round); pd.setTitle("Dealing..."); pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); // 设置为矩形进度条 pd.setCancelable(false); pd.setIndeterminate(false); // 设置进度条是否为不明确 pd.setMax(100); pd.setProgress(0); pd.show(); new Thread() { @Override public void run() { for (int i = 0; i < 100; i++) { try { Thread.sleep(80); } catch (InterruptedException e) { e.printStackTrace(); } final int j = i + 1; pd.setProgress(j); } pd.dismiss(); } }.start(); 如果你改一行pd.setIndeterminate(true);, 那么就不会显示百分比, 和处理具体数. 直接上图: 效果图 如果你注释掉pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);这一行. 那么就是那种圆形扫描式的进度条, 你见过的. 上图: 效果图 最后 还有下篇的五个知识点哦, 觉得不错记得点赞或者关注我哦~
前言 我惊奇地发现我竟然没有写过Java安装的文档(可能是需要的地方太多, 反而没在意了), 借着这次写安装Tomcat的, 加一波Java安装. 这次平台选择了Win10(主要是FastStone Capture截图真的太好用), 但是, 但是, 我依旧推荐你使用Linux, 可以使用Ubuntu 16.04LTS或者Linux Mint 18.x, 这里打个广告, 是我之前一篇美化Linux的文章. 然后对android的ndk开发有兴趣的, 要配置环境的, 可以看我之前另一篇文章. 多图预警!转载请注明出处!!! 安装Java 官网下载或者是某盘链接_Java8_win_x64 密码:la5p 然后我们一步一步来看 运行下载的exe程序 设置jdk路径 设置jdk路径 然后是设置jre路径 设置jre路径 等待安装完成 安装完成 右键此电脑进入属性 点击高级系统设置 高级系统设置 点击环境变量 环境变量 然后就是Java环境变量三连了. 添加JAVA_HOME 添加PATH 添加CLASSPATH 复制对应jdk路径到JAVA_HOME环境变量 JAVA_HOME环境变量 PATH和CLASSPATH是固定写法 CLASSPATH PATH 然后如图所示 然后就是检测安装是否成功了, 点击win + x, 选择运行, 进入命令行 输入java, 如图代表成功, 找不到指令代表失败. 输入javac, 如图代表成功, 找不到指令代表失败. 输入java -version查看版本, 如图代表成功, 找不到指令代表失败. java javac java -version 安装Tomcat9.0 官网下载或者是某盘链接Tomcat9.0.4_win_x64 密码:htz2 解压Tomcat9.0.4压缩包, 复制路径, 添加到TOMCAT_HOME环境变量, 需要新建的哦. TOMCAT_HOME 在新建一个CATALINA_HOME环境变量, 复制同样的路径 CATALINA_HOME 在PATH和CLASSPATH中添加, 如图所示 CLASSPATH PATH 打开命令行输入service install Tomcat9安装 安装 打开控制面板->系统和安全->管理工具, 点击服务 找到如图所示的Tomcat9.0服务, 右键开启 然后解压目录的bin目录下, 有Tomcat的开启和关闭的批处理命令 开启和关闭的批处理命令 打开浏览器键入http://localhost:8080测试一下, 出现如图所示代表成功了哦. 成功 最后 Java也好, Tomcat也好, 都是很实用的啦, 早点掌握还是有必要的. 喜欢记得点赞哦, 有意见或者建议评论区哦, 当然暗中关注我也是可以的.
下载apk试用 密码: wjep去github看源码 前言 马上就要过年了, 做一个App来送祝福是不错的哦, 这里我考虑用ViewPager来做, 大家可以考虑用其它的试试看哦. 效果图 不废话, 先上图. 可以认为分成两部分, 先是一个闪屏页, 然后再是滑动页. 效果图 闪屏页 布局图 闪屏页不难做, 关键是动画的设置. 直接上代码. public class SplashActivity extends AppCompatActivity { private LinearLayout ll_splash; private AnimationSet animationSet; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_splash); initUI(); initAnim(); //4. 启动动画 ll_splash.setAnimation(animationSet); animationSet.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { Intent intent = new Intent(getApplication(), GuideActivity.class); startActivity(intent); finish(); } @Override public void onAnimationRepeat(Animation animation) { } }); } /** * 初始化动画Animation */ private void initAnim() { //1.1 旋转动画 RotateAnimation rotateAnimation = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); //1.2 设置时长 rotateAnimation.setDuration(500); // rotateAnimation.setRepeatCount(1); // rotateAnimation.setRepeatMode(Animation.REVERSE); //1.3 保持动画结束 rotateAnimation.setFillAfter(true); //2. 缩放动画 ScaleAnimation scaleAnimation = new ScaleAnimation(0, 1, 0, 1, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); scaleAnimation.setDuration(500); scaleAnimation.setFillAfter(true); //3. 渐变动画 AlphaAnimation alphaAnimation = new AlphaAnimation(0, 1); alphaAnimation.setDuration(1000); alphaAnimation.setFillAfter(true); //4. 动画集合 animationSet = new AnimationSet(true); animationSet.addAnimation(rotateAnimation); animationSet.addAnimation(scaleAnimation); animationSet.addAnimation(alphaAnimation); } /** * 初始化界面ui */ private void initUI() { ll_splash = (LinearLayout) findViewById(R.id.ll_splash); } } 解析: 来简单解析一下, 第一步就是创建各种动画(你可以弄得简约一些, 也可以夸张一些), 然后添加到一个动画集合当中, 设置给我们的视图. 之后还要监听下动画结束, 在结束之后调用下一个activity并且关闭当前的activity, 然后就到了滑动页. 滑动页 来看看滑动页布局代码, 因为有些视图是在代码中生成, 所以布局图看不出效果. <?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:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.so.happynewyear.activity.GuideActivity"> <android.support.v4.view.ViewPager android:id="@+id/vp_pager" android:layout_width="match_parent" android:layout_height="match_parent"> </android.support.v4.view.ViewPager> <Button android:id="@+id/bt_start" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_marginBottom="52dp" android:background="@drawable/btn_guide_selector" android:onClick="toMain" android:paddingLeft="12dp" android:paddingRight="12dp" android:text="@string/bt_start" android:textColor="@drawable/color_guide_selector" android:visibility="invisible" /> <RelativeLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_marginBottom="20dp"> <LinearLayout android:id="@+id/ll_guide_point" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal"> </LinearLayout> <ImageView android:id="@+id/iv_red_point" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/shape_point_red" tools:ignore="ContentDescription" /> </RelativeLayout> </RelativeLayout> 滑动页就没有这么简单了, 当然了, 如果你有一个自己已经写好的PagerAdapter子类就要舒服多了. 适配器的代码我就不添了, 可以去看源码. /** * 初始化数据 */ private void initData() { imageViews = new ArrayList<>(); for (int i = 0; i < mImageIds.length; i++) { //1. 添加图片资源 ImageView view = new ImageView(this); view.setBackgroundResource(mImageIds[i]); imageViews.add(view); //2. 初始化导航圆点 ImageView point = new ImageView(this); //3. 修改属性值 LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); if (i > 0) { params.leftMargin = 32; } point.setLayoutParams(params); //4. 添加导航圆点 point.setImageResource(R.drawable.shape_point_gray); ll_guide_point.addView(point); } } 解析: 先要初始化数据, 向ArrayList中添加图片和同等数量的圆点. 不要忘了给圆点设置外边距, 否则就难看了. @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_guide); initUI(); initData(); vp_pager.setAdapter(new GuideAdapter(this, imageViews)); vp_pager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { float leftMargin = mPointsDis * (position + positionOffset); RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) iv_red_point.getLayoutParams(); params.leftMargin = (int) leftMargin; iv_red_point.setLayoutParams(params); } @Override public void onPageSelected(int position) { if (position == imageViews.size() - 1) { bt_start.setVisibility(View.VISIBLE); } else { bt_start.setVisibility(View.INVISIBLE); } } @Override public void onPageScrollStateChanged(int state) { } }); iv_red_point.getViewTreeObserver().addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { iv_red_point.getViewTreeObserver().removeOnGlobalLayoutListener(this); //measure->layout->draw(在onCreate执行完成之后执行) //layout方法执行结束之后的回调 mPointsDis = ll_guide_point.getChildAt(1).getLeft() - ll_guide_point.getChildAt(0).getLeft(); Log.i(TAG, "mPointsDis: " + mPointsDis); } }); } 在填充好ArrayList之后, 我们要对滑页进行监听, 主要有两个目的: 第一个就是前面的页面都是不需要按钮的, 最后一个页面要加上一个按钮, 可以用来关闭App或者是跳转到其它activity. 第二个就是滑动页面的时候让下面的小红点(代表当前页所在位置)一起跟着动. 有一个麻烦的点就是说, 要等视图绘制完成了我们才可以计算出两个小圆点之间的距离, 但是我们现在在onCreate之中, 所以我这里加了一个监听, 绘制(onLayout)完成会回调我们这里加的监听, 当然我们监听以此就可以了, 之后不需要了, 记得取消iv_red_point.getViewTreeObserver().removeOnGlobalLayoutListener(this);. 最后 行了, 就说到这里. 大家完全可以在我的代码的基础上加上更多有趣的东西, 比如播放语音啊, 贴上照片啊, 或者改成情人节告白App也是妥妥的哦. 喜欢记得点赞, 有意见或者建议评论区见, 暗中关注我也没问题哦. 下载apk试用 密码: wjep去github看源码
前言 弹窗是图形界面必备的一个模块, 回忆一下windows那些恶心爆了的错误弹窗吧, 把弹窗制作的更高效友好一点是非常必要的. 这里说两个常用的弹窗类, PopupWindow和AlertDialog. 我的理解就是, PopupWindow较为随性, 可以在任意位置弹窗, 比如你经常看到的朋友圈点赞的那个小的弹窗. 那AlertDialog就很正经了, 位置固定在中央, 比如无比烦人的更新提示就是用的它, 大多数都是消息标题+内容+确定按钮+取消按钮. 好, 不多废话了. 为了保护你的眼睛, 图片已处理 PopupWindow 官方文档传送门 实例解析 先来看一段常规的PopupWindow的使用, 然后逐行分析下. PopupWindow popupWindow = new PopupWindow(); popupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); popupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); popupWindow.setContentView(View.inflate(this, R.layout.layout_popup, null)); popupWindow.setBackgroundDrawable(new ColorDrawable(0x00000000)); popupWindow.setFocusable(true); popupWindow.setOutsideTouchable(false); popupWindow.setAnimationStyle(R.style.anim_menu_bottombar); popupWindow.showAsDropDown(bt_popup, 0, 0); 解析: 首先肯定是创建一个PopupWindow对象了, 当然, 它肯定还有许多带参数的构造方法, 而无参肯定是最好理解的那种. 然后就是设置三连, 设置宽高, 设置布局View. 如果想要显示一个弹窗, 这三句话是必须的. 然后popupWindow.setBackgroundDrawable(new ColorDrawable(0x00000000));这个很有意思. 在高版本的android中(比如8.0), 实测可以不写, 但是低版本就不行了(比如4.1), 低版本不写的话, 会导致点击返回或者是屏幕其它地方无法取消弹窗, 所以稳妥起见还是加上, 并设置一个透明色. popupWindow.setFocusable(true);是比较重要的, 一般都为true, 也就是弹窗之后, 焦点就到了弹窗, 你再点击其它地方, 弹窗失去焦点就会消失. popupWindow.setOutsideTouchable(false);这句在之前那句为true的前提下, true和false效果几乎一样. 再往下是添加一个动画效果, 你可以用默认的, 或者自定义. 最后一句显示弹窗, 默认对齐左下, 后面两个参数是偏移值, 应该很好理解啦. 然后我们来看一张效果图. 效果图 内容补充 RelativeLayout rl_content = (RelativeLayout) findViewById(R.id.rl_content); popupWindow.showAtLocation(rl_content, Gravity.CENTER, 0, 0); 补充: 显示还有一种方法, showAtLocation(). 举个栗子, 就是如上代码, 先获取一个布局, 然后设置Gravity.CENTER, 以及偏移量, 这样就会把弹窗设置到布局中心加上偏移量的一个位置. AlertDialog 官方文档传送门 实例解析 解析: 先来看一下Module中的build.gradle, 关键是compile 'com.android.support:appcompat-v7:25.3.1', 版本要确保大于22, 因为22中引入了Material Design风格的Dialog(5.0引入的Material Design), 当然, 如果你用Android Studio, 这点基本无需担心. dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile 'com.android.support:appcompat-v7:25.3.1' compile 'com.android.support.constraint:constraint-layout:1.0.2' testCompile 'junit:junit:4.12' } 再者下面两句是不同的, 第二种实例化方法会导致5.0前和5.0后风格不统一, 这里来两张效果图. 用API16的虚拟机. android.support.v7.app.AlertDialog.Builder builder = new android.support.v7.app.AlertDialog.Builder(this); AlertDialog.Builder builder = new AlertDialog.Builder(this); 右侧是Material Design风格 好, 接下来进入正文. 我们构建一个最简单的弹窗. 当然, 以下代码可以浓缩成一行代码, 但是不够直观, 我更喜欢清晰一点的代码. android.support.v7.app.AlertDialog.Builder builder = new android.support.v7.app.AlertDialog.Builder(this); builder.setIcon(R.mipmap.ic_launcher); builder.setTitle("title"); builder.setMessage("message"); builder.setPositiveButton("positive", null); builder.setNegativeButton("negative", null); builder.setNeutralButton("neutral", null); builder.setCancelable(true); android.support.v7.app.AlertDialog dialog = builder.create(); dialog.show(); 举个栗子 注意一点就是, setPositiveButton等函数第二个参数其实是监听器, 你可以如下操作. builder.setPositiveButton("positive", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Toast.makeText(getApplicationContext(), "positive", Toast.LENGTH_SHORT).show(); } }); 这行builder.setCancelable(true);就是意思点击弹窗以外的部分可以取消弹窗, 设置false则不取消弹窗, 按需设置啦. 然后就是AlertDialog是非常便于放入各种条目的, 比如单选和多选. 但是注意, 这之间会起冲突, 比如之前的builder.setMessage("message");和设置单选多选条目不能同时存在. 下方展示代码和效果图. 当然了, 监听我都没写, 按需求自己完成啦. builder.setItems(new String[]{"1", "2", "3"}, null); builder.setSingleChoiceItems(new String[]{"1", "2", "3"}, 2, null); //数字对应默认选中, 从0开始 builder.setMultiChoiceItems(new String[]{"1", "2", "3"}, new boolean[]{true, true, false}, null); //boolean数组对应勾选 普通条目展示 单选多选条目展示 内容补充 补充: 我们现在来说一个比较复杂的, 也比较有意思的. 就是在弹窗中填充自定义view. 当然啦, 还有adapter的方法, 但是我暂时不打算在这次的文章中写, 因为用adapter的时候太多了, 可能要下次弄个单独的部分. 举个栗子 <?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:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp"> <EditText android:id="@+id/et_account" android:layout_width="match_parent" android:layout_height="wrap_content" tools:ignore="TextFields" /> <EditText android:id="@+id/et_password" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/et_account" tools:ignore="TextFields" /> <Button android:id="@+id/bt_cancel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_below="@id/et_password" android:text="@string/bt_cancel" tools:ignore="RtlHardcoded" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/et_password" android:layout_toLeftOf="@id/bt_cancel" android:text="@string/bt_login" tools:ignore="RtlHardcoded" /> </RelativeLayout> 随便写个布局文件, 然后我们在java中加入如下一行. 要说的是, 大家仔细看效果图, 会发现自定义View可以和builder.setMessage("message");共存, 但事实上, 好像没有这个必要, 我故意把一些非自定义的也展示出来, 其实自定义View完全可以全部自己写. builder.setView(View.inflate(this, R.layout.layout_login, null)); 最后 喜欢记得点个赞了, 有意见或者建议可以评论区见哦, 当然啦, 暗中关注我也是可以的.