Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

Python:渗透测试开源项目

$
0
0

sql注入工具:sqlmap
DNS安全监测:DNSRecon
暴力破解测试工具:patator
XSS漏洞利用工具:XSSer

Web服务器压力测试工具:HULK

SSL安全扫描器:SSLyze

网络

Scapy: send, sniff and dissect and forge network packets. Usable interactively or as a library

pypcapPcapy and pylibpcap: several different Python bindings for libpcap

libdnet: low-level networking routines, including interface lookup and Ethernet frame transmission

dpkt: fast, simple packet creation/parsing, with definitions for the basic TCP/IP protocols

Impacket: craft and decode network packets. Includes support for higher-level protocols such as NMB and SMB

pynids: libnids wrapper offering sniffing, IP defragmentation, TCP stream reassembly and port scan detection

Dirtbags py-pcap: read pcap files without libpcap

flowgrep: grep through packet payloads using regular expressions

Knock Subdomain Scan, enumerate subdomains on a target domain through a wordlist

Mallory, extensible TCP/UDP man-in-the-middle proxy, supports modifying non-standard protocols on the fly

Pytbull: flexible IDS/IPS testing framework (shipped with more than 300 tests)

调试和逆向工程

Paimei: reverse engineering framework, includes PyDBG, PIDA, pGRAPH

Immunity Debugger: scriptable GUI and command line debugger

mona.py: PyCommand for Immunity Debugger that replaces and improves on pvefindaddr

IDAPython: IDA Pro plugin that integrates the Python programming language, allowing scripts to run in IDA Pro

PyEMU: fully scriptable IA-32 emulator, useful for malware analysis

pefile: read and work with Portable Executable (aka PE) files

pydasm: Python interface to the libdasm x86 disassembling library

PyDbgEng: Python wrapper for the Microsoft Windows Debugging Engine

uhooker: intercept calls to API calls inside DLLs, and also arbitrary addresses within the executable file in memory

diStorm: disassembler library for AMD64, licensed under the BSD license

python-ptrace: debugger using ptrace (Linux, BSD and Darwin system call to trace processes) written in Python

vdb / vtrace: vtrace is a cross-platform process debugging API implemented in python, and vdb is a debugger which uses it

Androguard: reverse engineering and analysis of Android applications

Fuzzing

Sulley: fuzzer development and fuzz testing framework consisting of multiple extensible components

Peach Fuzzing Platform: extensible fuzzing framework for generation and mutation based fuzzing (v2 was written in Python)

antiparser: fuzz testing and fault injection API

TAOF, (The Art of Fuzzing) including ProxyFuzz, a man-in-the-middle non-deterministic network fuzzer

untidy: general purpose XML fuzzer

Powerfuzzer: highly automated and fully customizable web fuzzer (HTTP protocol based application fuzzer)

SMUDGE

Mistress: probe file formats on the fly and protocols with malformed data, based on pre-defined patterns

Fuzzbox: multi-codec media fuzzer

Forensic Fuzzing Tools: generate fuzzed files, fuzzed file systems, and file systems containing fuzzed files in order to test the robustness of forensics tools and examination systems

Windows IPC Fuzzing Tools: tools used to fuzz applications that use Windows Interprocess Communication mechanisms

WSBang: perform automated security testing of SOAP based web services

Construct: library for parsing and building of data structures (binary or textual). Define your data structures in a declarative manner

fuzzer.py (feliam): simple fuzzer by Felipe Andres Manzano

Fusil: Python library used to write fuzzing programs

Web

Requests: elegant and simple HTTP library, built for human beings

HTTPie: human-friendly cURL-like command line HTTP client

ProxMon: processes proxy logs and reports discovered issues

WSMap: find web service endpoints and discovery files

Twill: browse the Web from a command-line interface. Supports automated Web testing

Ghost.py: webkit web client written in Python

Windmill: web testing tool designed to let you painlessly automate and debug your web application

FunkLoad: functional and load web tester

spynner: Programmatic web browsing module for Python with Javascript/AJAX support

python-spidermonkey: bridge to the Mozilla SpiderMonkey JavaScript engine; allows for the evaluation and calling of Javascript scripts and functions

mitmproxy: SSL-capable, intercepting HTTP proxy. Console interface allows traffic flows to be inspected and edited on the fly

pathod / pathoc: pathological daemon/client for tormenting HTTP clients and servers

取证

Volatility: extract digital artifacts from volatile memory (RAM) samples

LibForensics: library for developing digital forensics applications

TrIDLib, identify file types from their binary signatures. Now includes Python binding

aft: Android forensic toolkit

恶意程序分析

pyew: command line hexadecimal editor and disassembler, mainly to analyze malware

Exefilter: filter file formats in e-mails, web pages or files. Detects many common file formats and can remove active content

pyClamAV: add virus detection capabilities to your Python software

jsunpack-n, generic JavaScript unpacker: emulates browser functionality to detect exploits that target browser and browser plug-in vulnerabilities

yara-python: identify and classify malware samples

phoneyc: pure Python honeyclient implementation

PDF

Didier Stevens' PDF tools: analyse, identify and create PDF files (includes PDFiDpdf-parserand make-pdf and mPDF)

Opaf: Open PDF Analysis Framework. Converts PDF to an XML tree that can be analyzed and modified.

Origapy: Python wrapper for the Origami Ruby module which sanitizes PDF files

pyPDF: pure Python PDF toolkit: extract info, spilt, merge, crop, encrypt, decrypt…

PDFMiner: extract text from PDF files

python-poppler-qt4: Python binding for the Poppler PDF library, including Qt4 support

Misc

InlineEgg: toolbox of classes for writing small assembly programs in Python

Exomind: framework for building decorated graphs and developing open-source intelligence modules and ideas, centered on social network services, search engines and instant messaging

RevHosts: enumerate virtual hosts for a given IP address

simplejson: JSON encoder/decoder, e.g. to use Google's AJAX API

PyMangle: command line tool and a python library used to create word lists for use with other penetration testing tools

Hachoir: view and edit a binary stream field by field

py-mangle: command line tool and a python library used to create word lists for use with other penetration testing tools

其他有用的Py库和工具

IPython: enhanced interactive Python shell with many features for object introspection, system shell access, and its own special command system

Beautiful Soup: HTML parser optimized for screen-scraping

matplotlib: make 2D plots of arrays

Mayavi: 3D scientific data visualization and plotting

RTGraph3D: create dynamic graphs in 3D

Twisted: event-driven networking engine

Suds: lightweight SOAP client for consuming Web Services

M2Crypto: most complete OpenSSL wrapper

NetworkX: graph library (edges, nodes)

Pandas: library providing high-performance, easy-to-use data structures and data analysis tools

pyparsing: general parsing module

lxml: most feature-rich and easy-to-use library for working with XML and HTML in the Python language

Whoosh: fast, featureful full-text indexing and searching library implemented in pure Python

Pexpect: control and automate other programs, similar to Don Libes `Expect` system

Sikuli, visual technology to search and automate GUIs using screenshots. Scriptable inJython

PyQt and PySide: Python bindings for the Qt application framework and GUI library

其他

Python 作为程序员的宠儿,越来越得到人们的关注,使用 Python 进行应用程序开发的越来也多。那么,在 2013 年有哪些流行的 Python 项目呢?下面,我们一起来看下。

  一、测试和调试

  • python_koans:Python Koans 算 “Ruby Koans” 的一部分,作为交互式教程,可以学习 TDD 技巧。
  • sure:Sure 是最适合自动化测试的 Python 工具,包含流利的断言、深度选择器等等特性。
  • responses:用 responses 能令测试更加轻松,这是一个可以伪装各种请求的库。
  • boom:Boom! Apache Bench 的替代品。作为一个命令行工具,Boom 能对你的应用进行快捷的 smoke test
  • cricketBeeWare 套件的一部分,cricket 是种图形化工具,协助你进行案例测试。
  • bugjarBeeWare 套件的一部分,bugjar 是针对 Python 的图形化交互式调试器。
  • pudb:pudn 是针对 Python 的全屏命令行调试器。
  • voltron:更好的 gdb 界面。

  二、Web 框架

  • django-stronghold:试过将 login_required 装饰器四处乱放? 在你的堡垒中令所有 Django 视图有默认 login_required 呗。
  • Falcon Framework:Falcon 自称为髙性能云接口框架,号称能在相同硬件条件下提高服务端性能 30 倍! 听起来有点儿意思?
  • django-xadmin:用 bootstrap 对 django-admin 进行了深度升级,提供了可插件安装的仪表盘。
  • clay:基于 Flask 的封装,能令我们轻松的创建 RESTful 后端服务,完整文档可查看 clay
  • flask-restful:基于 Flask 的简单框架,用以创建 REST 接口。
  • sandman:Sandman 希望通过 REST 接口暴露你现有的 app,相关 博客也值得一读。
  • Django Unchained:名字很髙大上,也的确是 Python Django 初学者的靠谱指南。

  三、并发

  • pulsar:部署新 web 服务器走起! 有趣的事件驱动的并发框架 ! 兼容从 2.6+ 到 pypy 的所有 python 版本!
  • toro:同步化的 Tornado 协程支持。
  • offset:Offset Go 的并发模式在 Python 中的实现,请参考相关演讲 幻灯来理解!

  四、任务调度

  • pyres:从 resque 获得灵感的纯 Python 任务调度模块,是 celery 的替代。
  • dagobah:Dagobah 是 Python 完成的简单关系依赖为基础的任务调度模块,还包含很 COOL 的关联任务工作流图形工具。
  • schedule:使用生成器模式来为定期任务生成配置的进程调度模块。

  五、实用工具

  • howdoi:发觉你总在 Google 一些简单的最简单的编程任务? howdoi 能让你远离浏览器,就解决这类事儿!
  • delorean:时间旅行?简单! Delorean 的目标就是令你的 Python 项目在处理时间/日期时轻而易举!查阅完备的 文档
  • powerline-shell:对于那些想让常用工具漂亮起来人,一定要用 powerline-bash,能打造漂亮的 shell 提示符,增加力线(powerline),兼容 Bash/Zsh。
  • fn.py:在 Python 中谈及函数编程时失落的那节"电池"终于出现了! 如果对 Python 函数式编程有兴趣的立即下手安装体验吧!
  • lice:为你的开源工程方便的追加许可证,而不用自个儿去 Google,支持 BSD、MIT 和 GPL 以及变种协议。
  • usblock:基于 USB 来锁定或是解锁你的笔记本!
  • Matchbox:MatchBox 能在你自个儿的服务器上提供类似 Dropbox 风格的备份服务! 基于 Flask 并通过 http 协议进行文件传输。
  • cleanify:用 cleanify 能异步美化你项目的所有 html/css/js 文件。
  • locksmith:Locksmith 是 AES 加密的口令管理器,看起来不错,完全开源,源代码、截屏都有。
  • storm:在 Storm 的命令行界面,管理你所有的 SSH 连接。
  • sqlparse::这个很给力! sqlparse 是个 SQL 有效性分析器,支持解析/分裂/格式化 SQL 语句。
  • autopep8:能自动化以 pep8来格式化你的代码。
  • colout:colout 用以在命令行中色彩化输出,这就从其 github page 查看范例来体验吧。
  • bumpversion:版本号冲撞总是恼人的,而每个人总是忘记给发行版本打 tag,bumpversion 用一条简单的命令简化了这方面的操作。
  • pyenv:需要更好的管理你 Python 的多版本环境 ? pyenv 让你能简洁的作到!(甚至超出你的预期!有插件能将 VirtualEnv 也无缝结合进来!)
  • pip-tools:一整套能令你的 Python 项目保持清爽的工具。
  • cdiff:Cdiff 是种非常 nice 的工具,可以用彩色输出统一 diff 格式信息,或用双栏形式来展示。

  六、数据科学及可视化

  • data_hacks:由 bitly 发布的一堆数据分析用命令行工具。这些工具接受命令行或是其它工具输入的数据,轻易的生成柱图以及直方图等等。
  • 给黒客的概率编程和贝叶斯方法:这书是极好的,介绍如何用贝叶斯方法和概率编程进行数据分析,而且每章都提供了用以 iPython Notebooks 的示例。
  • simmetrica:想对自个儿的应用基于时间的数据序列进行展示、汇总、分享嘛? 赶紧上 simmetrica 吧,同时还提供了可定制的仪表盘。
  • vincent: Python 构建的专为运用 D3.js 进行可视化的 vega 转换工具。
  • bamboo:一种简洁的实时数据分析应用,bamboo 提供了一个进行合并、汇总、数值计算的数据实时接口。
  • dataset:难以置信的工具,dataset 让对数据库的读写简单的象对 JSON 文件的操作,没有其它的文件配置,顷刻间就让你在 BOSS 面前高大上起来。
  • folium:喜欢地图?也爱 Python? Folium 让你在地图上自在操纵数据。
  • prettyplotlib:用 prettyplotlib 来强化你的 matplotlib,让你默认的 matplotlib 输出图片更加漂亮.
  • lifelines:有兴趣在 Python 中研究生存分析的话,不用观望了,用 lifelines! 包含对 Kaplan-Meier、Nelson-Aalen 和生存回归分析。

  七、编辑器及其改善

  • sublime-snake:想在无尽的 coding 中喘口气? 当然是这种经典游戏了……
  • spyderlib:又一个用 Python 写的开源 IDE。
  • vimfox:对于 Vim 党最贴心的 web 专发工具,VimFox 能让 vim 实时的看到 css/js/html 的修改效果,能神奇的让 vim 中的修订,立即在浏览器中看到。
  • pcode:基于 Py3 的 IDE,通过简单的 UI 提供了重构、工程管理等。

  八、持续交付

  • metrology:这个库很酷,支持你对应用进行多种测量并轻松输出给类似 graphite 的外部系统。
  • python-lust:支持在 Unix 系统中用 Python 实现一个守护进程。
  • scales:Scales 对你的 Python 应用进行持续状态和统计,并发送数据到 graphite
  • glances:跨平台,基于 curses 命令行的系统监视工具。
  • ramona:企业级的应用监管。 Ramona 保证每个进程在值,一但需要立即重启,并有监控/日志输出,会发送邮件提醒。
  • salmon:基于 Salt Stack 的多服务监视系统,即能作报警系统,也能当监控系统。
  • graph-explorer:Graph-explorer 是对 Graphite 面板的增强,比原版的好很多,值得体验。
  • sovereign:Sovereign 是一系列 ansible 的攻略手册,能为自个儿建造个私人云。
  • shipyard:能在指定的机器上弹出你的弹窗实例,也支持你创建/删除等等对弹窗的远程控制。
  • docker-py:疯狂的 docker 工程接口的 Python 包装。
  • dockerui:基于 docker 接口通过 web 界面进行交互操作的工具。
  • django-docker:如果想知道怎么将 Djnago 应用同 Docker 结合? 可以从这里学习。
  • diamond:Python 实现的守护进程,自动从你的服务或是其它指定数据源中提取数值,并 graphite以及其它支持的状态面板/收集系统输出。

  九、Git

  • git-workflow:可视化你的 git 工作流程的工具,示例: Demo
  • gitto:简洁的库,协助你建立自个儿的 git 主机。
  • git-imerge:git-imerge 能让 git 进行增量合并。 本质上是允许你在进行 imerge 有冲突时,有机会先合并掉,再继续。

  十、邮件与聊天

  • mailbox:Mailbox 是对 Python 的 IMAP 一个人性化的再造。 基于简单即是美的态度,作者对 IMAP 接口给出了一个简单又好理解的形式。
  • deadchat:deadchat 旨在不安全的网络环境中提供安全的单一房间群聊服务以及客户端。
  • Mailpile:Mailpile 是针对邮件的索引及搜索引擎 。

  十一、音频和视频

  • pms:穷人的 Spotify,搜索和收集音乐流!
  • dejavu:在琢磨 Shazam 的原理? 音频指纹识别算法的 Python 实现在此!(译注:Shazam:是个神奇的音乐识别应用,对她啍个几秒调子,就能精确告诉你是什么歌曲、作者、歌词……)
  • HTPC-Manager:为 HTPC 粉丝准备的工具,提供了完备的界面来管理所有家庭媒体服务器上的好物。
  • cherrymusic:Python 实现的一个音乐流媒体服务器。 流化输出你的音乐到所有设备上。
  • moviepy:脚本化的电影剪辑包,切/串/插入标题等基本操作,几行就搞定!

  十二、其它

  • emit:用 redis 为你的函式追加可订阅能力,很有趣。
  • zipline:Zipline 是种很 Pythonic 的交易算法库。
  • raspberry.io:Raspberry.io 是树莓派的社区实现。 刚刚发布,汇集了各种创意想法,有兴趣的话立即检出折腾吧。
  • NewsBlur:Google Reader 已经关张儿了,Newsblur 已经发布了有段日子了,开源的 RSS 阅读器,这绝对是应该首先体验的。
  • macropy:Macropy 是在 Python 中实现 macros 的库。 检出文档,参考所有功能,看怎么用上了。
  • mini:对编译器以及语言设计有兴趣的,一定要看看这个仓库,以及配套的录像!
  • parsimonious:Parsimonious 的目标是最快的 arbitrary-lookahead 解析器。 用 Python 实现,基本可用。
  • isso:Disqus 的开源替代,从 demo 看很不错,而且提供了更好的隐私设置。
  • deaddrop:Deaddrop 能为新闻机构或是其它人提供在线投递箱,详细信息参考其 github page
  • nude.py:裸体检测的 Python 实现,是 node.js 的仿制。
  • kaptan: Kaptan 是你应用的配置管理器!
  • luigi:Luigi 帮你构建复杂的管道来完成批处理。
  • gramme:Gramme 以简单而优雅的方式,通过 UDP 接口对易失数据完成消息包装序列化。
  • q:为你的 Python 程序提供快速而随性的日志。 有一系列帮手来追踪你的函式参数,并能在控制台快速交互式加载。
  • fuqit:来自伟大的 Zed Shaw 最新作品,fuqit 试图令你忘记 MVC 的经验,用全新的方式专注简洁一切。
  • simplicity:基于靠谱的 pydanny 将你的新结构化文本转换为 JSON 格式。
  • lassie:Lassie 允许你轻松的从网站检索出内容来。
  • paperwork:Paperwork 是个 OCR 文档并完成可搜索转化的工具,用 GTK/Glade 实现了友好的界面。
  • cheat:cheat 允许你创建并查阅命令行上的交互式备忘。设计目的是帮助 *nix 的系统管理员们在习惯的环境中,快速调阅不易记忆的常用命令。
  • cookiecutter:良心模块! 提供一堆有用但是不常写,所以记不下来的代码模板,也支持自制代码模板。
  • pydown:支持用 Python 构建亮丽的 HTML5 效果幻灯,Demo
  • Ice:模拟器粉丝们现在能用 Ice 向 Steam 里塞 ROM 来玩了。
  • pants:用以编写异步网络应用的轻量级框架。 Pants 是单线程,回调服务,也包含支持 Websockets 的 HTTP 服务、WSGI 支持和一个简单的 web 框架。
  • pipeless:Pipeless 是一个构建简单 数据管道的框架。
  • marshmallow:marshmallow 是个 ORM 无关的库,能将复杂的数据类型转换为 Python 原生类型对象,以便容易的转换为 JSON 提供接口使用。
  • twosheds:Python 实现的库,用来构造命令或是 shell 解释器。Twosheds 让你用 Python 来定制自个儿的 shell 环境。
作者:huayucong 发表于2016/11/30 0:07:19 原文链接
阅读:10 评论:0 查看评论

微信小程序开发之视频播放器 弹幕 弹幕颜色自定义

$
0
0

把录音的模块尝试过之后就想着微信小程序的视频播放会不会更有趣?

果然,微信小程序视频自带弹幕.是不是很爽,跟我一起来看看.

微信小程序开发之录音机 音频播放 动画 (真机可用)

先上gif:



再上几张图:

1.视频播放器


2.选择弹幕颜色


3.弹幕来了...



1.视频播放器

微信已经封装的非常好.我这里只用了很简单的几个属性

由于以前没做过弹幕,看到danmu-list就激动了.而且只需要将弹幕内容加入集合即可.

弹幕列表的元素:

 {
        text: '第 1s 出现的红色弹幕',//文本
        color: '#ff0000',//颜色
        time: 1//发送的时间
      }

其他的属性就不说了,以后遇到再细细研究.



2.选择弹幕颜色

从上面的弹幕列表元素可以看出,微信并没有给开发者太多的自定义空间.文本?时间?颜色?

也就颜色还能玩出点花样吧.

于是我就简单的做了个常用颜色的列表.算是自定义弹幕颜色吧


上代码:

ps:代码没整理,很烂,凑活着看吧.

1.index.wxml

<!--index.wxml-->
<view class="section tc">
  <video id="myVideo" style="height:{{videoHeight}}px;width:{{videoWidth}}px" src="http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400" binderror="videoErrorCallback" danmu-list="{{danmuList}}" enable-danmu danmu-btn controls></video>
  <view class="btn-area">
     <view class="weui-cell weui-cell_input">
                <view class="weui-cell__bd">
                    <input class="weui-input" placeholder="请输入弹幕" bindblur="bindInputBlur" />
                </view>
            </view>
  
    <button style="margin:30rpx;" bindtap="bindSendDanmu">发送弹幕</button>
  </view>
</view>
      <view class="weui-cells weui-cells_after-title">
            <view class="weui-cell weui-cell_switch">
                <view class="weui-cell__bd">随机颜色</view>
                <view class="weui-cell__ft">
                    <switch checked bindchange="switchChange" />
                </view>
   </view>
<view class="colorstyle"  bindtap="selectColor">
<text>选择颜色</text>
<view style="height:80rpx;width:80rpx;line-height: 100rpx;margin:10rpx;background-color:{{numberColor}}"></view>
</view>


2.index.wxss

(从别的项目粘过来的.哈哈)

/**index.wxss**/
.weui-cells {
  position: relative;
  margin-top: 1.17647059em;
  background-color: #FFFFFF;
  line-height: 1.41176471;
  font-size: 17px;
}
.weui-cells:before {
  content: " ";
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  height: 1px;
  border-top: 1rpx solid #D9D9D9;
  color: #D9D9D9;
  
}
.weui-cells:after {
  content: " ";
  position: absolute;
  left: 0;
  bottom: 0;
  right: 0;
  height: 1px;
  border-bottom: 1rpx solid #D9D9D9;
  color: #D9D9D9;
}
.weui-cells_after-title {
  margin-top: 0;
}
.weui-cell__bd {
  -webkit-box-flex: 1;
  -webkit-flex: 1;
          flex: 1;
}
.weui-cell__ft {
  text-align: right;
  color: #999999;
}

.weui-cell {
  padding: 10px 10px;
  position: relative;
  display: -webkit-box;
  display: -webkit-flex;
  display: flex;
  -webkit-box-align: center;
  -webkit-align-items: center;
          align-items: center;
}
.weui-cell:before {
  content: " ";
  position: absolute;
  top: 0;
  right: 0;
  height: 1px;
  border-top: 1rpx solid #D9D9D9;
  color: #D9D9D9;
  left: 15px;
}
.weui-cell:first-child:before {
  display: none;
}
.colorstyle{
    border-top: 2px solid #eee;
    border-bottom: 2px solid #eee;
    padding-left: 10px;
    padding-right: 10px;
    font-size: 17px;
    line-height: 100rpx;
    display: flex;
    flex-direction: row;
    justify-content:space-between;
}


3.index.js


//index.js
function getRandomColor() {
  let rgb = []
  for (let i = 0; i < 3; ++i) {
    let color = Math.floor(Math.random() * 256).toString(16)
    color = color.length == 1 ? '0' + color : color
    rgb.push(color)
  }
  return '#' + rgb.join('')
}

Page({
  onLoad: function () {
    var _this = this;
    //获取屏幕宽高  
    wx.getSystemInfo({
      success: function (res) {
        var windowWidth = res.windowWidth;
        //video标签认宽度300px、高度225px,设置宽高需要通过wxss设置width和height。
        var videoHeight = (225 / 300) * windowWidth//屏幕高宽比  
        console.log('videoWidth: ' + windowWidth)
        console.log('videoHeight: ' + videoHeight)
        _this.setData({
          videoWidth: windowWidth,
          videoHeight: videoHeight
        })
      }
    })
  },
  onReady: function (res) {
    this.videoContext = wx.createVideoContext('myVideo')
  },
  onShow: function () {
    var _this = this;
    //获取年数
    wx.getStorage({
      key: 'numberColor',
      success: function (res) {
        console.log(res.data + "numberColor----")
        _this.setData({
          numberColor: res.data,
        })
      }
    })
  },
  inputValue: '',
  data: {
    isRandomColor: true,//默认随机
    src: '',
    numberColor: "#ff0000",//默认黑色

    danmuList: [
      {
        text: '第 1s 出现的红色弹幕',
        color: '#ff0000',
        time: 1
      },
      {
        text: '第 2s 出现的绿色弹幕',
        color: '#00ff00',
        time: 2
      }
    ]
  },
  bindInputBlur: function (e) {
    this.inputValue = e.detail.value
  },
  bindSendDanmu: function () {
    if (this.data.isRandomColor) {
      var color = getRandomColor();
    } else {
      var color = this.data.numberColor;
    }

    this.videoContext.sendDanmu({
      text: this.inputValue,
      color: color
    })
  },
  videoErrorCallback: function (e) {
    console.log('视频错误信息:')
    console.log(e.detail.errMsg)
  },
  //选择颜色页面
  selectColor: function () {
    wx.navigateTo({
      url: '../selectColor/selectColor',
      success: function (res) {
        // success
      },
      fail: function () {
        // fail
      },
      complete: function () {
        // complete
      }
    })
  },
  //switch是否选中
  switchChange: function (e) {
    this.setData({
      isRandomColor: e.detail.value
    })
  }
})


4.selectColor.wxml

<!--selectColor.wxml-->
<view class="page">
    <view class="page__bd">
        <view class="weui-grids">
            <block wx:for-items="{{color}}"  >
                <view  class="weui-grid"  data-number="{{item.number}}" bindtap="selectColor" >
                    <view class="weui-grid__icon"  style="background:{{item.number}}"/>
                </view>
            </block>
        </view>
    </view>
</view>


5.selectColor.wxss


/**selectColor.wxss**/
.weui-grids {
  border-top: 1rpx solid #D9D9D9;
  border-left: 1rpx solid #D9D9D9;
}
.weui-grid {
  position: relative;
  float: left;
  padding: 20rpx 20rpx;
  width: 20%;
  box-sizing: border-box;
  border-right: 1rpx solid #D9D9D9;
  border-bottom: 1rpx solid #D9D9D9;
}
.weui-grid_active {
  background-color: #ccc;
}
.weui-grid__icon {
  display: block;
  width: 100rpx;
  height: 100rpx;
  margin: 0 auto;
 box-shadow: 3px 3px 5px #bbb;
   
}
.weui-grid__label {
  margin-top: 5px;
  display: block;
  text-align: center;
  color: #000000;
  font-size: 14px;
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
}

6.selectColor.js


/selectColor.js
//获取应用实例
var app = getApp()
Page({
  data: {
    color: [
      { key: 1, color: ' 白色 ', number: '#FFFFFF' },

      { key: 2, color: ' 红色 ', number: '#FF0000' },

      { key: 3, color: ' 绿色 ', number: '#00FF00' },

      { key: 4, color: ' 蓝色 ', number: '#0000FF' },

      { key: 5, color: ' 牡丹红 ', number: '#FF00FF' },

      { key: 6, color: ' 青色 ', number: '#00FFFF' },

      { key: 7, color: ' 黄色 ', number: '#FFFF00' },

      { key: 8, color: ' 黑色 ', number: '#000000' },

      { key: 9, color: ' 海蓝 ', number: '#70DB93' },

      { key: 10, color: ' 巧克力色 ', number: '#5C3317' },

      { key: 11, color: ' 蓝紫色 ', number: '#9F5F9F' },

      { key: 12, color: ' 黄铜色 ', number: '#B5A642' },

      { key: 13, color: ' 亮金色 ', number: '#D9D919' },

      { key: 14, color: ' 棕色 ', number: '#A67D3D' },

      { key: 15, color: ' 青铜色 ', number: '#8C7853' },

      { key: 16, color: ' 2号青铜色 ', number: '#A67D3D' },

      { key: 17, color: ' 士官服蓝色 ', number: '#5F9F9F' },

      { key: 18, color: ' 冷铜色 ', number: '#D98719' },

      { key: 19, color: ' 铜色 ', number: '#B87333' },

      { key: 20, color: ' 珊瑚红 ', number: '#FF7F00' },

      { key: 21, color: ' 紫蓝色 ', number: '#42426F' },

      { key: 22, color: ' 深棕 ', number: '#5C4033' },

      { key: 23, color: ' 深绿 ', number: '#2F4F2F' },

      { key: 24, color: ' 深铜绿色 ', number: '#4A766E' },

      { key: 25, color: ' 深橄榄绿 ', number: '#4F4F2F' },

      { key: 26, color: ' 深兰花色 ', number: '#9932CD' },

      { key: 27, color: ' 深紫色 ', number: '#871F78' },

      { key: 28, color: ' 深石板蓝 ', number: '#6B238E' },

      { key: 29, color: ' 深铅灰色 ', number: '#2F4F4F' },

      { key: 30, color: ' 深棕褐色 ', number: '#97694F' },

      { key: 32, color: ' 深绿松石色 ', number: '#7093DB' },

      { key: 33, color: ' 暗木色 ', number: '#855E42' },

      { key: 34, color: ' 淡灰色 ', number: '#545454' },

      { key: 35, color: ' 土灰玫瑰红色 ', number: '#856363' },

      { key: 36, color: ' 长石色 ', number: '#D19275' },

      { key: 37, color: ' 火砖色 ', number: '#8E2323' },

      { key: 38, color: ' 森林绿 ', number: '#238E23' },

      { key: 39, color: ' 金色 ', number: '#CD7F32' },

      { key: 40, color: ' 鲜黄色 ', number: '#DBDB70' },

      { key: 41, color: ' 灰色 ', number: '#C0C0C0' },

      { key: 42, color: ' 铜绿色 ', number: '#527F76' },

      { key: 43, color: ' 青黄色 ', number: '#93DB70' },

      { key: 44, color: ' 猎人绿 ', number: '#215E21' },

      { key: 45, color: ' 印度红 ', number: '#4E2F2F' },

      { key: 46, color: ' 土黄色 ', number: '#9F9F5F' },

      { key: 47, color: ' 浅蓝色 ', number: '#C0D9D9' },

      { key: 48, color: ' 浅灰色 ', number: '#A8A8A8' },

      { key: 49, color: ' 浅钢蓝色 ', number: '#8F8FBD' },

      { key: 59, color: ' 浅木色 ', number: '#E9C2A6' },

      { key: 60, color: ' 石灰绿色 ', number: '#32CD32' },

      { key: 61, color: ' 桔黄色 ', number: '#E47833' },

      { key: 62, color: ' 褐红色 ', number: '#8E236B' },

      { key: 63, color: ' 中海蓝色 ', number: '#32CD99' },

      { key: 64, color: ' 中蓝色 ', number: '#3232CD' },

      { key: 65, color: ' 中森林绿 ', number: '#6B8E23' },

      { key: 66, color: ' 中鲜黄色 ', number: '#EAEAAE' },

      { key: 67, color: ' 中兰花色 ', number: '#9370DB' },

      { key: 68, color: ' 中海绿色 ', number: '#426F42' },

      { key: 69, color: ' 中石板蓝色 ', number: '#7F00FF' },

      { key: 70, color: ' 中春绿色 ', number: '#7FFF00' },

      { key: 71, color: ' 中绿松石色 ', number: '#70DBDB' },

      { key: 72, color: ' 中紫红色 ', number: '#DB7093' },

      { key: 73, color: ' 中木色 ', number: '#A68064' },

      { key: 74, color: ' 深藏青色 ', number: '#2F2F4F' },

      { key: 75, color: ' 海军蓝 ', number: '#23238E' },

      { key: 76, color: ' 霓虹篮 ', number: '#4D4DFF' },

      { key: 77, color: ' 霓虹粉红 ', number: '#FF6EC7' },

      { key: 78, color: ' 新深藏青色 ', number: '#00009C' },

      { key: 79, color: ' 新棕褐色 ', number: '#EBC79E' },

      { key: 80, color: ' 暗金黄色 ', number: '#CFB53B' },

      { key: 81, color: ' 橙色 ', number: '#FF7F00' },
    ],
  },

  onLoad: function () {

  },
  //点击后关闭选色页面
  selectColor: function (e) {
    var number = e.currentTarget.dataset.number;
    console.log("number: " + number)
    try {
      wx.setStorageSync('numberColor', number)
    } catch (e) {
    }
    wx.navigateBack({
      delta: 1, // 回退前 delta(默认为1) 页面
      success: function (res) {
        // success
      },
      fail: function () {
        // fail
      },
      complete: function () {
        // complete
      }
    })
  }
})




demo代码下载


我的博客: http://blog.csdn.net/qq_31383345
欢迎批评!

作者:qq_31383345 发表于2016/11/30 1:07:12 原文链接
阅读:9 评论:0 查看评论

Android开发中跑马灯效果的实现

$
0
0
  • 跑马灯效果这个功能,是因为现在做的这个项目的老大想要加的一个功能点。所以我找了些资料和一些自己的改进及理解,这里分享给大家。
  • 跑马灯在编程中,通常指有时需要用一矩形条显示少量用户特别关心的信息,这条信息串首尾相连,向一个方向循环滚动。
    效果图就不给大家展示了,这里自行百度一下就能找到很多相关的图片。


  • 找到的实现方法很多,有的是直接在textView中添加一些必要的属性,但是这个方法弊端很多,比如你的文字长度需要比textView控件的长度宽,不然是跑不起来的。下面说的是自定义一个View,xml布局中引用这个view的方法,这样的话对文字长度就没有限制了。好了,废话不多说。

首先,自定义一个MyTextView类继承TextView类,重写相关构造方法,类中的代码如下所示:


import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.Display;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.TextView;

public class MyTextView extends TextView implements OnClickListener {

    private float textLength = 0f;// 文本长度
    private float viewWidth = 0f;
    private float step = 0f;// 文字的横坐标
    private float y = 0f;// 文字的纵坐标
    private float temp_view_plus_text_length = 0.0f;// 用于计算的临时变量
    private float temp_view_plus_two_text_length = 0.0f;// 用于计算的临时变量
    public boolean isStarting = false;// 是否开始滚动
    private Paint paint = null;// 绘图样式
    private String text = "";// 文本内容

    public MyTextView(Context context) {
        super(context);
        initView();
    }

    public MyTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public MyTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initView();
    }

    private void initView() {
        // TODO Auto-generated method stub
        setOnClickListener(this);
    }

    @Override
    public boolean isFocused() {
        return true;
    }

    public void init(WindowManager windowManager) {
        paint = getPaint();
        // 邹奇   2016/11/30  这里可以自己设置文字显示的颜色,这里我设置为了蓝色,下载我的apk自己体验
        // 默认为黑色
        if(color != 0){
            paint.setColor(color);
        }
        text = getText().toString();
        textLength = paint.measureText(text);
        viewWidth = getWidth();
        if (viewWidth == 0) {
            if (windowManager != null) {
                Display display = windowManager.getDefaultDisplay();
                viewWidth = display.getWidth();
            }
        }
        step = textLength;
        temp_view_plus_text_length = viewWidth + textLength;
        temp_view_plus_two_text_length = viewWidth + textLength * 2;
        y = getTextSize() + getPaddingTop();
    }

    @Override
    public Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        SavedState ss = new SavedState(superState);

        ss.step = step;
        ss.isStarting = isStarting;

        return ss;

    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        if (!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }
        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());

        step = ss.step;
        isStarting = ss.isStarting;

    }

    public static class SavedState extends BaseSavedState {
        public boolean isStarting = false;
        public float step = 0.0f;

        SavedState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeBooleanArray(new boolean[] { isStarting });
            out.writeFloat(step);
        }

        public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {

            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }

            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }
        };

        private SavedState(Parcel in) {
            super(in);
            boolean[] b = null;
            in.readBooleanArray(b);
            if (b != null && b.length > 0)
                isStarting = b[0];
            step = in.readFloat();
        }
    }

    public void startScroll() {
        isStarting = true;
        invalidate();
    }

    public void stopScroll() {
        isStarting = false;
        invalidate();
    }

    @Override
    public void onDraw(Canvas canvas) {
        canvas.drawText(text, temp_view_plus_text_length - step, y, paint);
        if (!isStarting) {
            return;
        }
        if(speed != 0){
            step += speed;// speed为用户自己设定的文字滚动速度
        }else {
            step += 0.5;// 用户没有设置速度,则默认0.5为文字滚动速度。
        }
        if (step > temp_view_plus_two_text_length)
            step = textLength;
        invalidate();

    }

    private double speed = 0;// 邹奇  2016/11/30  声明变量表示文字滚动的速度
    /**
     * 邹奇   2016/11/30  用户自己设定文字的滚动速度
     * @param speed 速度(一般设置值为2.0即可,快慢自己可以设置新值调节)
     */
    public void setSpeed(double speed){
        this.speed = speed;
    }

    private int color = 0;// 邹奇 2016/11/30  声明变量表示文字显示的颜色
    /**
     * 邹奇   2016/11/30  用户自己设定文字显示的颜色
     * @param color 颜色
     */
    public void setColors(int color){
        this.color = color;
    }

    @Override
    public void onClick(View v) {
        if (isStarting)
            stopScroll();
        else
            startScroll();

    }

}

代码不过多讲了,讲一下使用方法。有好用的大家记得要学会拿来使用,好点的自己可以试着改进。
我在原来的类里面加了两个方法,一个是可以设置文字滚动的速度方法:

/**
     * 邹奇   2016/11/30  用户自己设定文字的滚动速度
     * @param speed 速度(一般设置值为2.0即可,快慢自己可以设置新值调节)
     */
    public void setSpeed(double speed){
        this.speed = speed;
    }

一个是可以设置文字显示颜色的方法:

/**
     * 邹奇   2016/11/30  用户自己设定文字显示的颜色
     * @param color 颜色
     */
    private int color = 0;// 邹奇 2016/11/30  声明变量表示文字显示的颜色
    public void setColors(int color){
        this.color = color;
    }

自定义view有了后,就在你的xml布局中引用这个自定义的View即可,代码如下:

<LinearLayout 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:focusableInTouchMode="false"
    android:orientation="vertical"
    tools:context="com.example.buttontest.MainActivity" >

    <com.example.buttontest.MyTextView
        android:id="@+id/tv_pmd"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="跑马灯效果显示" />

</LinearLayout>

下面给出apk下载链接,毕竟只有自己体验后才知道是否是自己需要的东西。点我下载跑马灯apk


MainActivity里的代码如下:

package com.example.buttontest;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyTextView textView = (MyTextView) findViewById(R.id.tv_pmd);
        textView.setTextSize(30);// 可以设置字体的大小
        textView.setColors(getResources().getColor(R.color.login_btn));// 设置文字的颜色
        textView.init(getWindowManager());// 初始化必要参数
        textView.setSpeed(2.0);// 设置滚动速度
        textView.startScroll();// 开始滚动
    }

}

代码较简单,必要的方法如下:

textView.init(getWindowManager());// 初始化必要参数
textView.startScroll();// 开始滚动

当然你还可以初始化必要的参数前设置字体的大小、字体的颜色,如下:

textView.setTextSize(30);// 可以设置字体的大小
textView.setColors(getResources().getColor(R.color.login_btn));// 设置文字的颜色

注意了,上面字体大小和字体的颜色设置方法需要放到init方法前,这样你的设置才会生效。



到这里,跑马灯的功能就实现了。大家还是要多学会使用已有的资源,然后自己好好的利用起来,优化它,让它能更加的好用,最后把它分享给大家,这是一个很棒的行为。因为不仅提高了自己,还可以给大家提供一个参考的问题解决方案。真的,分享知识后的那种快乐,很奇怪,无法形容,大家自己慢慢的去感受。嘻嘻!


每天进步一点点!加油!

作者:csdnzouqi 发表于2016/11/30 20:41:23 原文链接
阅读:53 评论:0 查看评论

【Android图像处理】水波纹滤镜的实现

$
0
0

假如在生活中看到很清澈的水面上的水波纹,是不是会觉得很美呢?

可是你想过用算法来模拟这样的画面吗?

实现水波纹的基原理如下:

说到波,学过物理的都知道,可以用sin或cos来模拟波,水波纹亦是如此。不同的是,在现实中随着传播的距离的增加,水波的能量衰减的很快,而不是我们所学的一直以不变的振幅传播下去。


方法如下:

  • 以图片的中心为水波的中心,取宽高的较大值的一半做水波纹的直径
  • 波能的扩散/衰减
  • 渲染效果
代码如下:

	//水波纹
	public static Bitmap WaterWave(Bitmap bitmap){
		int w = bitmap.getWidth();
		int h = bitmap.getHeight();

		int[] buf1 = new int[w * h];
		int[] buf2 = new int[w * h];
		int[] source = new int[w * h];

		Bitmap result = Bitmap.createBitmap(w, h, Bitmap.Config.RGB_565);

		bitmap.getPixels(source, 0, w, 0, 0, w, h);

		int[] temp = new int[source.length];

		int x = w / 2; 
		int y = h / 2;
		int stonesize = Math.max(w, h) / 4;
		int stoneweight = Math.max(w, h);


		if ((x + stonesize) > w || (y + stonesize) > h || (x - stonesize) < 0 || (y - stonesize) < 0){
			return null;
		}
		for (int posx = x - stonesize; posx < x + stonesize; posx++){
			for (int posy = y - stonesize; posy < y + stonesize; posy++){
				if ((posx - x) * (posx - x) + (posy - y) * (posy - y) <= stonesize * stonesize){
					buf1[w * posy + posx] = (int)-stoneweight;
				}
			}
		}
		for(int i = 0 ; i < 170; i++){			
			for (int j = w; j < w * h - w; j++){
				//波能扩散
				buf2[j] =(int)(((buf1[j-1]+buf1[j+1]+buf1[j-w]+buf1[j+w])>>1)- buf2[j]);
				//波能衰减
				buf2[j] -= buf2[j]>>5;
			}
			//交换波能数据缓冲区
			int[] tmp =buf1;
			buf1 = buf2;
			buf2 = tmp;

			/* 渲染水纹效果 */
			int xoff, yoff;
			int k = w;
			for (int m = 1; m < h - 1; m++) {
				for (int j = 0; j < w; j++) {
					//计算偏移量
					xoff = buf1[k-1]-buf1[k+1];
					yoff = buf1[k-w]-buf1[k+w];
					//判断坐标是否在窗口范围内
					if ((m+yoff )< 0 || (m+yoff )>= h || (j+xoff )< 0 || (j+xoff )>= w) {
						k++; 
						continue;
					}
					//计算出偏移象素和原始象素的内存地址偏移量		       				       
					int pos1, pos2;
					pos1=w * (m + yoff) + (j + xoff);
					pos2=w * m + j;    			
					temp[pos2++]=source[pos1++];
					k++;
				}
			}
		}
		result.setPixels(temp, 0, w, 0, 0, w, h);

		return result;
	}

效果如下:


原图如下:


效果还是有待改善。

这里是效果比较好的博文链接:链接

作者:qq_32353771 发表于2016/11/30 21:05:43 原文链接
阅读:42 评论:0 查看评论

Android学习_使用Adapter创建ListView

$
0
0

Adapter本身只是一个接口,它派生了ListAdapter和SpinnerAdapter两个子接口。

Adapter常用的实现类如下:
–ArrayAdapter:简单、易用的Adapter,通常用于将数组或List集合的多个值包装成多个列表项。
–SimpleAdapter:不简单、功能强大的Adapter,可用于将List集合的多个对象包装成多个列表项。
–SimpleCursorAdapter:与SimpleAdapter基本相似,只是用于包装Cursor提供的数据。
–BaseAdapter:通常用于被扩展。扩展BaseAdapter可以对各列表项进行最大限度的定制。

通过实例介绍

使用ArrayAdapter创建ListView

//此布局文件定义了两个ListView,但这两个ListView都没有指定android:entries属性,这意味着都需要通过Adapter来提供列表项
 <!--设置使用红色的分隔条-->
    <ListView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/list1"
        android:divider="#f00"
        android:dividerHeight="2px"
        android:headerDividersEnabled="false"/>
    <!--设置使用绿色的分隔条-->
    <ListView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/list2"
        android:divider="#0f0"
        android:dividerHeight="2px"
        android:headerDividersEnabled="false"/>

Activity提供Adapter,Adapter决定ListView所显示的列表项

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ListView list1 = (ListView) findViewById(R.id.list1);
        //定义一个数组
        String[] arr1 = {"孙悟空", "牛魔王", "至尊宝"};
        //将数组包装为ArrayAdapter
        ArrayAdapter<String> adapter1 = new ArrayAdapter<String>(this, R.layout.array_item, arr1);
        //为ListView设置Adapter
        list1.setAdapter(adapter1);

        ListView list2 = (ListView) findViewById(R.id.list2);
        //定义一个数组
        String[] arr2 = {"eclipse", "android studio", "vs2013"};
        //将数组包装为ArrayAdapter
        ArrayAdapter<String> adapter2 = new ArrayAdapter<String>(this, R.layout.checked_item, arr2);
        //为ListView设置Adapter
        list2.setAdapter(adapter2);
       }
}

创建ArrayAdapter时必须指定以下三个参数:
–Context:代表访问整个Android应用的接口。几乎创建所有组件都需要传入Context对象。如上面代码使用this。
–textViewResourceld:资源ID,该资源代表一个TextView,该TextView组件将作为ArrayAdapter的列表项组件。
–数组或list:该数组或List兼顾则为多个列表项提供数据。

从上面不难看出,创建ArrayAdapter时传入的第二个参数控制每个列表项的组件,第三个参数则负责为列表提供数据。该数组或List包含多少个元素,就将生成多少个列表项,每个列表项都是TextView组件,TextView组件显示的文本由数组或List的元素提供。

每个列表项的组件外观由下面布局文件控制:

<?xml version="1.0" encoding="utf-8"?>

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/TextView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="24dp"
    android:padding="10px"
    android:textColor="#f0f"
    android:shadowDx="4"
    android:shadowDy="4"
    android:shadowRadius="2">

</TextView>

基于ListActivity实现列表

如果程序的窗口仅仅需要显示一个列表,则可以直接让Activity继承ListActivity来实现,ListActivity的子类无须调用setContentView()方法来显示某个界面,而是可以直接传入一个内容Adapter,ListActivity的子类就呈现出一个列表。

public class TwoActivity extends ListActivity {

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //无需布局文件
        String[] arr = {"丁一", "王二", "张三", "李四"};
        //创建ArrayAdapter对象
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
                android.R.layout.simple_list_item_multiple_choice, arr);
        //设置该窗口显示列表
        setListAdapter(adapter);

        //用setchoicemode方法将选择方式设置为CHOICE_MODE_MULTIPLE,即可实现多选的选中效果。
        final ListView listView = getListView();
        listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
    }
}

使用SimpleAdapter创建ListView

ArrayAdapter虽然简单、易用,但ArrayAdapter的功能比较有限,它的每个列表项只能是TextView。如果需要实现更复杂的列表项,则可以使用SimpleAdapter。

界面布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <!--定义一个ListView-->
    <ListView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/list3"/>
</LinearLayout>

每个列表项的界面布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <!--定义一个ImageView,用于作为列表项的一部分-->
    <ImageView
        android:id="@+id/header"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:paddingLeft="10dp"
        android:scaleType="fitCenter"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <!--定义一个TextView,用于作为列表项的一部分-->
        <TextView
            android:id="@+id/name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="40dp"
            android:textColor="#742974"
            android:paddingLeft="10dp"/>
        <!--定义一个TextView,用于作为列表项的一部分-->
        <TextView
            android:id="@+id/desc"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="20dp"
            android:paddingLeft="10dp"
            android:singleLine="true"
            android:ellipsize="end"/>
    </LinearLayout>

</LinearLayout>

Activity代码:

public class ThreeActivity extends AppCompatActivity{

    private String[] names = new String[]{
            "哈利", "皮特", "杰瑞", "汤姆", "川普"};
    private String[] descs = new String[]{
            "头上带闪电的天选之人", "被蜘蛛咬的天选之人",
            "一只脱离了低级趣味的老鼠", "一只奋斗的猫",
            "大美兴,川普王,川普来了不纳粮"
    };
    private int[] imageIds = new int[]{
            R.drawable.hali, R.drawable.zhizhuxia,
            R.drawable.jierui, R.drawable.tangmu,
            R.drawable.chuanpu
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_three);

        //创建一个List集合,List集合的元素是Map
        List<Map<String, Object>> listItems = new ArrayList<Map<String, Object>>();
        for (int i = 0; i < names.length; i++){

            Map<String, Object> listItem = new HashMap<String, Object>();
            listItem.put("header", imageIds[i]);
            listItem.put("personName", names[i]);
            listItem.put("desc", descs[i]);
            listItems.add(listItem);
        }
        //创建一个SimpleAdapter
        SimpleAdapter simpleAdapter = new SimpleAdapter(this,
                listItems, R.layout.simple_item,
                new String[]{"personName", "header", "desc"},
                new int[]{R.id.name, R.id.header, R.id.desc});
        ListView list = (ListView) findViewById(R.id.list3);
        //为ListView设置Adapter
        list.setAdapter(simpleAdapter);

        //为ListView的列表项的单击事件绑定时间监听器
        list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
                Toast.makeText(ThreeActivity.this, names[i] + "被单击了",Toast.LENGTH_SHORT).show();
                System.out.println(names[i] + "被单机了");
            }
        });
    }
}

上面代码中还添加了OnItemClickListener单击事件,需要注意的是,OnItemSelectedListener事件可能会无响应,这是因为OnItemSelectedListener是用来监听【选中某一项】的事件,不是【点击】某一项的事件,虽然点击能引发选中事件,但不是必然的, 一般用按键或者鼠标滚轮移动焦点都会产生这个选中事件。


扩展BaseAdapter实现不存储列表项的ListView

扩展BaseAdapter可以取得对Adapter最大的控制权:程序要创建多少个列表项,每个列表项的组件都有开发者决定。

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/list4"/>

</LinearLayout>

Activity:

public class FourActivity extends AppCompatActivity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_four);

        //获取ListView组件
        ListView list4 = (ListView) findViewById(R.id.list4);
        BaseAdapter adapter = new BaseAdapter() {
            @Override
            public int getCount() {
                //指定一共包含40个选项
                return 40;
            }

            @Override
            public Object getItem(int i) {
                return null;
            }
            //重写该方法,该方法的返回值将作为列表项的ID
            @Override
            public long getItemId(int i) {
                return i;
            }
            //重写该方法,该方法返回的View将作为列表框
            @Override
            public View getView(int i, View view, ViewGroup viewGroup) {
                //创建一个LinearLayout,并向其中添加两个组件
                LinearLayout linearLayout = new LinearLayout(FourActivity.this);
                //设置排列方式
                linearLayout.setOrientation(LinearLayout.HORIZONTAL);
                //图片
                ImageView imageView = new ImageView(FourActivity.this);
                imageView.setImageResource(R.drawable.chuanpu);
                //文本
                TextView textView = new TextView(FourActivity.this);
                textView.setText("第" + (i + 1) + "个列表项");
                textView.setTextSize(20);
                textView.setTextColor(Color.RED);
                linearLayout.addView(imageView);
                linearLayout.addView(textView);
                //返回LinearLayout实例
                return linearLayout;
            }
        };
        list4.setAdapter(adapter);
    }
}

扩展对象需要重写如下4个方法:
–getCount():该方法的返回值控制该Adapter将会包含多少个列表项。
–getItem(int position):该方法的返回值决定第position处的列表项的内容。
–getItemId(int position):该方法的返回值决定第position处的列表项的ID。
–getView(int position, View convertView, ViewGroup parent):该方法的返回值决定第position处的列表项组件。

上述四个方法中最重要的是第一个和第四个。


以上实现Adapter的方法完全适用于AdapterView的其他子类:GridView、、Spinner、Gallery、AdapterViewFlipper等。

总的来说创建步骤就是两步:
1.采用四种方式之一创建Adapter。
2.调用AdapterView的setAdapter(Adapter)方法设置Adapter即可。

作者:qq_35073878 发表于2016/11/30 21:26:16 原文链接
阅读:33 评论:0 查看评论

【iOS】iOS数据存储,应用沙盒,XML,Preference,NSKeyedArchiver归档,SQLite3

$
0
0

版权声明:本文为博主原创,如需转载请注明出处。

应用沙盒

每个iOS应用都有自己的应用沙盒(应用沙盒就是文件系统目录),与其他文件系统隔离。应用必须待在自己的沙盒里,其他应用不能访问该沙盒

应用沙盒的文件系统目录,如下图所示(假设应用的名称叫Layer)

模拟器应用沙盒的根路径在: (apple是用户名, 6.0是模拟器版本)
/Users/apple/Library/Application Support/iPhone Simulator/6.0/Applications

在使用Xcode5的时候,模拟器中的app可以在电脑如下路径找到:

/Users/用户名/Library/Application Support/iPhone Simulator/系统版本号/Applications

而在Xcode6环境下,存放位置已经发生了变化。

调查发现,新的路径变成了/Users/{YOUR NAME}/Library/Developer/CoreSimulator/Devices/设备型号/data/Containers

其中设备型号是用uuid表示的,可以用如下命令获取它们之间的对应关系(stackoverflow):xcrun simctl list
再往下,app和本地文件已经分开放了:app在Bundle/Application下;本地文件在Data/Application下
每个目录下都是一堆uuid命名的文件夹,很难区分。

找的时候可以通过在app运行时打印bundle路径和NSHomeDirectory()发现具体路径。

➜  Devices xcrun simctl list
== Device Types ==
iPhone 4s (com.apple.CoreSimulator.SimDeviceType.iPhone-4s)
iPhone 5 (com.apple.CoreSimulator.SimDeviceType.iPhone-5)
iPhone 5s (com.apple.CoreSimulator.SimDeviceType.iPhone-5s)
iPhone 6 (com.apple.CoreSimulator.SimDeviceType.iPhone-6)
iPhone 6 Plus (com.apple.CoreSimulator.SimDeviceType.iPhone-6-Plus)
iPhone 6s (com.apple.CoreSimulator.SimDeviceType.iPhone-6s)
iPhone 6s Plus (com.apple.CoreSimulator.SimDeviceType.iPhone-6s-Plus)
iPad 2 (com.apple.CoreSimulator.SimDeviceType.iPad-2)
iPad Retina (com.apple.CoreSimulator.SimDeviceType.iPad-Retina)
iPad Air (com.apple.CoreSimulator.SimDeviceType.iPad-Air)
iPad Air 2 (com.apple.CoreSimulator.SimDeviceType.iPad-Air-2)
iPad Pro (com.apple.CoreSimulator.SimDeviceType.iPad-Pro)
Apple TV 1080p (com.apple.CoreSimulator.SimDeviceType.Apple-TV-1080p)
Apple Watch - 38mm (com.apple.CoreSimulator.SimDeviceType.Apple-Watch-38mm)
Apple Watch - 42mm (com.apple.CoreSimulator.SimDeviceType.Apple-Watch-42mm)
== Runtimes ==
iOS 9.0 (9.0 - 13A344) (com.apple.CoreSimulator.SimRuntime.iOS-9-0)
iOS 9.1 (9.1 - 13B143) (com.apple.CoreSimulator.SimRuntime.iOS-9-1)
iOS 9.2 (9.2 - 13C75) (com.apple.CoreSimulator.SimRuntime.iOS-9-2)
iOS 9.3 (9.3 - 13E230) (com.apple.CoreSimulator.SimRuntime.iOS-9-3)
tvOS 9.2 (9.2 - 13Y227) (com.apple.CoreSimulator.SimRuntime.tvOS-9-2)
watchOS 2.2 (2.2 - 13V143) (com.apple.CoreSimulator.SimRuntime.watchOS-2-2)
== Devices ==
-- iOS 9.0 --
    iPhone 4s (41931634-624C-4AB8-8652-9EFA6E816379) (Shutdown)
    iPhone 5 (3A03D3D9-35BC-4AFC-8D3A-AA729DFA4417) (Shutdown)
    iPhone 5s (9091604E-CB39-4089-B594-336212024A21) (Shutdown)
    iPhone 6 (CB81F1ED-3B3F-407A-BF58-E5E932D253E2) (Shutdown)
    iPhone 6 Plus (F166EB11-39BB-4EFA-87E3-0ADD79849409) (Shutdown)
    iPhone 6s (F6487BC7-400C-4A91-A950-2A18B9558DC3) (Shutdown)
    iPhone 6s Plus (1E17B5B9-9765-4306-A646-3253620178AE) (Shutdown)
    iPad 2 (5C4751E6-1CA2-418C-891F-3C450C91DCE7) (Shutdown)
    iPad Retina (14F984F6-3E7E-4BB3-A6F8-3EF337058DCF) (Shutdown)
    iPad Air (C492DDAF-7CE3-45D3-9734-9EB221DF8EC3) (Shutdown)
    iPad Air 2 (4136305D-8F47-4CD4-BED3-B274162E4223) (Shutdown)
-- iOS 9.1 --
    iPhone 4s (20F804F4-3551-4975-8FAD-99B387FA7101) (Shutdown)
    iPhone 5 (DF220FF8-4AD7-486D-9701-3DC532BF3190) (Shutdown)
    iPhone 5s (89EC0AF7-2799-47C5-8CA6-26D978153FAE) (Shutdown)
    iPhone 6 (873A48E8-DF55-46AA-9895-D723BFD65262) (Shutdown)
    iPhone 6 Plus (2AC4B279-5B83-4EB3-BB6F-511FFBE8602E) (Shutdown)
    iPhone 6s (86F1DE4C-B407-457C-B170-82FEE1FBA1DD) (Shutdown)
    iPhone 6s Plus (AFA83ACE-0FE5-4814-81EB-189100A2D8B3) (Shutdown)
    iPad 2 (33CE0EA4-F9AF-43FA-A5AE-8784ABB6DBB8) (Shutdown)
    iPad Retina (EFA3EEE6-DBBB-424C-A1EB-988FE40C6E01) (Shutdown)
    iPad Air (05398825-A9A4-41F2-8E91-DD1651E27C4C) (Shutdown)
    iPad Air 2 (2F805563-E2E5-4708-A846-CBFB8F23BB98) (Shutdown)
    iPad Pro (D2A724F2-BDEB-4D2E-AF83-3CD1A2DF929A) (Shutdown)
-- iOS 9.2 --
    iPhone 4s (FFB2BDB2-B759-4F40-A396-A38A43D946E5) (Shutdown)
    iPhone 5 (A60C4213-F011-4DBA-8E94-CCD8214E7B04) (Shutdown)
    iPhone 5s (6A6F57A5-1057-4187-B034-AB085349DF6B) (Shutdown)
    iPhone 6 (F9B18CA1-0D31-4077-9F4B-B94C195E0E67) (Shutdown)
    iPhone 6 Plus (B9D38A37-C004-485B-83A9-C442AA92D644) (Shutdown)
    iPhone 6s (9F8EB146-B2A2-4F1F-84CC-32D33E0A7187) (Shutdown)
    iPhone 6s Plus (E09E8DE5-F60C-4F30-9144-B45A7DF9ED98) (Shutdown)
    iPad 2 (92EE4298-E5FD-4408-93EF-554AF18EE12D) (Shutdown)
    iPad Retina (B1DD1D12-F67A-4EC4-9833-1B6989BB8A45) (Shutdown)
    iPad Air (3F1980FD-3C5B-43CF-834C-B4869B9A89C8) (Shutdown)
    iPad Air 2 (42802305-9282-4A19-9B3B-5EBA3163B6EF) (Shutdown)
    iPad Pro (02A937DA-694D-40DE-B1EE-604D8F46AD8F) (Shutdown)
-- iOS 9.3 --
    iPhone 4s (BDE69A93-B858-4B5A-AB3F-CAD796FCC045) (Shutdown)
    iPhone 5 (A95F5000-7E2A-455E-B847-9212722A4136) (Shutdown)
    iPhone 5s (E5D555C0-C723-4338-BFCF-B5E2E669FE54) (Shutdown)
    iPhone 6 (9226B68B-5702-4953-947B-0E655FADDE12) (Shutdown)
    iPhone 6 Plus (C4E60731-2679-4237-8E24-AC9512DF9E9E) (Shutdown)
    iPhone 6s (6F7F28DE-CE2A-40DE-B91B-203EA774B275) (Booted)
    iPhone 6s Plus (115B2F18-065F-490B-9EAB-9C63B0BDE1CF) (Shutdown)
    iPad 2 (EF2F1963-B398-45E8-A609-170F0F09E6AA) (Shutdown)
    iPad Retina (0F830533-9A66-46EE-B0F8-388E12E41FCF) (Shutdown)
    iPad Air (04ED9886-705E-4E63-9322-6EF1E04D2578) (Shutdown)
    iPad Air 2 (816C1B6D-6F7F-4A41-A952-A84BFECE6230) (Shutdown)
    iPad Pro (ECD0C28F-28CE-4CAB-A7C3-F31F7D4A3146) (Shutdown)
-- tvOS 9.2 --
    Apple TV 1080p (8129655F-4134-4998-A2C9-0461C28FDF00) (Shutdown)
-- watchOS 2.2 --
    Apple Watch - 42mm (2B69FD3C-1DBC-41AC-82BA-B8A4215E09C4) (Shutdown)
-- Unavailable: com.apple.CoreSimulator.SimRuntime.iOS-10-0 --
    iPhone 5 (DF66161D-F762-44FB-BF8B-EE8FDECFD203) (Shutdown) (unavailable, runtime profile not found)
    iPhone 5s (00F328A8-5704-4C88-A128-5D310C51B476) (Shutdown) (unavailable, runtime profile not found)
    iPhone 6 (A83359F2-41E9-4FDB-971A-1022811451BE) (Shutdown) (unavailable, runtime profile not found)
    iPhone 6 Plus (A824AD89-D1CE-4E04-AD73-0E0C333238C2) (Shutdown) (unavailable, runtime profile not found)
    iPhone 6s (14B59EAE-E267-4CF9-90CF-65F1153E8B51) (Shutdown) (unavailable, runtime profile not found)
    iPhone 6s Plus (CC3124B7-500A-470D-B5DB-F330F5B173E1) (Shutdown) (unavailable, runtime profile not found)
    iPad Retina (2550E4C9-54B2-4AC1-BD64-8C21595B2FFA) (Shutdown) (unavailable, runtime profile not found)
    iPad Air (B8CD31CA-1690-4F7E-BABA-087AC4C1E894) (Shutdown) (unavailable, runtime profile not found)
    iPad Air 2 (7F6E412B-CC71-4E92-A542-10AB7CEE0440) (Shutdown) (unavailable, runtime profile not found)
    iPad Pro (12.9 inch) (693C487D-BBDC-4ED6-A0B5-43832111CA89) (Shutdown) (unavailable, runtime profile not found)
    iPad Pro (9.7 inch) (580B7423-99CD-411A-AD11-8C7906C44279) (Shutdown) (unavailable, runtime profile not found)
    iPhone SE (7482B157-12B7-4713-9BF7-F4C7083EC1B5) (Shutdown) (unavailable, runtime profile not found)
-- Unavailable: com.apple.CoreSimulator.SimRuntime.tvOS-10-0 --
    Apple TV 1080p (F9A62967-74C3-49DE-8A35-979531501889) (Shutdown) (unavailable, runtime profile not found)
-- Unavailable: com.apple.CoreSimulator.SimRuntime.watchOS-3-0 --
    Apple Watch - 38mm (CFD06DDD-5ED7-4C2E-AD2D-180959274F4E) (Shutdown) (unavailable, runtime profile not found)
    Apple Watch - 42mm (835B5691-599C-4A5E-919F-04F68991042F) (Shutdown) (unavailable, runtime profile not found)
== Device Pairs ==
B136137E-9B10-4427-9706-59FBD01359E1 (active, disconnected)
    Watch: Apple Watch - 42mm (2B69FD3C-1DBC-41AC-82BA-B8A4215E09C4) (Shutdown)
    Phone: iPhone 6s Plus (115B2F18-065F-490B-9EAB-9C63B0BDE1CF) (Shutdown)
07105DA6-F4F3-4FF4-8945-4A1E0A1B97AE (unavailable)
    Watch: Apple Watch - 38mm (CFD06DDD-5ED7-4C2E-AD2D-180959274F4E) (Shutdown)
    Phone: iPhone 6 (A83359F2-41E9-4FDB-971A-1022811451BE) (Shutdown)
E142260B-6DF1-4B45-8389-E108F0F79652 (unavailable)
    Watch: Apple Watch - 42mm (835B5691-599C-4A5E-919F-04F68991042F) (Shutdown)
    Phone: iPhone 6 Plus (A824AD89-D1CE-4E04-AD73-0E0C333238C2) (Shutdown)

应用沙盒结构分析

  • 应用程序包:(上图中的Layer)包含了所有的资源文件和可执行文件
  • Documents:保存应用运行时生成的需要持久化的数据,iTunes同步设备时会备份该目录。例如,游戏应用可将游戏存档保存在该目录
  • tmp:保存应用运行时所需的临时数据,使用完毕后再将相应的文件从该目录删除。应用没有运行时,系统也可能会清除该目录下的文件。iTunes同步设备时不会备份该目录
  • Library/Caches:保存应用运行时生成的需要持久化的数据,iTunes同步设备时不会备份该目录。一般存储体积大、不需要备份的非重要数据
  • Library/Preference:保存应用的所有偏好设置,iOS的Settings(设置)应用会在该目录中查找应用的设置信息。iTunes同步设备时会备份该目录

应用沙盒目录的常见获取方式

  • 沙盒根目录
NSString *home = NSHomeDirectory();
  • Documents:(2种方式)
  • 利用沙盒根目录拼接”Documents”字符串
NSString *home = NSHomeDirectory();
NSString *documents = [home stringByAppendingPathComponent:@"Documents"];
// 不建议采用,因为新版本的操作系统可能会修改目录名
  • 利用NSSearchPathForDirectoriesInDomains函数
// NSUserDomainMask 代表从用户文件夹下找
// YES 代表展开路径中的波浪字符“~”
NSArray *array =  NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, NO);
// 在iOS中,只有一个目录跟传入的参数匹配,所以这个集合里面只有一个元素
NSString *documents = [array objectAtIndex:0];

tem 目录

NSString *tmp = NSTemporaryDirectory();

Library/Caches:(跟Documents类似的2种方法)

  • 利用沙盒根目录拼接”Caches”字符串
  • 利用NSSearchPathForDirectoriesInDomains函数(将函数的第2个参数改为:NSCachesDirectory即可)

Library/Preference:通过NSUserDefaults类存取该目录下的设置信息

属性列表

  • 属性列表是一种XML格式的文件,拓展名为plist
  • 如果对象是NSString、NSDictionary、NSArray、NSData、NSNumber等类型,就可以使用writeToFile:atomically:方法直接将对象写到属性列表文件中

属性列表-归档NSDictionary

将一个NSDictionary对象归档到一个plist属性列表中

// 将数据封装成字典
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:@"小明" forKey:@"name"];
[dict setObject:@"15013141314" forKey:@"phone"];
[dict setObject:@"27" forKey:@"age"];
// 将字典持久化到Documents/stu.plist文件中
[dict writeToFile:path atomically:YES];

写入结果

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>age</key>
    <string>27</string>
    <key>name</key>
    <string>小明</string>
    <key>phone</key>
    <string>15013141314</string>
</dict>
</plist>

读取属性列表,恢复NSDictionary对象

读取属性列表,恢复NSDictionary对象

// 读取Documents/stu.plist的内容,实例化NSDictionary
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
NSLog(@"name:%@", [dict objectForKey:@"name"]);
NSLog(@"phone:%@", [dict objectForKey:@"phone"]);
NSLog(@"age:%@", [dict objectForKey:@"age"]);

打印结果:

2016-08-24 09:47:42.626 PersonalContacts02[2225:169033] name:小明
2016-08-24 09:47:42.626 PersonalContacts02[2225:169033] phone:15013141314
2016-08-24 09:47:42.626 PersonalContacts02[2225:169033] age:27

属性列表-NSDictionary的存储和读取过程

偏好设置

  • 很多iOS应用都支持偏好设置,比如保存用户名、密码、字体大小等设置,iOS提供了一套标准的解决方案来为应用加入偏好设置功能
  • 每个应用都有个NSUserDefaults实例,通过它来存取偏好设置
  • 比如,保存用户名、字体大小、是否自动登录
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:@"yoferzhang" forKey:@"username"];
[defaults setFloat:18.0f forKey:@"text_size"];
[defaults setBool:YES forKey:@"auto_login"];

读取上次保存的设置

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *username = [defaults stringForKey:@"username"];
float textSize = [defaults floatForKey:@"text_size"];
BOOL autoLogin = [defaults boolForKey:@"auto_login"];

注意:UserDefaults设置数据时,不是立即写入,而是根据时间戳定时地把缓存中的数据写入本地磁盘。所以调用了set方法之后数据有可能还没有写入磁盘应用程序就终止了。出现以上问题,可以通过调用synchornize方法强制写入

[defaults synchornize];

NSKeyedArchiver

如果对象是NSString、NSDictionary、NSArray、NSData、NSNumber等类型,可以直接用NSKeyedArchiver进行归档和恢复

不是所有的对象都可以直接用这种方法进行归档,只有遵守了NSCoding协议的对象才可以
NSCoding协议有2个方法:

  • encodeWithCoder:

每次归档对象时,都会调用这个方法。一般在这个方法里面指定如何归档对象中的每个实例变量,可以使用encodeObject:forKey:方法归档实例变量

  • initWithCoder:

每次从文件中恢复(解码)对象时,都会调用这个方法。一般在这个方法里面指定如何解码文件中的数据为对象的实例变量,可以使用decodeObject:forKey方法解码实例变量

NSKeyedArchiver-归档NSArray

归档一个NSArray对象到Documents/array.archive

NSArray *array = [NSArray arrayWithObjects:@"a",@"b",nil];
[NSKeyedArchiver archiveRootObject:array toFile:path];

恢复(解码)NSArray对象

NSArray *array = [NSKeyedUnarchiver unarchiveObjectWithFile:path];

NSKeyedArchiver-归档Person对象(Person.h)

Person对象(Person.h)

@interface Person : NSObject <NSCoding>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) float height;
@end

Person对象(Person.m)

@implementation Person

- (void)encodeWithCoder:(NSCoder *)encoder {
    [encoder encodeObject:self.name forKey:@"name"];
    [encoder encodeInt:self.age forKey:@"age"];
    [encoder encodeFloat:self.height forKey:@"height"];
}

- (id)initWithCoder:(NSCoder *)decoder {
    self.name = [decoder decodeObjectForKey:@"name"];
    self.age = [decoder decodeIntForKey:@"age"];
    self.height = [decoder decodeFloatForKey:@"height"];
    return self;
}

@end

归档Person对象(编码和解码)

  • 归档(编码)
NSArray *array =  NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documents = [array objectAtIndex:0];
NSString *filePath = [documents stringByAppendingPathComponent:@"data.plist"];
Person *person = [[Person alloc] init];
person.name = @"yoferzhang";
person.age = 27;
person.height = 1.83f;
[NSKeyedArchiver archiveRootObject:person toFile:filePath];

data.plist

  • 恢复(解码)
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:path];

归档对象的注意

如果父类也遵守了NSCoding协议,请注意:

应该在encodeWithCoder:方法中加上一句

[super encodeWithCode:encode];

确保继承的实例变量也能被编码,即也能被归档

应该在initWithCoder:方法中加上一句

self = [super initWithCoder:decoder];

确保继承的实例变量也能被解码,即也能被恢复

NSData

  • 使用archiveRootObject:toFile:方法可以将一个对象直接写入到一个文件中,但有时候可能想将多个对象写入到同一个文件中,那么就要使用NSData来进行归档对象
  • NSData可以为一些数据提供临时存储空间,以便随后写入文件,或者存放从磁盘读取的文件内容。可以使用[NSMutableData data]创建可变数据空间

NSData-归档2个Person对象到同一文件中

  • 归档(编码)
// 新建一块可变数据区
NSMutableData *data = [NSMutableData data];
// 将数据区连接到一个NSKeyedArchiver对象
NSKeyedArchiver *archiver = [[[NSKeyedArchiver alloc] initForWritingWithMutableData:data] autorelease];
// 开始存档对象,存档的数据都会存储到NSMutableData中
[archiver encodeObject:person1 forKey:@"person1"];
[archiver encodeObject:person2 forKey:@"person2"];
// 存档完毕(一定要调用这个方法)
[archiver finishEncoding];
// 将存档的数据写入文件
[data writeToFile:path atomically:YES];
  • 恢复(解码)
// 从文件中读取数据
NSData *data = [NSData dataWithContentsOfFile:path];
// 根据数据,解析成一个NSKeyedUnarchiver对象
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
Person *person1 = [unarchiver decodeObjectForKey:@"person1"];
Person *person2 = [unarchiver decodeObjectForKey:@"person2"];
// 恢复完毕
[unarchiver finishDecoding];

利用归档实现深复制

比如对一个Person对象进行深复制

// 临时存储person1的数据
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:person1];
// 解析data,生成一个新的Person对象
Student *person2 = [NSKeyedUnarchiver unarchiveObjectWithData:data];
// 分别打印内存地址
NSLog(@"person1:0x%x", person1); // person1:0x7177a60
NSLog(@"person2:0x%x", person2); // person2:0x7177cf0

SQLite3

  • SQLite3是一款开源的嵌入式关系型数据库,可移植性好、易使用、内存开销小
  • SQLite3是无类型的,意味着你可以保存任何类型的数据到任意表的任意字段中。比如下列的创表语句是合法的:
   create table t_person(name, age);
   为了保证可读性,建议还是把字段类型加上:
   create table t_person(name text, age integer);
  • SQLite3常用的5种数据类型:text、integer、float、boolean、blob
  • 在iOS中使用SQLite3,首先要添加库文件libsqlite3.dylib和导入主头文件

创建、打开、关闭数据库

  • 创建或打开数据库
// path为:~/Documents/person.db
sqlite3 *db;
int result = sqlite3_open([path UTF8String], &db); 

代码解析:

  • sqlite3_open()将根据文件路径打开数据库,如果不存在,则会创建一个新的数据库。如果result等于常量SQLITE_OK,则表示成功打开数据库
  • sqlite3 *db:一个打开的数据库实例
  • 数据库文件的路径必须以C字符串(而非NSString)传入

关闭数据库:

sqlite3_close(db);

执行不返回数据的SQL语句

执行创表语句

char *errorMsg;  // 用来存储错误信息
char *sql = "create table if not exists t_person(id integer primary key autoincrement, name text, age integer);";
int result = sqlite3_exec(db, sql, NULL, NULL, &errorMsg);

代码解析:

  • sqlite3_exec()可以执行任何SQL语句,比如创表、更新、插入和删除操作。但是一般不用它执行查询语句,因为它不会返回查询到的数据
  • sqlite3_exec()还可以执行的语句:

    1. 开启事务:begin transaction;
    2. 回滚事务:rollback;
    3. 提交事务:commit;

带占位符插入数据

char *sql = "insert into t_person(name, age) values(?, ?);";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
    sqlite3_bind_text(stmt, 1, "母鸡", -1, NULL);
    sqlite3_bind_int(stmt, 2, 27);
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
    NSLog(@"插入数据错误");
}
sqlite3_finalize(stmt);

sqlite3_prepare_v2()返回值等于SQLITE_OK,说明SQL语句已经准备成功,没有语法问题

sqlite3_bind_text():大部分绑定函数都只有3个参数

  1. 第1个参数是sqlite3_stmt *类型
  2. 第2个参数指占位符的位置,第一个占位符的位置是1,不是0
  3. 第3个参数指占位符要绑定的值
  4. 第4个参数指在第3个参数中所传递数据的长度,对于C字符串,可以传递-1代替字符串的长度
  5. 第5个参数是一个可选的函数回调,一般用于在语句执行后完成内存清理工作

sqlite_step():执行SQL语句,返回SQLITE_DONE代表成功执行完毕

sqlite_finalize():销毁sqlite3_stmt *对象

查询数据

char *sql = "select id,name,age from t_person;";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
    while (sqlite3_step(stmt) == SQLITE_ROW) {
        int _id = sqlite3_column_int(stmt, 0);
        char *_name = (char *)sqlite3_column_text(stmt, 1);
        NSString *name = [NSString stringWithUTF8String:_name];
        int _age = sqlite3_column_int(stmt, 2);
        NSLog(@"id=%i, name=%@, age=%i", _id, name, _age);
    }
}
sqlite3_finalize(stmt)

sqlite3_step()返回SQLITE_ROW代表遍历到一条新记录

sqlite3_column_*()用于获取每个字段对应的值,第2个参数是字段的索引,从0开始

作者:zyq522376829 发表于2016/11/30 21:33:21 原文链接
阅读:27 评论:0 查看评论

打造自己的可双击放大、多指缩放、放大等功能的ImageView

$
0
0

不多说上代码

package com.sdp.panda.myviewapp.view;

import android.content.Context;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewTreeObserver;
import android.widget.ImageView;

/**
 * Created by 80926 on 2016/11/23.
 */


public class ZoomImageView extends ImageView implements ViewTreeObserver.OnGlobalLayoutListener,
        ScaleGestureDetector.OnScaleGestureListener, View.OnTouchListener {
    //多指的放大与缩小的成员变量
    private boolean mOnce;
    private float mInitScale;//初始化的最小缩放值
    private float mMidScale;//双击的缩放值
    private float mMaxScale;//放大的最大值
    private Matrix mMatrix;
    private ScaleGestureDetector mScaleGestureDetector;//手势

    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~````
    //自由移动的成员变量
    private int mLastPointerCount;//最后多指的数目
    private float mLastX;
    private float mLastY;
    private int mTouchSlop;
    private boolean isCanDrag;//可以拖拽
    private boolean isMoveTopAndBottom;//是否可以上移下移
    private boolean isMoveLeftAndRight;

    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~``~~~~~~~~~~~~~~~~
    //双击的成员变量
    private GestureDetector mGestureDetector;//双击的类
    private boolean isAutoScale;//避免用户一直双击

    public ZoomImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    public ZoomImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ZoomImageView(Context context) {
        this(context, null);//一个参数的引用两个参数的
    }

    private void init(Context context) {
        mMatrix = new Matrix();
//        setScaleType(ScaleType.MATRIX);
        mScaleGestureDetector = new ScaleGestureDetector(context, this);
        setOnTouchListener(this);
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();//是可以move的值
        mGestureDetector = new GestureDetector(context,new GestureDetector.SimpleOnGestureListener(){
            @Override
            public boolean onDoubleTap(MotionEvent e) {
                if (isAutoScale){
                    return true;
                }
                float x = e.getX();
                float y = e.getY();
                if (getScale()<mMidScale){
//                    mMatrix.postScale(mMidScale/getScale(),mMidScale/getScale(),x,y);
//                    setImageMatrix(mMatrix);
                    postDelayed(new AutoScaleRunnable(mMidScale,x,y),15);
                    isAutoScale = true;
                }else {
//                    mMatrix.postScale(mInitScale/getScale(),mInitScale/getScale(),x,y);
//                    setImageMatrix(mMatrix);
                    postDelayed(new AutoScaleRunnable(mInitScale,x,y),15);
                    isAutoScale = true;
                }
                return true;
            }
        });
    }


    @Override
    public void onGlobalLayout() {
        if (!mOnce) {

            int width = getWidth();//控件的宽和高
            int height = getHeight();
            Drawable drawable = getDrawable();
            if (drawable == null) {
                return;
            }
            float scaleSize = 0;//缩放尺寸
            int dWidth = drawable.getIntrinsicWidth();//图片的宽和高
            int dHeight = drawable.getIntrinsicHeight();
            if (dWidth > width && dHeight < height) {
                scaleSize = width * 1.0f / dWidth;//防止为0,提前转换为float类型
            }
            if (dHeight > height && dWidth < width) {
                scaleSize = height * 1.0f / dHeight;
            }
            if (dWidth > width && dHeight > height || dWidth < width && dHeight < height) {
                scaleSize = Math.min(height * 1.0f / dHeight, width * 1.0f / dWidth);
            }
            mInitScale = scaleSize;
            mMidScale = mInitScale * 2;//双击的初始化的两倍
            mMaxScale = mInitScale * 4;//最大是初始化的4倍
            //将图片移动至屏幕的中心
            int centerX = (width - dWidth) / 2;
            int centerY = (height - dHeight) / 2;
            mMatrix.postTranslate(centerX, centerY);
            mMatrix.postScale(mInitScale, mInitScale, width / 2, height / 2);
            setImageMatrix(mMatrix);//矩阵 长度为9的一元数组
            mOnce = true;
        }
    }

    @Override
    protected void onAttachedToWindow() {//实现window的时候天剑
        super.onAttachedToWindow();
        getViewTreeObserver().addOnGlobalLayoutListener(this);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        getViewTreeObserver().removeOnGlobalLayoutListener(this);
    }

    //获取当前图片的缩放值
    private float getScale() {
        float[] values = new float[9];
        mMatrix.getValues(values);
        return values[Matrix.MSCALE_X];
    }

    @Override
    public boolean onScale(ScaleGestureDetector detector) {//缩放中
        float scaleFactor = detector.getScaleFactor();//获取缩放值
        //缩放区间 initScale~maxScale;
        float scale = getScale();
        if (getDrawable() == null) {
            return true;
        }
        if (scale < mMaxScale && scaleFactor > 1.0f ||
                scale > mInitScale && scaleFactor < 1.0f) {
            if (scale * scaleFactor < mInitScale) {
                scaleFactor = mInitScale / scale;
            }
            if (scale * scaleFactor > mMaxScale) {
                scaleFactor = mMaxScale / scale;
            }
            mMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
            setImageViewInCenterWhenCheck();
            setImageMatrix(mMatrix);
        }
        return true;
    }

    //获取可以得到图片的四个角的矩形
    private RectF getMatrixRectF() {
        Matrix mMatrix = this.mMatrix;
        RectF rectF = new RectF();
        Drawable d = getDrawable();
        if (d != null) {
            rectF.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
            mMatrix.mapRect(rectF);
        }
        return rectF;
    }

    //设置图片在缩放最后中间
    private void setImageViewInCenterWhenCheck() {
        RectF matrixRectF = getMatrixRectF();
        float postX = 0;
        float postY = 0;
        int width = getWidth();
        int height = getHeight();
        if (matrixRectF.width() >= width) {
            if (matrixRectF.left > 0) {
                postX = -matrixRectF.left;
            }
            if (matrixRectF.right < width) {
                postX = width - matrixRectF.right;
            }
        }
        if (matrixRectF.height() >= height) {
            if (matrixRectF.top > 0) {
                postY = -matrixRectF.top;
            }
            if (matrixRectF.bottom < height) {
                postY = height - matrixRectF.bottom;
            }
        }
        //宽度和高度小于控件的宽和高的时候让其居中
        if (matrixRectF.width() < width) {
            postX = width / 2 - matrixRectF.right + matrixRectF.width() / 2;
        }
        if (matrixRectF.height() < height) {
            postY = height / 2 - matrixRectF.bottom + matrixRectF.height() / 2;
        }
        mMatrix.postTranslate(postX, postY);
    }
    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {//开始
        return true;
    }
    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {}

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (mGestureDetector.onTouchEvent(event)){
            return true;
        }
        mScaleGestureDetector.onTouchEvent(event);
        float x = 0;
        float y = 0;
        int pointerCount = event.getPointerCount();
        for (int i = 0; i < pointerCount; i++) {
            x += event.getX(i);
            y += event.getY(i);
        }
        x /= pointerCount;
        y /= pointerCount;
        if (mLastPointerCount != pointerCount) {
            isCanDrag = false;
            mLastX = x;
            mLastY = y;
        }
        mLastPointerCount = pointerCount;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //解决与viewPager的冲突事件:加上0.01防止误差
                if (getMatrixRectF().width()>getWidth()+0.01||getMatrixRectF().height()>getHeight()+0.01){
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (getMatrixRectF().width()>getWidth()+0.01||getMatrixRectF().height()>getHeight()+0.01){
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                float dx = x - mLastX;
                float dy = y - mLastY;//移动量
                if (!isCanDrag) {
                    isCanDrag = isCanTouchMove(dx, dy);
                }
                if (isCanDrag) {
                    RectF matrixRectF = getMatrixRectF();
                    if (getDrawable() != null) {
                        isMoveLeftAndRight = isMoveTopAndBottom = true;
                        if (matrixRectF.width() < getWidth()) {//如果图片的宽和高小于控件的宽和高不允许移动
                            isMoveLeftAndRight = false;
                            dx = 0;
                        }
                        if (matrixRectF.height() < getHeight()) {
                            isMoveTopAndBottom = false;
                            dy = 0;
                        }
                        mMatrix.postTranslate(dx, dy);
                        WhenMoveCheck();
                        setImageMatrix(mMatrix);
                    }
                }
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mLastPointerCount = 0;
                break;
        }
        return true;
    }
    //该方法是用于制约图片移动的时候不会留白
    private void WhenMoveCheck() {
        RectF matrixRectF = getMatrixRectF();
        float dx = 0;
        float dy = 0;
        int width = getWidth();
        int height = getHeight();
        if (matrixRectF.top>0&&isMoveTopAndBottom){
            dy = - matrixRectF.top;
        }
        if (matrixRectF.bottom<height&&isMoveTopAndBottom){
            dy = height-matrixRectF.bottom;
        }
        if (matrixRectF.left>0&&isMoveLeftAndRight){
            dx = -matrixRectF.left;
        }
        if (matrixRectF.right<width&& isMoveLeftAndRight){
            dx = width-matrixRectF.right;
        }
        mMatrix.postTranslate(dx,dy);
        setImageMatrix(mMatrix);
    }

    private boolean isCanTouchMove(float dx, float dy) {
        return Math.sqrt(dx * dx + dy * dy) > mTouchSlop;
    }

    private class AutoScaleRunnable implements Runnable{
        private float mTargetScale;//缩放的目标值
        private float x;
        private float y;//中心点
        private final float BIGGER = 1.05F;
        private final float SMALL = 0.95F;
        private float tmpScale;
        public AutoScaleRunnable(float targetScale,float x,float y){
            this.mTargetScale = targetScale;
            this.x = x;
            this.y = y;
            if (getScale()<mTargetScale){
                tmpScale = BIGGER;
            }
            if (getScale()>mTargetScale){
                tmpScale = SMALL;
            }
        }
        @Override
        public void run() {
            mMatrix.postScale(tmpScale,tmpScale,x,y);
            setImageViewInCenterWhenCheck();
            setImageMatrix(mMatrix);
            float currentScale = getScale();
            if (tmpScale>1.0f&&currentScale <mTargetScale || tmpScale<1.0f && currentScale>mTargetScale){
                postDelayed(this,15);
            }else {//设置目标值
                float scale = mTargetScale/currentScale;
                mMatrix.postScale(scale,scale,x,y);
                setImageViewInCenterWhenCheck();
                setImageMatrix(mMatrix);
                isAutoScale = false;
            }
        }
    }
}

此自定义控件可以与viewPager等使用,但是需处理其冲突事件

因为在viewPager中如不处理就无法进行对放大的图片进行点击移动的效果。。。
上述代码中:
  if (getMatrixRectF().width()>getWidth()+0.01
  ||getMatrixRectF().height()>getHeight()+0.01){
                getParent().requestDisallowInterceptTouchEvent(true);
            }
 正是处理这个冲突事件,只是先处理ImageView的放大缩小事件,最后可以进行滑动
作者:songdongpanCSDN 发表于2016/11/30 22:19:13 原文链接
阅读:17 评论:0 查看评论

Android常用整理

$
0
0

SharePreferance

 SharedPreferences sp = getSharedPreferences("app", MODE_PRIVATE);
        // 保存配置到 SharedPreferences
        SharedPreferences.Editor editor = sp.edit();

        // 添加内容到存储区
        editor.putString("name", mTxtName.getText().toString());

        editor.putString("pass", mTxtPass.getText().toString());

        // Editor 必须要 提交 可以使用commit() 或者 apply() (API 9以上)

        editor.apply();
SharedPreferences sp =getSharedPreferences("app",MODE_PRIVATE);

            String name = sp.getString("name", null);
            String pass = sp.getString("pass", null);

机型适配:

  1. 合理使用Wrap_content ,match_parent
  2. 尽可能使用RelativiLayout
  3. 针对不同的机型,使用不同的布局文件(Use Size Qualifiers):
    res/layout/main.xml
    res/layout-large/main.xml
  4. 尽量使用点9图片(Use Nine-patch Bitmaps)

px:即像素,1px代表屏幕上一个物理的像素点;px单位不被建议使用,因为同样100px的图片,在不同手机上显示的实际大小可能不同,偶尔用到px的情况,是需要画1像素表格线或阴影线的时候,用其他单位如dp会显得模糊。
dp也可写为dip,即density-independent pixel。你可以想象dp更类似一个物理尺寸,比如一张宽和高均为100dp的图片在320×480和480×800的手机上“看起来”一样大。而实际上,它们的像素值并不一样。dp正是这样一个尺寸,不管这个屏幕的密度是多少,屏幕上相同dp大小的元素看起来始终差不多大。
sp:sp和dp很类似但唯一的区别是,Android系统允许用户自定义文字尺寸大小(小、正常、大、超大等等),所以目前主流应用字体大小已经改用dp,不用sp,省去用户手动调整字体适配的麻烦。
这里写图片描述

加密解密:

package mobi.vhly.cryptodemo;

/**
 * Created by vhly[FR].
 * <p>
 * Author: vhly[FR]
 * Email: vhly@163.com
 * Date: 2016/10/19
 */

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * 常用的加密算法
 */
public final class CryptUtil {

    private CryptUtil(){}

    ///////////////////////////////////////////////////////////////////////////
    // DES
    ///////////////////////////////////////////////////////////////////////////

    /**
     * DES 加密算法
     * @param data 原始数据
     * @param key 密码,必须是8个字节
     * @return byte[] 经过加密之后的内容
     */
    public static byte[] desEncrypt(byte[] data, byte[] key){
        byte[] ret = null;
        if (data != null && key != null) {
            if(data.length > 0 && key.length == 8){
                // 1. 使用 Cipher 引擎 来初始化 加密,并且设置密码
                try {
                    Cipher cipher = Cipher.getInstance("DES");

                    // 1.1 DESKeySpec 用于描述DES的密码
                    DESKeySpec spec = new DESKeySpec(key);
                    // 1.2 使用 SecretKeyFactory 生成 Key对象
                    SecretKeyFactory factory = SecretKeyFactory.getInstance("DES");
                    SecretKey sk = factory.generateSecret(spec);

                    // 1.3 初始化 Cipher 为加密操作,并且指定密钥
                    cipher.init(Cipher.ENCRYPT_MODE, sk);

                    // 2. 加密数据
                    ret = cipher.doFinal(data);

                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                } catch (NoSuchPaddingException e) {
                    e.printStackTrace();
                } catch (InvalidKeyException e) {
                    e.printStackTrace();
                } catch (InvalidKeySpecException e) {
                    e.printStackTrace();
                } catch (BadPaddingException e) {
                    e.printStackTrace();
                } catch (IllegalBlockSizeException e) {
                    e.printStackTrace();
                }
            }
        }
        return ret;
    }

    /**
     * DES 解密算法
     * @param data 原始数据
     * @param key 密码,必须是8个字节
     * @return byte[] 经过解密之后的内容
     */
    public static byte[] desDecrypt(byte[] data, byte[] key){
        byte[] ret = null;
        if (data != null && key != null) {
            if(data.length > 0 && key.length == 8){
                // 1. 使用 Cipher 引擎 来初始化 解密,并且设置密码
                try {
                    Cipher cipher = Cipher.getInstance("DES");

                    // 1.1 DESKeySpec 用于描述DES的密码
                    DESKeySpec spec = new DESKeySpec(key);
                    // 1.2 使用 SecretKeyFactory 生成 Key对象
                    SecretKeyFactory factory = SecretKeyFactory.getInstance("DES");
                    SecretKey sk = factory.generateSecret(spec);

                    // 1.3 初始化 Cipher 为解密操作,并且指定密钥
                    cipher.init(Cipher.DECRYPT_MODE, sk);

                    // 2. 解密数据
                    ret = cipher.doFinal(data);

                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                } catch (NoSuchPaddingException e) {
                    e.printStackTrace();
                } catch (InvalidKeyException e) {
                    e.printStackTrace();
                } catch (InvalidKeySpecException e) {
                    e.printStackTrace();
                } catch (BadPaddingException e) {
                    e.printStackTrace();
                } catch (IllegalBlockSizeException e) {
                    e.printStackTrace();
                }
            }
        }
        return ret;
    }

    ///////////////////////////////////////////////////////////////////////////
    // AES 方式1
    ///////////////////////////////////////////////////////////////////////////

    public static byte[] aesEncryptSimple(byte[] data, byte[] key){
        byte[] ret = null;
        if (data != null && key != null) {
            if(data.length > 0 && key.length == 16){
                // AES 128bit = 16bytes
                try {
                    Cipher cipher = Cipher.getInstance("AES");
                    SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
                    cipher.init(Cipher.ENCRYPT_MODE, keySpec);
                    ret = cipher.doFinal(data);
                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                } catch (NoSuchPaddingException e) {
                    e.printStackTrace();
                } catch (InvalidKeyException e) {
                    e.printStackTrace();
                } catch (BadPaddingException e) {
                    e.printStackTrace();
                } catch (IllegalBlockSizeException e) {
                    e.printStackTrace();
                }
            }
        }
        return ret;
    }
    public static byte[] aesDecryptSimple(byte[] data, byte[] key){
        byte[] ret = null;
        if (data != null && key != null) {
            if(data.length > 0 && key.length == 16){
                // AES 128bit = 16bytes
                try {
                    Cipher cipher = Cipher.getInstance("AES");
                    SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
                    cipher.init(Cipher.DECRYPT_MODE, keySpec);
                    ret = cipher.doFinal(data);
                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                } catch (NoSuchPaddingException e) {
                    e.printStackTrace();
                } catch (InvalidKeyException e) {
                    e.printStackTrace();
                } catch (BadPaddingException e) {
                    e.printStackTrace();
                } catch (IllegalBlockSizeException e) {
                    e.printStackTrace();
                }
            }
        }
        return ret;
    }

    ///////////////////////////////////////////////////////////////////////////
    // AES 方式2 使用两套密码
    ///////////////////////////////////////////////////////////////////////////

    /**
     * 使用两套密码的加密,强度更高
     * @param data 数据
     * @param key 第一个密码
     * @param ivData 第二个密码
     * @return byte[]
     */
    public static byte[] aesEncryptWithIv(byte[] data, byte[] key, byte[] ivData){
        byte[] ret = null;
        if (data != null && key != null && ivData != null) {
            if(data.length > 0 && key.length == 16 && ivData.length == 16){
                // 使用两套密码的,算法需要写成 AES/算法模式/填充模式
                try {
                    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
                    // 准备第一套密码
                    SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
                    // 准备第二套密码
                    IvParameterSpec iv = new IvParameterSpec(ivData);
                    // 设置两套密码的初始化
                    cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
                    cipher.update(data);
                    ret = cipher.doFinal();
                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                } catch (NoSuchPaddingException e) {
                    e.printStackTrace();
                } catch (InvalidAlgorithmParameterException e) {
                    e.printStackTrace();
                } catch (InvalidKeyException e) {
                    e.printStackTrace();
                } catch (BadPaddingException e) {
                    e.printStackTrace();
                } catch (IllegalBlockSizeException e) {
                    e.printStackTrace();
                }
            }
        }
        return ret;
    }

    public static byte[] aesDecryptWithIv(byte[] data, byte[] key, byte[] ivData){
        byte[] ret = null;
        if (data != null && key != null && ivData != null) {
            if(data.length > 0 && key.length == 16 && ivData.length == 16){
                // 使用两套密码的,算法需要写成 AES/算法模式/填充模式
                try {
                    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
                    // 准备第一套密码
                    SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
                    // 准备第二套密码
                    IvParameterSpec iv = new IvParameterSpec(ivData);
                    // 设置两套密码的初始化
                    cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
                    cipher.update(data);
                    ret = cipher.doFinal();
                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                } catch (NoSuchPaddingException e) {
                    e.printStackTrace();
                } catch (InvalidAlgorithmParameterException e) {
                    e.printStackTrace();
                } catch (InvalidKeyException e) {
                    e.printStackTrace();
                } catch (BadPaddingException e) {
                    e.printStackTrace();
                } catch (IllegalBlockSizeException e) {
                    e.printStackTrace();
                }
            }
        }
        return ret;
    }

    ///////////////////////////////////////////////////////////////////////////
    // RSA
    ///////////////////////////////////////////////////////////////////////////

    // 1. 生成密钥对 公钥和私钥

    /**
     *
     * @param bits 必须在 1024,2048
     * @return
     */
    public static KeyPair generateRsaKey(int bits){
        KeyPair ret = null;
        try {
            KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");

            kpg.initialize(bits);

            ret = kpg.generateKeyPair();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return ret;
    }

    /**
     * RSA加密,使用公钥加密,那么必须使用私钥解密
     *         使用私钥机密,那么必须使用公钥解密
     * @param data
     * @param key
     * @return
     */
    public static byte[] rsaEncrypt(byte[] data, Key key){
        byte[] ret = null;
        if (data != null && data.length > 0 && key != null) {
            try {
                Cipher cipher = Cipher.getInstance("RSA");
                cipher.init(Cipher.ENCRYPT_MODE, key);
                ret = cipher.doFinal(data);
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (NoSuchPaddingException e) {
                e.printStackTrace();
            } catch (InvalidKeyException e) {
                e.printStackTrace();
            } catch (BadPaddingException e) {
                e.printStackTrace();
            } catch (IllegalBlockSizeException e) {
                e.printStackTrace();
            }
        }
        return ret;
    }

    public static byte[] rsaDecrypt(byte[] data, Key key){
        byte[] ret = null;
        if (data != null && data.length > 0 && key != null) {
            try {
                Cipher cipher = Cipher.getInstance("RSA");
                cipher.init(Cipher.DECRYPT_MODE, key);
                ret = cipher.doFinal(data);
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (NoSuchPaddingException e) {
                e.printStackTrace();
            } catch (InvalidKeyException e) {
                e.printStackTrace();
            } catch (BadPaddingException e) {
                e.printStackTrace();
            } catch (IllegalBlockSizeException e) {
                e.printStackTrace();
            }
        }
        return ret;
    }

}
作者:qq_34306963 发表于2016/11/30 22:27:38 原文链接
阅读:27 评论:0 查看评论

微信接入探秘(二)——懒人的OXM之路

$
0
0

本文出处:http://blog.csdn.net/chaijunkun/article/details/53396765,转载请注明。由于本人不定期会整理相关博文,会对相应内容作出完善。因此强烈建议在原始出处查看此文

在上一篇博文中,简单对微信接口分成了两大类:被动回调接口和主动调用接口。并阐述了两类接口的实现机理。在序列图中我们可以得知任何数据都是有具体格式要求的,那么如何管理这些结构化的数据呢?这里就跟大家分享一下我在管理实体对象时走过的一些弯路和技术探索。

本专栏代码可在本人的CSDN代码库中下载,项目地址为:https://code.csdn.net/chaijunkun/wechat-common

抽象数据层次关系

Java是一门面向对象的语言,要方便地使用结构化的数据,就必须对其进行对象化映射。抽象数据层次是一个很主观的事情,好在微信API已经规定好了格式,我们要做的就是对其进行更好地适配。一定要时刻谨记面向对象三要素:继承、封装和多态。

接下来就是发现规律的时刻。挑重点看了一下关于回调接口文档,下面是一些消息的格式说明:

普通消息

普通消息是用户显式发送给公众号的消息,现有的消息类型分别覆盖了在客户端能直接发送的所有消息种类。

文本消息

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>1348831860</CreateTime>
    <MsgType><![CDATA[text]]></MsgType>
    <Content><![CDATA[this is a test]]></Content>
    <MsgId>1234567890123456</MsgId>
</xml>

图片消息

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>1348831860</CreateTime>
    <MsgType><![CDATA[image]]></MsgType>
    <PicUrl><![CDATA[this is a url]]></PicUrl>
    <MediaId><![CDATA[media_id]]></MediaId>
    <MsgId>1234567890123456</MsgId>
</xml>

语音消息

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>1357290913</CreateTime>
    <MsgType><![CDATA[voice]]></MsgType>
    <MediaId><![CDATA[media_id]]></MediaId>
    <Format><![CDATA[Format]]></Format>
    <Recognition><![CDATA[腾讯微信团队]]></Recognition>
    <MsgId>1234567890123456</MsgId>
</xml>

视频消息

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>1357290913</CreateTime>
    <MsgType><![CDATA[video]]></MsgType>
    <MediaId><![CDATA[media_id]]></MediaId>
    <ThumbMediaId><![CDATA[thumb_media_id]]></ThumbMediaId>
    <MsgId>1234567890123456</MsgId>
</xml>

小视频消息

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>1357290913</CreateTime>
    <MsgType><![CDATA[shortvideo]]></MsgType>
    <MediaId><![CDATA[media_id]]></MediaId>
    <ThumbMediaId><![CDATA[thumb_media_id]]></ThumbMediaId>
    <MsgId>1234567890123456</MsgId>
</xml>

地理位置消息

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>1351776360</CreateTime>
    <MsgType><![CDATA[location]]></MsgType>
    <Location_X>23.134521</Location_X>
    <Location_Y>113.358803</Location_Y>
    <Scale>20</Scale>
    <Label><![CDATA[位置信息]]></Label>
    <MsgId>1234567890123456</MsgId>
</xml>

链接消息

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>1351776360</CreateTime>
    <MsgType><![CDATA[link]]></MsgType>
    <Title><![CDATA[公众平台官网链接]]></Title>
    <Description><![CDATA[公众平台官网链接]]></Description>
    <Url><![CDATA[url]]></Url>
    <MsgId>1234567890123456</MsgId>
</xml>

普通消息格式的总结

从以上数据中我们可以发现如下规律:
1. 数据均为XML格式,且根节点名称均为“xml”;
2. 目前都是一层深度,没有标签的嵌套,标签内也没有属性,结构比较简单
3. 有一些节点为了防止内容注入攻击,使用了CDATA
4. 有共同的节点。这里我们可以抽出出一个公共的XML结构

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>1351776360</CreateTime>
    <MsgType><![CDATA[link]]></MsgType>
    <MsgId>1234567890123456</MsgId>
</xml>

有人一定觉得,这太容易了,就用这个接口映射的实体当父类,然后依具体消息类型格式对其进行继承。先别着急,当时作者也是基于先进行了映射,导致在实现后续的功能时发现了问题。

事件推送

在本节中,我们接触一种特殊的消息。该消息并不是由用户显示发送,而是由用户一系列操作引发微信通知公众号平台而收到的消息。

关注/取消关注事件

关注事件

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[FromUser]]></FromUserName>
    <CreateTime>123456789</CreateTime>
    <MsgType><![CDATA[event]]></MsgType>
    <Event><![CDATA[subscribe]]></Event>
</xml>

用户未关注时,扫描二维码进入,进行关注后的事件推送

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[FromUser]]></FromUserName>
    <CreateTime>123456789</CreateTime>
    <MsgType><![CDATA[event]]></MsgType>
    <Event><![CDATA[subscribe]]></Event>
    <EventKey><![CDATA[qrscene_123123]]></EventKey>
    <Ticket><![CDATA[TICKET]]></Ticket>
</xml>

值得一提的是,由于微信产品设计的原因,当用户扫描公众号生成的二维码后,若用户没有关注过该公众号,则用户在点击提示信息中的“确定”后,微信回调公众号后台的消息并非是扫描带参数二维码事件,而是“关注事件”。与普通的关注事件不同的是,由二维码扫描进入的回调事件消息会多出EventKey和Ticket两个节点

取消关注事件

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[FromUser]]></FromUserName>
    <CreateTime>123456789</CreateTime>
    <MsgType><![CDATA[event]]></MsgType>
    <Event><![CDATA[unsubscribe]]></Event>
</xml>

扫描带参数二维码事件

用户已关注时的事件推送

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[FromUser]]></FromUserName>
    <CreateTime>123456789</CreateTime>
    <MsgType><![CDATA[event]]></MsgType>
    <Event><![CDATA[SCAN]]></Event>
    <EventKey><![CDATA[SCENE_VALUE]]></EventKey>
    <Ticket><![CDATA[TICKET]]></Ticket>
</xml>

上报地理位置事件

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>123456789</CreateTime>
    <MsgType><![CDATA[event]]></MsgType>
    <Event><![CDATA[LOCATION]]></Event>
    <Latitude>23.137466</Latitude>
    <Longitude>113.352425</Longitude>
    <Precision>119.385040</Precision>
</xml>

自定义菜单事件

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[FromUser]]></FromUserName>
    <CreateTime>123456789</CreateTime>
    <MsgType><![CDATA[event]]></MsgType>
    <Event><![CDATA[CLICK]]></Event>
    <EventKey><![CDATA[EVENTKEY]]></EventKey>
</xml>

事件推送格式的总结

从以上数据中我们可以发现如下规律:
1. 数据均为XML格式,且根节点名称均为“xml”;
2. 目前都是一层深度,没有标签的嵌套,标签内也没有属性,结构比较简单
3. 有一些节点为了防止内容注入攻击,使用了CDATA
4. 有共同的节点。这里我们可以抽出出一个公共的XML结构

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[FromUser]]></FromUserName>
    <CreateTime>123456789</CreateTime>
    <MsgType><![CDATA[event]]></MsgType>
    <Event><![CDATA[subscribe]]></Event>
</xml>

搭建数据结构继承关系

通过上文我们可以总结出,无论是普通消息还是事件推送,都拥有很多类似的结构,这也就保证了对数据结构的封装构想是行得通的。首先我们来分析一下不同类型的消息是怎样区分开的,因为只有深入了解这一区别,才能让我们搭建的数据结构很好地服务于最终的程序。消息的具体类型是由MsgType和Event两个因子共同决定的

Created with Raphaël 2.1.0接收到消息获取MsgType字段获取Event字段MsgType=event?Event=subscribe?转换为关注事件结束Event=unsubscribe?转换为取消关注事件Event=SCAN?转换为扫描带参数二维码事件Event=LOCATION?转换为上报地理位置事件Event=CLICK?转关为自定义菜单事件忽略此消息MsgType=text?转换为文本消息MsgType=image?转换为图片消息MsgType=voice?转换为语音消息MsgType=video?转换为视频消息MsgType=shortvideo?转换为小视频消息MsgType=location?转换为地理位置消息yesnoyesnoyesnoyesnoyesnoyesnoyesnoyesnoyesnoyesnoyesnoyesno

按照上述流程进行匹配后,即可得到具体消息类型。于是我们有了充分的理由,先构建一个专门用于判断消息类型的实体:TypeAnalyzingBean

public class TypeAnalyzingBean {
    /** 消息类型 */
    private String msgType;
    /** 事件类型 */
    private String event;
    //省略getters 和 setters...
}

得到了具体类型后就可以拆分为普通消息事件推送的父类定义,但是过程中我们发现两者还有很多共同之处,所以新建了一个公共的字段的实体定义:CommonXML

public class CommonXML {
    /** 接收方微信号 */
    private String toUserName;
    /** 发送方微信号,若为普通用户,则是一个OpenID */
    private String fromUserName;
    /** 消息创建时间 */
    private Long createTime;
    /** 消息类型 */
    private String msgType;
    //省略getters 和 setters...
}

普通消息的基础实体定义:BaseMsg,实际上只是在公共字段的基础上增加了msgId属性。

public class BaseMsg extends CommonXML {
    /** 消息id */
    private Long msgId;
    //省略getters 和 setters...
}

事件推送的基础实体定义:BaseEvent,实际上只是在公共字段的基础上增加了event属性。

public class BaseEvent extends CommonXML {
    /** 事件类型 */
    private String event;
    //省略getters 和 setters...
}

有了这两个基础实体,其他的消息和事件只需要分别继承两个基类,进行相应的属性拓展即可,这里就不赘述了。

选一个好的OXM框架很重要

微信回调给我们的数据是XML格式的,而我们要面向对象开发,于是必须将这两者映射起来,这就是OXM(Object/XML Mapping)。细心的你也许已经发现,回调给我们的代码风格和Java属性命名风格是不一致的,因此我们要把原有的节点名转义后映射到对应的实体属性上。虽然Java包中提供的w3c API可以实现XML的DOM解析,但是需要一个节点一个节点地爬,整个过程将异常繁琐,后期极难维护,这里就不再展示了。接下来我将介绍第一版使用的JAXB和最终采用的基于Jackson StAX的方案,并进行对比。

在这里我们以最常用的文本消息作为示例,它的继承关系为:

Created with Raphaël 2.1.0TextMsgBaseMsgCommonXML
public class TextMsg extends BaseMsg {
    /** 文本消息内容 */
    private String content;
    //省略getters 和 setters...
}

JAXB方式的OXM

JAXB支持annotation方式的命名转义,这也正是我在设计之初选择其作为基础OXM框架的原因。由于所有的XML根节名称都是“xml”,因此想将根节点重命名annotation放到CommonXML上,这样,其他类直接或间接继承自他,注解也能够通过继承链来爬取到相应的配置。

这里可以很负责地告诉你,我的想法还是Too young too simple了,最早尝试的是JDK1.6+自带的JAXB实现,这个实现有个恶心的问题:父类注解无法继承到子类。只有在每一个相关类上设置@XmlRootElement(name=”xml”)才可以,否则将报这样的错误:com.sun.istack.internal.SAXException2: unable to marshal type “net.csdn.blog.chaijunkun.wechat.common.callback.xml.msg.TextMsg” as an element because it is missing an @XmlRootElement annotation。

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

import org.eclipse.persistence.oxm.annotations.XmlCDATA;

@XmlRootElement(name="xml")
@XmlType(name="", namespace="")
@XmlAccessorType(XmlAccessType.FIELD)
public class CommonXML {
    @XmlElement(name="ToUserName")
    @XmlCDATA
    private String toUserName;
    //后面的代码就不贴了,只是一些重命名操作和一些getters/setters
}

由于JAXB对CDATA支持得不好,需要自己写@XmlJavaTypeAdapter(CDataAdapter.class)中的CDataAdapter,而且网上找到的例子经过评估,有注入漏洞,于是又额外引入了EclipseLink的moxy(一套JAXB的实现):

<dependency>
    <groupId>org.eclipse.persistence</groupId>
    <artifactId>org.eclipse.persistence.moxy</artifactId>
    <version>2.6.3</version>
</dependency>

为此我写了一个单元测试看下序列化与反序列化的测试:

public class XMLTest extends BaseTest {

    @Test
    public void doTest() throws JAXBException{
        TextMsg msg = new TextMsg();
        msg.setToUserName("jackson");
        msg.setFromUserName("hawaii");
        msg.setContent("jack<xml val='Json'>]]>");

        ByteArrayOutputStream xmlOut = null;
        ByteArrayInputStream xmlIn = null;
        try{
            xmlOut = new ByteArrayOutputStream();
            XMLFactory.toXML(msg, xmlOut);
            String xml = new String(xmlOut.toByteArray());
            logger.info("生成xml:{}", xml);
            xmlIn = new ByteArrayInputStream(xml.getBytes());
            TextMsg msgFromXml = XMLFactory.fromXML(xmlIn, TextMsg.class);
            logger.info("反序列化结果:发送方:{}, 接收方:{}, 内容:{}", msgFromXml.getFromUserName(), msgFromXml.getToUserName(), msgFromXml.getContent());
        }finally{
            IOUtils.closeQuietly(xmlIn);
            IOUtils.closeQuietly(xmlOut);
        }
    }
}

由于CDATA的结束符是“]]>”,在设置消息内容时特别使用了带有歧义性的文字来测试注入危险。先来看下序列化的结果:

<?xml version="1.0" encoding="utf-8"?>
<xml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="textMsg">
    <ToUserName><![CDATA[jackson]]></ToUserName>
    <FromUserName><![CDATA[hawaii]]></FromUserName>
    <Content><![CDATA[jack<xml val='Json'>]]]]><![CDATA[>]]></Content>
</xml>

moxy实现的JAXB有效地解决了父级注解无法继承到子类上的问题,CDATA内容也顺利通过了我们的测试(它的解决方案是将能够引起歧义的字符组合”]]>”拆分成了”]]”和“>”,然后再分别使用CDATA标签进行包装),但是也带来了新的问题。我们再来回顾一下官方文档提供给我们的回调xml数据格式:

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>1348831860</CreateTime>
    <MsgType><![CDATA[text]]></MsgType>
    <Content><![CDATA[this is a test]]></Content>
    <MsgId>1234567890123456</MsgId>
</xml>

通过对比我们发现使用moxy生成的数据首行多出了xml指令。它标记了文档版本和编码格式,经过相关验证,这个变动和微信对接没有影响,可以正常运行。而根节点“xml”中增加了“xmlns:xsi”“xsi:type”属性,在将这样的数据返回给微信回调时报错了,这两个多余的属性必须去掉才能正常工作。

尽管实体->XML没问题了,但接入微信联调时发现XML->实体又出现了状况:在确定了消息类型后,我们需要把XML字符串立即转换成明确的具体消息类型,例如TextMsg.class。然而由于TextMsg实体上没有@XmlRootElement(name=”xml”)根节点注解,转换失败,类似于使用原生JAXB的状况。设想一下,本来希望“根节点命名”这件事放在一处(CommonXML)来做,现在不得不每个子类上都标注上这头疼的家伙,将来维护起来是多么困难。

经过很多尝试都无果的情况下,JAXB方案我准备放弃了。为了方便大家研究,这部分代码我也上传到了我的代码仓库中,有兴趣的读者可以运行单元测试一试下,仓库地址:https://code.csdn.net/chaijunkun/wechat-common-draft

另辟蹊径的Jackson

说到Jackson很多人都体验过它的JSON处理能力,无论是从灵活性、性能还是文档健全度上都是值得在生产环境中使用的,本人也是其忠实拥趸。就在和JAXB纠结寻找其它途径时,得知Jackson的XML处理能力也很了得,于是立刻将代码进行改造,适配Jackson类似功能的注解:

import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlCData;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;

@JacksonXmlRootElement(localName="xml")
public class CommonXML {
    /** 接收方微信号 */
    @JacksonXmlProperty(localName = "ToUserName")
    @JacksonXmlCData
    private String toUserName;
    //后面的代码就不贴了,只是一些重命名操作和一些getters/setters
}

根据一些StackOverflow上的资料,整理出了一些必要的依赖:

<!-- json和xml的序列化与反序列化工具 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.5.2</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.5.2</version>
</dependency>
<!-- 使用woodstox不仅比jdk提供的stax实现更快,且可以避免一些已知问题例如在根节点自动添加namespace -->
<dependency>
    <groupId>org.codehaus.woodstox</groupId>
    <artifactId>woodstox-core-asl</artifactId>
    <version>4.4.1</version>
</dependency>

使用Jackson框架处理实体与XML相互转换所担心的注入问题还可以通过转义单个特殊字符的形式来处理,特殊字符及其可用代替字符的对照表:

特殊字符 转义字符
& &amp;
< &lt;
> &gt;
&quot;
&apos;

因此只需要写一个特殊字符互转功能即可。代码逻辑很简单,感兴趣的读者可参阅项目中的XMLStringSerializerXMLStringDeserializer,之后在XMLUtil初始化时将它们注册到JacksonXmlModule实例中即可:

JacksonXmlModule module = new JacksonXmlModule();
module.setDefaultUseWrapper(false);
//序列化与反序列化时针对特殊字符自动转义的工具
module.addSerializer(String.class, new XMLStringSerializer());
module.addDeserializer(String.class, new XMLStringDeserializer());

同样的测试数据,我们看下使用Jackson后会生成什么样的结果:

<?xml version='1.0' encoding='UTF-8'?>
<xml>
    <ToUserName><![CDATA[jackson]]></ToUserName>
    <FromUserName><![CDATA[hawaii]]></FromUserName>
    <Content><![CDATA[jack&lt;xml val=&apos;Json&apos;&gt;]]&gt;]]></Content>
</xml>

可以很明显地看到,根节点不再有讨厌的多余属性了,CDATA注入测试也通过了,没有产生歧义。后续和微信联调时再也没遇到格式不正确的问题了。

对比性能

我们已经通过Jackson完成了实体与XML的相互转换功能,然而性能怎么样呢?不妨做个测试。分别做10,000次转换。
实体->XML转换的测试代码:

/** 计数器 */
private int counter = 10000;

@Test
public void doTest() throws JAXBException{
    TextMsg msg = new TextMsg();
    msg.setToUserName("jackson");
    msg.setFromUserName("hawaii");
    msg.setContent("jack<xml val='Json'>]]>");
    long start = System.currentTimeMillis();
    for(int i=0; i< counter; i++){
        ByteArrayOutputStream xmlOut = null;
        ByteArrayInputStream xmlIn = null;
        try{
            xmlOut = new ByteArrayOutputStream();
            //使用Jackson时,替换成XMLUtil及其配套注解的TextMsg对象
            XMLFactory.toXML(msg, xmlOut);
            String xml = new String(xmlOut.toByteArray());
        }finally{
            IOUtils.closeQuietly(xmlIn);
            IOUtils.closeQuietly(xmlOut);
        }
    }
    long end = System.currentTimeMillis();
    logger.info("耗时:{}", end - start);
}

XML->实体转换的测试代码:

/** 计数器 */
private int counter = 10000;

@Test
public void doTest() throws IOException, JAXBException{
    String xml = "<?xml version='1.0' encoding='UTF-8'?><xml><ToUserName><![CDATA[jackson]]></ToUserName><FromUserName><![CDATA[hawaii]]></FromUserName><Content><![CDATA[jack&lt;xml val=&apos;Json&apos;&gt;]]&gt;]]></Content></xml>";
    long start = System.currentTimeMillis();
    for(int i=0; i< counter; i++){
        ByteArrayOutputStream xmlOut = null;
        ByteArrayInputStream xmlIn = null;
        try{
            xmlOut = new ByteArrayOutputStream();
            //使用Jackson时,替换成XMLUtil及其配套注解的TextMsg对象
            TextMsg textMsg = XMLFactory.fromXML(xml, TextMsg.class);
        }finally{
            IOUtils.closeQuietly(xmlIn);
            IOUtils.closeQuietly(xmlOut);
        }
    }
    long end = System.currentTimeMillis();
    logger.info("耗时:{}", end - start);
}

截取的实验结果(单位:毫秒,3次实验取平均值)如下表所示:

转换方式 \ 转换方案 JAXB Jackson 时间占比
实体->XML 24716 1123 22:1
XML->实体 31622 1049 30:1

结果很明显:执行相同的任务,序列化方向Jackson用时只需JAXB的1/22,而反序列化方向优势更明显,只是JAXB方案的1/30的时间。

作者:chaijunkun 发表于2016/11/30 22:37:35 原文链接
阅读:34 评论:0 查看评论

iOS纯Autolayout实现微信朋友圈和通讯录另附App启动页短视频效果

$
0
0

       根据个人的习惯而定,本博客主要以Autolayout为主,早之前没接触的时候,已经看习惯了代码布局UI,又长又

臭,而且主要是写出来不一定正确,跑起来的时候只有出一点错误,UI就飞了,一点都不直观,没错,这也是对立的

两派,由于习惯问题,很多人不愿意去接触Autolayout,但是它的存在真的很强大,首先考虑下微信微博发动态这类

布局UI,少说要上千行代码吧,但是Autolayout就可以为你省去那么多代码,你只要写逻辑就可以了,这样看上去非

常清晰,之前有写过一个Demo是简单介绍如何用Autolayout实现高度自适应的,比较简单,适合入门,需要的朋友可

以用力戳点击打开链接


     有个朋友需要让我写个朋友圈的Demo给他,正好晚上回去也没什么事,就写个微信Demo玩玩


启动页动画



朋友圈示例

             



介绍下主要分析的知识点

1.微信朋友圈AutoLayout实现高度自适应

2.微信通讯路根据名字首字母分类排序

3.首次启动用短视频来做动画


首先给大家介绍个小小的框架,专门用来写设置页面这种老变来变去cell需求的框架

IASKAppSettingsViewController

最简单的做法就是跟着demo来一遍,配置你需要的Plist文件,把每个cell需要展示的信息存起来



用的时候你只要在控制器根据之前plist里面配置好的key来读就可以了,非常容易修改配置,免得需求该来该去麻烦

- (void)settingsViewController:(IASKAppSettingsViewController*)sender tableView:(UITableView *)tableView didSelectCustomViewSpecifier:(IASKSpecifier*)specifier
{
        NSIndexPath * indexPath = [sender.settingsReader indexPathForKey:specifier.key];
        [tableView deselectRowAtIndexPath:indexPath animated:YES];
    
        CASE_TYPE type = [self caseIndexForKey:specifier.key];
    switch (type) {
        case FRIENTDCIRCLE:
        {
            DiscoveryViewController *discoverVC = [[DiscoveryViewController alloc] init];
            [self.navigationController pushViewController:discoverVC animated:YES];
        }
            break;
        case SHAKEANDSHAKE:
            
            break;
        case YAOYIYAO:
            
            break;
        case NEARBYPEOPLE:
            
            break;
        case SHOPPING:
            
            break;
        case GAME:
            
            break;
            
        default:
            break;
    }
}


进入正题,用Autolayout来布局高度自适应的朋友圈,控制的还是tableview,最关键的还是那个cell的约束设置,直

接看约束图



一套完整的约束必须是上下左右各边都有约束到,上面复杂约束已经做到了上左下右都有约束,如果有一边漏了,虽

然约束不会报错不会警告,但是如果你不完整,你到时候根本无法计算,看下最简单的约束示例



那么问题来了,布局完了是非常的简单高效,这应该不难吧,觉得难的看我一开始给的链接,那个入门级别的

1.label如何实现自适应高度

2.collectionView如何根据不同图片个数增加高度

3.评论的tableview如何动态变化


注:这里tableview的cell里面嵌了tableview和collectionView,那么这两个的代理方法就不介绍了,主要看如何动态变化,需要看的同学到时候自己下载代码跑起来看看就好了



关键代码如下

- (void)configCell:(MKJFriendTableViewCell *)cell indexpath:(NSIndexPath *)indexpath
{
    __weak typeof(cell)weakCell = cell;
    FriendIssueInfo *issueInfo = self.friendsDatas[indexpath.row];
    // headerImage 头像 实现渐隐效果
    [cell.headerImageView sd_setImageWithURL:[NSURL URLWithString:issueInfo.photo] placeholderImage:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
       
        if (image && cacheType == SDImageCacheTypeNone)
        {
            weakCell.headerImageView.alpha = 0;
            [UIView animateWithDuration:0.8 animations:^{
                weakCell.headerImageView.alpha = 1.0f;
            }];
        }
        else
        {
            weakCell.headerImageView.alpha = 1.0f;
        }
    }];
    
    // name 名字
    cell.nameLabel.text = issueInfo.userName;
    
    // description 描述 根据配置在数据源的是否展开字段确定行数
    cell.desLabel.text = issueInfo.message;
    cell.isExpand = issueInfo.isExpand;
    if (issueInfo.isExpand)
    {
        cell.desLabel.numberOfLines = 0;
        
    }
    else
    {
        cell.desLabel.numberOfLines = 3;
    }
    
    // 全文label 根据文字的高度是否展示全文lable  点击事件通过数据源来交互
    CGSize rec = [issueInfo.message boundingRectWithSize:CGSizeMake(SCREEN_WIDTH - 90, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName : [UIFont boldSystemFontOfSize:14]} context:nil].size;
    if (rec.height > 55) {
        cell.showAllDetailsButton.hidden = NO;
        cell.showAllHeight.constant = 15;
    }
    else
    {
        cell.showAllHeight.constant = 0;
        cell.showAllDetailsButton.hidden = YES;
    }

    
    // img  九宫格图片,用collectionView做
    cell.imageDatas = [[NSMutableArray alloc] initWithArray:issueInfo.messageSmallPics];
    cell.imageDatasBig = [[NSMutableArray alloc] initWithArray:issueInfo.messageBigPics];
    [cell.collectionView reloadData];
    // 这里可以用lauout来获取其高度,但是由于嵌套的关系,可能算不准,我们还是手动算好了
//    [cell.collectionView layoutIfNeeded];
//    cell.colletionViewHeight.constant = cell.collectionView.collectionViewLayout.collectionViewContentSize.height;
    CGFloat width = SCREEN_WIDTH - 90 - 20;
    // 没图片就高度为0 (约束是可以拖出来的哦哦)
    if ([NSArray isEmpty:issueInfo.messageSmallPics])
    {
        cell.colletionViewHeight.constant = 0;
    }
    else
    {
        if (issueInfo.messageSmallPics.count == 1)
        {
            cell.colletionViewHeight.constant = width / 1.5;
        }
        else
        {
            cell.colletionViewHeight.constant = ((issueInfo.messageSmallPics.count - 1) / 3 + 1) * (width / 3) + (issueInfo.messageSmallPics.count - 1) / 3 * 15;
        }
    }
    
    // timeStamp  时间
    cell.timeLabel.text = issueInfo.timeTag;
    
    // right action button  弹出黑色点赞或者评论的框
    cell.isShowPopView = NO;
    cell.backPopViewWidth.constant = 0;
    
    // right action button inline like button state   按钮状态也是根据数据源配置
    if (issueInfo.hadClickLike) {
        [cell.likeButton setTitle:@"取消" forState:UIControlStateNormal];
    }
    else
    {
        [cell.likeButton setTitle:@"赞" forState:UIControlStateNormal];
    }
    cell.hadLikeActivityMessage = issueInfo.hadClickLike; // 默认都是没有点赞
    
    // commentTableView  评论的taibleView
    // 这里的思路也是可以根据contentSize获取,但是貌似又可能算不准,我还是手动计算,思路就是
    // 最后一个cell的Y轴坐标加上其高度就是算出来的高度啦
    cell.commentdatas = [[NSMutableArray alloc] initWithArray:issueInfo.commentMessages];
    [cell.commentTableView reloadData];
    CGRect recHeight = CGRectZero;
    if (![NSArray isEmpty:issueInfo.commentMessages])
    {
        recHeight = [cell.commentTableView rectForRowAtIndexPath:[NSIndexPath indexPathForRow:issueInfo.commentMessages.count - 1 inSection:0]];
    }
    cell.tableViewHeight.constant = recHeight.origin.y + recHeight.size.height;
//    NSLog(@"%@,heightTable%f",indexpath,cell.tableViewHeight.constant);
}


注:可能要备注下,这里的collectionView和tableview的约束高度是可以拖出来重新赋值的,同学一定要记得啊,毕

竟动态高度,需要根据数据源来配置,而且这里点赞,收起什么的都用数据源来配置了


OK,根据上面的完美约束以及代码动态算了高度(这里内嵌的两个tableview和collection都可以用contentsize来获

取高度哦,也可以像我这样自己算),直接调用下面一句代码,高度自适应完美搞定!!!

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return [tableView fd_heightForCellWithIdentifier:identify cacheByIndexPath:indexPath configuration:^(MKJFriendTableViewCell *cell) {
        
        [self configCell:cell indexpath:indexPath];
        
    }];


功能点还是蛮多的

1.点赞

2.评论

3.文字展开收缩

4.图片展示

5.楼中楼评论

6.这么多都会涉及到复用,需要注意呀同学们,而且这是Demo,还有很多地方需要优化啊


简单介绍一种评论的键盘问题,需要更多的自行拉倒最下面去下载就好了


计算弹起高度看图就可以明白了,再配合下代码即可

#pragma mark - 键盘的代理 show or hidden
- (void)keyboardWillShow:(NSNotification *)noti
{
    self.isKeyboardShowing = YES;
    NSDictionary *userInfo = noti.userInfo;
    CGFloat keyboardHeight = [[userInfo valueForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
    CGFloat delta = 0;
    CGFloat topOffsetKeyboard = 0;
    if (self.isResponseByCell)
    { // 是通过点击cell触发键盘
        topOffsetKeyboard = [UIScreen mainScreen].bounds.size.height - keyboardHeight - kChatToolBarHeight - self.selectedCellHeight - 20;
        
    }
    else // 点击comment触发
    {
        topOffsetKeyboard = [UIScreen mainScreen].bounds.size.height - keyboardHeight - kChatToolBarHeight - 30;
    }
    delta = self.touch_offset_y - topOffsetKeyboard;
    
    CGFloat contentOffset = self.tableView.contentOffset.y; // 这个指的是顶部tableView滚动的距离
    contentOffset += delta; // delta + 下拉  -上拉
    if (contentOffset <= -64) {
        contentOffset = -64;
    }
    [self.tableView setContentOffset:CGPointMake(self.tableView.contentOffset.x, contentOffset) animated:YES];
}


其实主要还是涉及到不同子View之前的坐标转换到window上坐标的转换方式,理解不了的可以看点击打开链接


下面分析下通讯录里面实现搜索以及汉字转换成英文首字母排序

1.搜索控制器UISearchController点击打开链接

self.searchResult = [[SearchResultController alloc] init];
    self.searchResult.block = ^{
        
        [weakSelf.searchController.searchBar resignFirstResponder];
        
    };
    self.searchController = [[UISearchController alloc] initWithSearchResultsController:self.searchResult];
    self.searchController.searchResultsUpdater = self;
    self.searchController.searchBar.placeholder = @"查找联系人";
    self.searchController.searchBar.delegate = self;
    
    self.searchController.searchBar.showsCancelButton = NO;
    self.searchController.searchBar.returnKeyType = UIReturnKeySearch;
    self.searchController.searchBar.backgroundColor = RGBA(153, 153, 153, 1);
    self.searchController.searchBar.backgroundImage = [UIImage new];
    UITextField *searchBarTextField = [self.searchController.searchBar valueForKey:@"_searchField"];
    if (searchBarTextField)
    {
        [searchBarTextField setBackgroundColor:[UIColor whiteColor]];
        [searchBarTextField setBorderStyle:UITextBorderStyleRoundedRect];
        searchBarTextField.layer.cornerRadius = 5.0f;
        searchBarTextField.layer.borderColor = RGBA(204, 204, 204, 1).CGColor;
        searchBarTextField.layer.borderWidth = 0.5f;
    }
    self.tableView.tableHeaderView = self.searchController.searchBar;

2.代理实现搜索结果显示

// 搜索的代理
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
{
    NSLog(@"%@",searchController.searchBar.text);
    NSString *searchString = [self.searchController.searchBar text];
    if (self.filterFirends!= nil) {
        [self.filterFirends removeAllObjects];
    }
    if ([PinyinHelper isIncludeChineseInString:searchString])
    {
        for (FriendInfo *friend in self.allFriends)
        {
            if ([friend.userName containsString:searchString])
            {
                [self.filterFirends addObject:friend];
                
            }
        }
    }
    else
    {
        for (FriendInfo *friend in self.allFriends)
        {
            HanyuPinyinOutputFormat *outputFormat=[[HanyuPinyinOutputFormat alloc] init];
            outputFormat.vCharType = VCharTypeWithV;
            outputFormat.caseType = CaseTypeLowercase;
            outputFormat.toneType = ToneTypeWithoutTone;
            NSString *outputPinyin=[PinyinHelper toHanyuPinyinStringWithNSString:friend.userName withHanyuPinyinOutputFormat:outputFormat withNSString:@""];
            
            if ([[outputPinyin uppercaseString] containsString:[searchString uppercaseString]])
            {
                [self.filterFirends addObject:friend];
            }
        }
    }
    //过滤数据
    self.searchResult.filterData = self.filterFirends;
    //刷新表格
    [self.searchResult refreshData];
    
}

3.这里加载出来的数据源是要根据字母筛选的 A--数组  B--数组 ......Z--数组,这就是基本结构

// 处理英文首字母
- (void)handleFirstLetterArray
{
    // 拿到所有的key  字母
    NSMutableDictionary *letterDict = [[NSMutableDictionary alloc] init];
    for (FriendInfo *friend in self.allFriends) {
        HanyuPinyinOutputFormat *outputFormat=[[HanyuPinyinOutputFormat alloc] init];
        outputFormat.vCharType = VCharTypeWithV;
        outputFormat.caseType = CaseTypeLowercase;
        outputFormat.toneType = ToneTypeWithoutTone;
        NSString *outputPinyin=[PinyinHelper toHanyuPinyinStringWithNSString:friend.userName withHanyuPinyinOutputFormat:outputFormat withNSString:@""];
        NSLog(@"%@",outputPinyin);
        [letterDict setObject:friend forKey:[[outputPinyin substringToIndex:1] uppercaseString]];
    }
    // 字母数组
    self.letterArr = letterDict.allKeys;
    
    // 让key进行排序  A  -  Z
    self.letterArr = [[NSMutableArray alloc] initWithArray:[self.letterArr sortedArrayUsingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
        // 大于上升,小于下降
        return [obj1 characterAtIndex:0] > [obj2 characterAtIndex:0];
        
    }]];
    
    // 遍历所有的排序后的key  每个Key拿到对应的数组进行组装
    for (NSString *letter in self.letterArr)
    {
        NSMutableArray *nameArr = [[NSMutableArray alloc] init];
        for (FriendInfo *friend in self.allFriends) {
            HanyuPinyinOutputFormat *outputFormat=[[HanyuPinyinOutputFormat alloc] init];
            outputFormat.vCharType = VCharTypeWithV;
            outputFormat.caseType = CaseTypeUppercase;
            outputFormat.toneType = ToneTypeWithoutTone;
            NSString *outputPinyin=[PinyinHelper toHanyuPinyinStringWithNSString:friend.userName withHanyuPinyinOutputFormat:outputFormat withNSString:@""];
            if ([letter isEqualToString:[[outputPinyin substringToIndex:1] uppercaseString]])
            {
                [nameArr addObject:friend];
            }
        }
        // 根据key装大字典
        // A -- 一批通讯人
        // B -- 一批人
        // ...
        [self.nameDict setObject:nameArr forKey:letter];
    }
}


最后介绍下用短视频来做App首次启动的效果(类似keep uber 百灵鸟这些启动效果)
1.首先弄个MP4格式的本地小视频,用一个viewcontroller来做视频容器,用MPMoviePlayer来做视频

self.moviePlayer = [[MPMoviePlayerController alloc] initWithContentURL:self.videoURL];
    self.moviePlayer.view.frame = CGRectMake(0, 点击打开链接0, SCREEN_WIDTH, SCREEN_HEIGHT);
    self.moviePlayer.shouldAutoplay = YES;
    self.moviePlayer.controlStyle = MPMovieControlStyleNone;
    self.moviePlayer.repeatMode = MPMovieRepeatModeNone;
    self.moviePlayer.movieSourceType = MPMovieSourceTypeFile;
    
    [self.view addSubview:self.moviePlayer.view];
    [self configShimmerLabel];
    //监听播放完成
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(playFinsihed) name:MPMoviePlayerPlaybackDidFinishNotification object:nil];

2.这里视频底部有一串闪烁的文字,这里用的是facebook的一个动画效果 点击打开链

    就不展开介绍了,非常简单,需要看的朋友可以点链接


3.调用时机以及如何展示层级

- (void)jungleIfFirstLoading
{
    __weak typeof(self)weakSelf = self;
    NSInteger firstIN = [[[NSUserDefaults standardUserDefaults] valueForKey:@"FIRST_ENTER_IN"] integerValue];
    if (firstIN != 0) {
        return;
    }
    
    self.animationVC = [[AnimationVideoViewController alloc] init];
    self.animationVC.videoURL = [NSURL fileURLWithPath:[[NSBundle mainBundle]pathForResource:@"intro"ofType:@"mp4"]];
    self.animationVC.view.frame = CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
    self.animationVC.finishBlock = ^{
        [UIView animateWithDuration:1.0 animations:^{
            weakSelf.animationVC.view.alpha = 0;
        } completion:^(BOOL finished) {
            [weakSelf.animationVC.view removeFromSuperview];
            weakSelf.animationVC = nil;
        }];
        
    };
    [[[UIApplication sharedApplication] keyWindow] addSubview:self.animationVC.view];
    [[[UIApplication sharedApplication] keyWindow] bringSubviewToFront:self.animationVC.view];
    
    [[NSUserDefaults standardUserDefaults] setValue:@(1) forKey:@"FIRST_ENTER_IN"];
    [[NSUserDefaults standardUserDefaults] synchronize];
}


这里我们在主控制的ViewwillAppear里面调用这些代码,通过NSUserdefault来进行判断是否第一次进来,最后我们把

装载有视频的控制器View增加到Window上面并且带到最前面展示,当用户点击屏幕或播放完进入App的时候移除

2016-11-30 21:16:58.250 MKJWechat[3372:55057] dealloc-->AnimationVideoViewController


看到有打印就已经释放了,搞定!!!




详细Demo:点击打开链接


有需要改进的地方大家尽情留言,小弟下班时间抽空写的,没那么完善,各位还是自行跑Demo试试吧,我会不断完

善的,再多点时间就把即时通信也加一下进去,自己正好也学习下






作者:Deft_MKJing 发表于2016/11/30 22:42:01 原文链接
阅读:32 评论:0 查看评论

Java的反射机制

$
0
0
  • 反射机制的功能

Java反射机制主要提供了以下功能:
在运行时判断任意一个对象所属的类;
在运行时构造任意一个类的对象;
在运行时判断任意一个类所具有的成员变量和方法;
在运行时调用任意一个对象的方法;
生成动态代理。

  • 反射机制的利弊

其实好处就是,增加程序的灵活性,避免将程序写死到代码里;但是坏处也有,就是性能是一个问题,反射相当于一系列解释操作,通知jvm要做的事情,性能比直接的java代码要慢很多。且不安全,通过反射机制我们能拿到类的私有成员。

  • 详解

Reflection。这个字的意思是“反射、映象、倒影”,用在Java身上指的是我们可以于运行时加载、探知、使用编译期间完全未知的classes。换句话说,Java程序可以加载一个运行时才得知名称的class,获悉其完整构造(但不包括methods定义),并生成其对象实体、或对其fields设值、或唤起其methods。这种“看透class”的能力(the ability of the program to examine itself)被称为introspection(内省、内观、反省)。Reflection和introspection是常被并提的两个术语。

反射之中包含了一个“反”的概念,所以要想解释反射就必须先从“正”开始解释,一般而言,当用户使用一个类的时候,应该先知道这个类,而后通过这个类产生实例化对象,但是“反”指的是通过对象找到类。

packagecn.mldn.demo;
classPerson {

}
Public class TestDemo {
    Public static void main(String[] args) throwsException {
        Person per = newPerson() ; // 正着操作
        System.out.println(per.getClass().getName()); // 反着来
    }
}

以上的代码使用了一个getClass()方法,而后就可以得到对象所在的“包.类”名称,这就属于“反”了,但是在这个“反”的操作之中有一个getClass()就作为发起一切反射操作的开端。
Person的父类是Object类,而上面所使用getClass()方法就是Object类之中所定义的方法。
·取得Class对象:public final Class

packagecn.mldn.demo;classPerson {
publicclassTestDemo {publicstaticvoidmain(String[] args) throwsException {
    Person per = newPerson() ; // 正着操作
    Class<?> cls = per.getClass() ; // 取得Class对象
    System.out.println(cls.getName()); // 反着来
    }
}

方式二:使用“类.class”取得,在日后学习Hibernate开发的时候使用

packagecn.mldn.demo;
classPerson {}
publicclassTestDemo {
    publicstaticvoidmain(String[] args) throwsException {
        Class<?> cls = Person.class; // 取得Class对象
        System.out.println(cls.getName()); // 反着来
    }
}

方式三:使用Class类内部定义的一个static方法,主要使用
·取得Class类对象:public static Class

packagecn.mldn.demo;
classPerson {}
publicclassTestDemo {
    publicstaticvoidmain(String[] args) throwsException {
        Class<?> cls = Class.forName("cn.mldn.demo.Person") ; // 取得Class对象
        System.out.println(cls.getName()); // 反着来
    }
}

那么现在一个新的问题又来了,取得了Class类的对象有什么用处呢?对于对象的实例化操作之前一直依靠构造方法和关键字new完成,可是有了Class类对象之后,现在又提供了另外一种对象的实例化方法:
·通过反射实例化对象:public T newInstance() throws InstantiationException, IllegalAccessException;
范例:通过反射实例化对象

packagecn.mldn.demo;
classPerson {
    @Override
    publicString toString() {
        return"Person Class Instance .";
    }
}
publicclassTestDemo {
publicstaticvoidmain(String[] args) throwsException {
    Class<?> cls = Class.forName("cn.mldn.demo.Person") ; // 取得Class对象
        Object obj = cls.newInstance() ; // 实例化对象,和使用关键字new一样
        Person per = (Person) obj ; // 向下转型
        System.out.println(per);
    }
}

那么现在可以发现,对于对象的实例化操作,除了使用关键字new之外又多了一个反射机制操作,而且这个操作要比之前使用的new复杂一些,可是有什么用?
对于程序的开发模式之前一直强调:尽量减少耦合,而减少耦合的最好做法是使用接口,但是就算使用了接口也逃不出关键字new,所以实际上new是造成耦合的关键元凶。
范例:回顾一下之前所编写的工厂设计模式

packagecn.mldn.demo;
interfaceFruit {
    publicvoideat() ;
}

classApple implementsFruit {
    publicvoideat() {
        System.out.println("吃苹果。");
    };
}

classFactory {
    public static Fruit getInstance(String className) {
        if("apple".equals(className)){
            return new Apple() ;
        }
        returnnull;
    }
}
publicclassFactoryDemo {
    publicstaticvoidmain(String[] args) {
        Fruit f = Factory.getInstance("apple") ;
        f.eat() ;
    }
}

以上为之前所编写最简单的工厂设计模式,但是在这个工厂设计模式之中有一个最大的问题:如果现在接口的子类增加了,那么工厂类肯定需要修改,这是它所面临的最大问题,而这个最大问题造成的关键性的病因是new,那么如果说现在不使用关键字new了,变为了反射机制呢?
反射机制实例化对象的时候实际上只需要“包.类”就可以,于是根据此操作,修改工厂设计模式。

packagecn.mldn.demo;
interfaceFruit {
    publicvoideat() ;
}
classApple implementsFruit {
    publicvoideat() {
        System.out.println("吃苹果。");
    };
}
classOrange implementsFruit {
    publicvoideat() {
        System.out.println("吃橘子。");
    };
}
classFactory {
    publicstaticFruit getInstance(String className) {
        Fruit f = null;
        try{
            f = (Fruit) Class.forName(className).newInstance() ;
        } catch(Exception e) {
            e.printStackTrace();
        }
        returnf ;
    }
}
publicclassFactoryDemo {
    publicstaticvoidmain(String[] args) {
        Fruit f = Factory.getInstance("cn.mldn.demo.Orange") ;
        f.eat() ;
    }
}

发现,这个时候即使增加了接口的子类,工厂类照样可以完成对象的实例化操作,这个才是真正的工厂类,可以应对于所有的变化。如果单独从开发角度而言,与开发者关系不大,但是对于日后学习的一些框架技术这个就是它实现的命脉,在日后的程序开发上,如果发现操作的过程之中需要传递了一个完整的“包.类”名称的时候几乎都是反射机制作用。
3.12.2 、反射的深入应用
以上只是利用了Class类作为了反射实例化对象的基本应用,但是对于一个实例化对象而言,它需要调用类之中的构造方法、普通方法、属性,而这些操作都可以通过反射机制完成。
3.12.2 .1、调用构造
使用反射机制也可以取得类之中的构造方法,这个方法在Class类之中已经明确定义了:
以下两个方法
取得一个类的全部构造:
public Constructor

packagecn.mldn.demo;
importjava.lang.reflect.Constructor;
classPerson { // CTRL + K
    publicPerson() {}
    publicPerson(String name) {}
    publicPerson(String name,intage) {}
}

publicclassTestDemo {
    publicstaticvoidmain(String[] args) throwsException {
        Class<?> cls = Class.forName("cn.mldn.demo.Person") ; // 取得Class对象
        Constructor<?> cons [] = cls.getConstructors() ; // 取得全部构造
        for(intx = 0; x < cons.length; x++) {
            System.out.println(cons[x]);
        }
    }
}

验证:在之前强调的一个简单Java类必须存在一个无参构造方法
范例:观察没有无参构造的情况

packagecn.mldn.demo;
classPerson { // CTRL + K
    privateString name;
    privateintage;

    publicPerson(String name,intage) {
        this.name= name ;
        this.age= age ;
    }

    @Override
    publicString toString() {
        return"Person [name="+ name+ ", age="+ age+ "]";
    }
}

publicclassTestDemo {
publicstaticvoidmain(String[] args) throwsException {
        Class<?> cls = Class.forName("cn.mldn.demo.Person") ; // 取得Class对象
        Object obj = cls.newInstance(); // 实例化对象
        System.out.println(obj);
    }
}

此时程序运行的时候出现了错误提示“java.lang.InstantiationException”,因为以上的方式使用反射实例化对象时需要的是类之中要提供无参构造方法,但是现在既然没有了无参构造方法,那么就必须明确的找到一个构造方法,而后利用Constructor类之中的新方法实例化对象:
·实例化对象:public T newInstance(Object… initargs) throws InstantiationException, IllegalAccessException,IllegalArgumentException, InvocationTargetException

packagecn.mldn.demo;
importjava.lang.reflect.Constructor;

classPerson { // CTRL + K
    privateString name;
    privateintage;

    publicPerson(String name,intage) {
        this.name= name ;
        this.age= age ;
    }
    @Override
    publicString toString() {
        return"Person [name="+ name+ ", age="+ age+ "]";
    }
}

publicclassTestDemo {
    publicstaticvoidmain(String[] args) throwsException {
        Class<?> cls = Class.forName("cn.mldn.demo.Person") ; // 取得Class对象
        // 取得指定参数类型的构造方法
        Constructor<?> cons = cls.getConstructor(String.class,int.class) ;
        Object obj = cons.newInstance("张三", 20); // 为构造方法传递参数
        System.out.println(obj);
    }
}

很明显,调用无参构造方法实例化对象要比调用有参构造的更加简单、方便,所以在日后的所有开发之中,凡是有简单Java类出现的地方,都一定要提供无参构造。

3.12.2 .2、调用普通方法
当取得了一个类实例化对象之后,下面最需要调用的肯定是类之中的方法,所以可以继续使用Class类取得一个类中所定义的方法定义:
·取得全部方法:public Method[] getMethods() throws SecurityException;
·取得指定方法:public Method getMethod(String name, Class

packagecn.mldn.demo;
importjava.lang.reflect.Method;
classPerson {
    privateString name;

    publicvoidsetName(String name) {
        this.name= name;
    }
    publicString getName() {
        returnname;
    }
}
publicclassTestDemo {
    publicstaticvoidmain(String[] args) throwsException {
        Class<?> cls = Class.forName("cn.mldn.demo.Person") ; // 取得Class对象
        Method met [] = cls.getMethods() ; // 取得全部方法
        for(intx = 0; x < met.length; x++) {
            System.out.println(met[x]);
        }
    }
}

但是取得了Method类对象最大的作用不再于方法的列出(方法的列出都在开发工具上使用了),但是对于取得了Method类对象之后还有一个最大的功能,就是可以利用反射调用类中的方法:
·调用方法:public Object invoke(Object obj, Object… args) throws IllegalAccessException,IllegalArgumentException, InvocationTargetException
之前调用类中方法的时候使用的都是“对象.方法”,但是现在有了反射之后,可以直接利用Object类调用指定子类的操作方法。(同时解释一下,为什么setter、getter方法的命名要求如此严格)。
范例:利用反射调用Person类之中的setName()、getName()方法

packagecn.mldn.demo;
importjava.lang.reflect.Method;
classPerson {
    privateString name;

    publicvoidsetName(String name) {
        this.name= name;
    }
    publicString getName() {
        returnname;
    }
}

publicclassTestDemo {
    publicstaticvoidmain(String[] args) throwsException {
        Class<?> cls = Class.forName("cn.mldn.demo.Person") ; // 取得Class对象
        Object obj = cls.newInstance(); // 实例化对象,没有向Person转型
        String attribute = "name"; // 要调用类之中的属性
        Method setMet = cls.getMethod("set"+ initcap(attribute), String.class);// setName()
        Method getMet = cls.getMethod("get"+ initcap(attribute));// getName()
        setMet.invoke(obj, "张三") ; // 等价于:Person对象.setName("张三")
        System.out.println(getMet.invoke(obj));// 等价于:Person对象.getName()
    }

    publicstaticString initcap(String str) {
        returnstr.substring(0,1).toUpperCase().concat(str.substring(1)) ;
    }
}

在日后的所有框架技术开发之中,简单Java类都是如此应用的,所以必须按照标准进行。
3.12.2 .3、调用成员
类之中最后一个组成部分就是成员(Field,也可以称为属性),如果要通过反射取得类的成员可以使用方法如下:
·取得本类的全部成员:public Field[] getDeclaredFields() throws SecurityException;
·取得指定的成员:public Field getDeclaredField(String name) throws NoSuchFieldException, SecurityException;
这两个方法的返回值类型是java.lang.reflect.Field类的对象,下面首先观察如何取得一个类之中的全部属性。
范例:取得一个类之中的全部属性

packagecn.mldn.demo;
importjava.lang.reflect.Field;
classPerson {
    privateString name;
}
publicclassTestDemo {
    publicstaticvoidmain(String[] args) throwsException {
        Class<?> cls = Class.forName("cn.mldn.demo.Person") ; // 取得Class对象
        Field field [] = cls.getDeclaredFields() ; // 取得全部属性
        for(intx = 0; x < field.length; x++) {
            System.out.println(field[x]);
        }
    }
}

但是找到Field实际上就找到了一个很有意思的操作,在Field类之中提供了两个方法:
·设置属性内容(类似于:对象.属性= 内容):public void set(Object obj, Object value)
throws IllegalArgumentException, IllegalAccessException;
·取得属性内容(类似于:对象.属性):public Object get(Object obj)
throws IllegalArgumentException, IllegalAccessException
可是从类的开发要求而言,一直都强调类之中的属性必须封装,所以现在调用之前要想办法解除封装。
·解除封装:public void setAccessible(boolean flag) throws SecurityException;
范例:利用反射操作类中的属性

packagecn.mldn.demo;
importjava.lang.reflect.Field;
classPerson {
    privateString name;
}
publicclassTestDemo {
public static void main(String[] args) throwsException {
        Class<?> cls = Class.forName("cn.mldn.demo.Person"); // 取得Class对象
        Object obj = cls.newInstance(); // 对象实例化属性才会分配空间
        Field nameField = cls.getDeclaredField("name") ; // 找到name属性
        nameField.setAccessible(true) ; // 解除封装了
        nameField.set(obj, "张三") ; // Person对象.name = "张三"
        System.out.println(nameField.get(obj)); // Person对象.name
    }
}
作者:Jerey_Jobs 发表于2016/11/30 23:04:00 原文链接
阅读:7 评论:0 查看评论

百度地图:加强篇(交通图、卫星图的实现)

$
0
0

什么是地图图层
地图可以包含一个或多个图层,每个图层在每个级别都是由若干个图块组成的,它们覆盖了地图的整个表面。例如您所看到包括街道、兴趣点、学校、公园等内容的地图展现就是一个图层,另外交通流量的展现也是通过图层来实现的。
图层分类
1、底图
基本的地图图层,包括若干个缩放级别,显示基本的地图信息,包括道路、街道、学校、公园等内容。
2、实时交通信息图: MapView.setTraffic(true)
3、卫星图: MapView.setSatellite(true)
卫星地图是卫星拍摄的真实的地理面貌,所以卫星地图可用来检测地面的信息,你可以了解到地理位置,地形等。
布置一个MainActivity来装这些加强篇,即各个图形实现效果;
实现目的:MainActivity用一个ListView,文本方式采用自制的item样式,然后通过intent跳转到LayerDemo这个activity中,这个activity主要还是需要实现KEY的校验、底图的实现,另外增加的是交通图和卫星图,只要通过MapView.xxx调用即可。
实现效果:
当键盘点击1、2、3数字时,分别出现底图、交通图和卫星图。
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
MainActivity.java

package huaxa.it.map;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class MainActivity extends Activity
{

    private static String[]         objects = new String[]
                                            { "hello world", "图层", "几何图形元素",
            "展示文字", "多条目绘制", "矩形范围内搜索", "圆形区域", "全城搜索", "驾车路线", "步行路线", "公交换乘",
            "我的位置"                          };
    private static Class[]          clazzs  = new Class[]
                                            { HelloWorld.class,
        LayerDemo.class};

    private ListView                list;
    private ArrayAdapter<String>    adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        list = (ListView) findViewById(R.id.list);

        adapter = new ArrayAdapter<String>(getApplicationContext(),
                R.layout.item, objects);// Context context, int resource, int
                                        // textViewResourceId, List<T> objects)<--objects代表listView
        list.setAdapter(adapter);
        list.setOnItemClickListener(new OnItemClickListener()
        {

            @Override
            public void onItemClick(AdapterView<?> parent, View view,
                    int position, long id)
            {
                Intent intent = new Intent(getApplicationContext(),
                        clazzs[position]);
                startActivity(intent);
            }

        });

    }

}

LayerDemo.java

package huaxa.it.map;

import com.baidu.mapapi.BMapManager;
import com.baidu.mapapi.MKGeneralListener;
import com.baidu.mapapi.map.MKEvent;
import com.baidu.mapapi.map.MapController;
import com.baidu.mapapi.map.MapView;
import com.baidu.platform.comapi.basestruct.GeoPoint;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.widget.Toast;

/**
 * @项目名: BaiduMap
 * @包名: huaxa.it.map
 * @类名: LayerDemo
 * @创建者: 黄夏莲
 * @创建时间: 2016年11月29日 ,上午10:14:15
 * 
 * @描述:地图图层(底图、实时交通、卫星图)
 */
public class LayerDemo extends Activity
{
    private BMapManager     manager;
    private MapController   controller;
    private MapView         mapView;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        // KEY的校验
        initManager();
        setContentView(R.layout.common);
        init();
    }

    private void init()
    {
        mapView = (MapView) findViewById(R.id.MapView);
        controller = mapView.getController();
        controller.setZoom(12);// 设置地图的缩放级别。 这个值的取值范围是[3,19]。
        mapView.setBuiltInZoomControls(true);// 设置是否启用内置的缩放控件。
        // 如果启用,MapView将自动显示这些缩放控件。
    }

    /**
     * 初始化地图引擎
     */
    private void initManager()
    {
        manager = new BMapManager(getApplicationContext());

        /**
         * public boolean init(String strKey, MKGeneralListener listener) 参数:
         * strKey - 申请的授权验证码 listener -
         * 注册回调事件,该接口返回网络状态,授权验证等结果,用户需要实现该接口以处理相应事件
         */
        // KEY的校验
        manager.init(CanstantValue.KEY,null);

    }
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event)
    {
        //地图图层(底图、实时交通、卫星图)
        switch (keyCode)
        {
            case KeyEvent.KEYCODE_1:
                // 底图
                mapView.setSatellite(false);
                mapView.setTraffic(false);
                break;
            case KeyEvent.KEYCODE_2:
                // 实时交通
                mapView.setTraffic(true);
                mapView.setSatellite(false);
                break;
            case KeyEvent.KEYCODE_3:
                // 卫星图
                mapView.setSatellite(true);
                mapView.setTraffic(false);

                break;
            default:
                break;
        }
        return super.onKeyDown(keyCode, event);
    }

    @Override
    protected void onResume()
    {
        // TODO Auto-generated method stub
        super.onResume();
        mapView.onResume();
    }

    @Override
    protected void onPause()
    {
        // TODO Auto-generated method stub
        super.onPause();
        mapView.onPause();
    }

    @Override
    protected void onDestroy()
    {
        // TODO Auto-generated method stub
        super.onDestroy();
        mapView.destroy();
    }

}

item.xml

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/text"
    android:layout_width="match_parent"
    android:layout_height="50dip"
    android:textColor="@android:color/black"
    android:textSize="20sp" 
    android:gravity="center_vertical"
    />

activity_main.xml

<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=".MainActivity" >

    <ListView
        android:id="@+id/list"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />

</RelativeLayout>

Manifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="huaxa.it.map"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="16"
        android:targetSdkVersion="16" />

    <!-- gps -->
    <!-- 这个权限用于进行网络定位 -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" >
    </uses-permission>
    <!-- 这个权限用于访问GPS定位 -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" >
    </uses-permission>
    <!-- 用于访问wifi网络信息,wifi信息会用于进行网络定位 -->
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" >
    </uses-permission>
    <!-- 获取运营商信息,用于支持提供运营商信息相关的接口 -->
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" >
    </uses-permission>
    <!-- 这个权限用于获取wifi的获取权限,wifi信息会用来进行网络定位 -->
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" >
    </uses-permission>
    <!-- 用于读取手机当前的状态 -->
    <uses-permission android:name="android.permission.READ_PHONE_STATE" >
    </uses-permission>
    <!-- 写入扩展存储,向扩展卡写入数据,用于写入离线定位数据 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" >
    </uses-permission>
    <!-- 访问网络,网络定位需要上网 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <!-- SD卡读取权限,用户写入离线定位数据 -->
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" >
    </uses-permission>

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >


             <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>


        <activity
            android:name=".HelloWorld"
            android:label="@string/app_name" >
        </activity>
                <activity
            android:name=".LayerDemo"
            android:label="@string/app_name" >
        </activity>
    </application>

</manifest>
作者:u014299265 发表于2016/11/30 23:21:25 原文链接
阅读:19 评论:0 查看评论

【Android】Android6.0及以上不能读取外部存储问题

$
0
0

Android6.0级以上系统不能读取外部存储,有时候编写好的代码在低版本的模拟器上运行完全没有问题,但是一运行到api版本比较高的真机上就会出现问题。比如打开手机相册的时候,6.0以上的系统运行就会崩溃,为了解决这一问题,可以采用以下算法:

/**
 * 解决安卓6.0以上版本不能读取外部存储权限的问题
 * @param activity
 * @return
 */
public static boolean isGrantExternalRW(Activity activity) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && activity.checkSelfPermission(
            Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {

        activity.requestPermissions(new String[]{
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
        }, 1);

        return false;
    }

    return true;
}


作者:qq_32353771 发表于2016/11/30 23:37:26 原文链接
阅读:30 评论:0 查看评论

利用EGL在android上使用C/C++写OpenGL ES程序

$
0
0

很多教程都是在C/C++写的OpenGL的代码,其中有很多优秀的框架,除了前面提过的Assimp库外,还有很强大的库glm,从另外一个角度来看,在学习EGL的时候,很多的资料都是C语言的代码,我在android上写OpenGL ES的代码似乎从来没见过那些代码,不使用一下总觉得缺少点什么。

事实上,Android在native层构建OpenGL环境的步骤就如同前面博客OpenGL ES EGL介绍中讲过的这样,在Java层,与EGL相关的操作我们也看不到,其实都是由GLSurfaceView封装好了。

因此这篇博客就研究一下怎么使用Native代码写OpenGL ES代码和如何利用EGL自己创建OpenGL的环境。最终包含:

1.使用Native代码+GLSurfaceView写的六边形,这里会介绍到glm库。

2.Java代码自己写一个功能类似GLSurfaceView的类,主要是在java层使用EGL自己创建OpenGL的环境,思想上参考了GLSurfaceView的源码。

3.全部使用native代码写的六边形。Java层仅用了SurfaceView,余下的功能全部在native层实现。

先贴上效果图,这三幅图分别是上面描述的三个程序执行得到的结果。
第一幅图是第一篇写OpenGL入门的时候使用的Demo,我用本博客第二部分写的MySurfaceView替换了系统自带的GLSurfaceView执行的效果。
第一幅图和三幅图也是画一个六边形,只是没有旋转,大小并且都是用AndroidStudio创建的项目。

pic


使用Native代码+GLSurfaceView

这种方式其实就是将GLSurfaceView.Renderer的几个回调函数在native层实现,算是体验C/C++写OpenGL代码的第一步吧。Render类大概就是下面的样子

    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        String vetexShaderStr = LoadShaderStr(mContext, R.raw.vshader);
        String fragmentShaderStr = LoadShaderStr(mContext, R.raw.fshader);
      // native
        nativeInit(vetexShaderStr, fragmentShaderStr);
    }
    @Override
    public void onDrawFrame(GL10 gl) {
      // native
        nativeDraw(mAngleX, mAngleY);
    }
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
      // native
        nativeSurfaceChanged(width, height);
    }
    public static native void nativeInit(String vertexShaderCode, String fragmentShaderCode);
    private static native void nativeDraw(float angleX, float angleY);
    private static native void nativeSurfaceChanged(int width, int height);
}

在native层要做的事情其实和前面用java语言写的代码是类似的,无外乎就是写一个六边形的类,函数名也是一致的,因此写起来很简单。

使用Java代码的时候在android.opengl包中有个很方便的类Matrix,里面封装了矩阵的相关操作,设置投影、摄像机矩阵、矩阵相乘等操作都被封装好了,在C/C++中可没有这个类,不过也不是难事。Android Native Development Kit Cookbook一书的作者就封装了相关操作。

#ifndef MATRIX_H
#define MATRIX_H

#include <math.h>

#define MYPI 3.14159265358979323846

void translate_matrix(float tx, float ty, float tz, float *M);
void scale_matrix(float xs, float ys, float zs, float *M);
void rotate_matrix(float angle, float x, float y, float z, float *M);
void perspective_matrix(float fovy, float aspect, float znear, float zfar, float *M);
void multiply_matrix(float *A, float *B, float *M);

#endif



#include "matrix.h"

//all matrix are in column-major order
//the 4x4 matrix are stored in an array as shown below
//0-15 indicates the array index
//0 4   8   12
//1 5   9   13
//2 6   10  14
//3 7   11  15


void load_identity(float *M) {
    for (int i = 0; i < 16; ++i) {
        M[i] = 0.0f;
    }
    M[0] = 1.0f;
    M[5] = 1.0f;
    M[10] = 1.0f;
    M[15] = 1.0f;
}

//return translation matrix
//  1   0   0   tx
//  0   1   0   ty
//  0   0   1   tz
//  0   0   0   1
void translate_matrix(float tx, float ty, float tz, float *M) {
    load_identity(M);
    M[12] = tx;
    M[13] = ty;
    M[14] = tz;
}

//return scaling matrix
//  sx  0   0   0
//  0   sy  0   0
//  0   0   sz  0
//  0   0   0   1
void scale_matrix(float sx, float sy, float sz, float *M) {
    load_identity(M);
    M[0] *= sx;
    M[5] *= sy;
    M[10] *= sz;
}

//return rotation matrix
//R = Rx*Ry*Rz
//          1   0           0       0
//  Rx =    0   cosx        -sinx   0
//          0   sinx        cosx    0
//          0   0           0       1

//      cosy        0       siny        0
//      0           1       0           0
//  Ry =-siny       0       cosy        0
//      0           0       0           1
//
//      cosz        -sinz   0       0
//  Rz= sinz        cosz    0       0
//      0           0       1       0
//      0           0       0       1
//refer to http://lignumcad.sourceforge.net/doc/en/HTML/SOHTML/TechnicalReference.html
//for detailed info

void rotate_matrix(float angle, float x, float y, float z, float *M) {
    double radians, c, s, c1, u[3], length;
    int i, j;

    radians = (angle * MYPI) / 180.0;
    c = cos(radians);
    s = sin(radians);
    c1 = 1.0 - cos(radians);
    length = sqrt(x * x + y * y + z * z);
    u[0] = x / length;
    u[1] = y / length;
    u[2] = z / length;

    for (i = 0; i < 16; i++) {
        M[i] = 0.0;
    }
    M[15] = 1.0;

    for (i = 0; i < 3; i++) {
        M[i * 4 + (i + 1) % 3] = u[(i + 2) % 3] * s;
        M[i * 4 + (i + 2) % 3] = -u[(i + 1) % 3] * s;
    }
    for (i = 0; i < 3; i++) {
        for (j = 0; j < 3; j++) {
            M[i * 4 + j] += c1 * u[i] * u[j] + (i == j ? c : 0.0);
        }
    }
}


/* simulate gluPerspectiveMatrix
 * //return perspective projection matrix
 *  cot(fovy/2) / aspect        0               0                   0
 *  0                       cot(fovy/2)         0                   0
 *  0                           0       (znear+zfar)/(znear-zfar)   2*znear*zfa/(znear-zfar)
 *  0                           0               -1                  0
 */
void perspective_matrix(float fovy, float aspect, float znear, float zfar, float *M) {
    int i;
    double f;

    load_identity(M);

    f = 1.0/tan(fovy * 0.5);

    M[0] = f / aspect;
    M[5] = f;
    M[10] = (znear + zfar) / (znear - zfar);
    M[11] = -1.0;
    M[14] = (2.0 * znear * zfar) / (znear - zfar);
    M[15] = 0.0;
}

//Multiplies A by B and output to C, a tmp buffer is used to support in-place multiplication
void multiply_matrix(float *A, float *B, float *C) {
    int i, j, k;
    float tmpM[16];

    for (i = 0; i < 4; i++) {
        for (j = 0; j < 4; j++) {
            tmpM[j * 4 + i] = 0.0;
            for (k = 0; k < 4; k++) {
                tmpM[j * 4 + i] += A[k * 4 + i] * B[j * 4 + k];
            }
        }
    }
    for (i = 0; i < 16; i++) {
        C[i] = tmpM[i];
    }
}

不过我觉得这都是不必要的工作,很浪费时间,事实上有一个非常强大的数学库glm(OpenGL Mathematics

)主要还体现在它是为OpenGL开发人员量身定做的,和使用着色器语言一样,使用很方便。我在这个Demo中主要用了它的设置摄像机矩阵、投影矩阵。

#include "glm/mat4x4.hpp"
#include "glm/ext.hpp"
glm::mat4 projection;
glm::mat4 view;
//glm::mat4 module;
projection = glm::ortho(-1.0f, 1.0f, -(float) height / width, (float) height / width, 5.0f,
                            7.0f);
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 6.0f),
                       glm::vec3(0.0f, 0.0f, 0.0f),
                       glm::vec3(0.0f, 1.0f, 0.0f));
// 矩阵相乘太方便
glm::mat4 mvpMatrix = projection * view/* * module*/;
// 装换成数组形式
float *mvp = (float *) glm::value_ptr(mvpMatrix);
mShape.draw(mvp);

代码在这里:https://github.com/qhyuan1992/OpenGL-ES/tree/master/NativeGLESView

在Java层使用EGL

其实在利用OpenGL ES进行Android手游录屏研究这篇博客中已经接触到了Java层的EGL使用了,GLSurfaceView继承自SurfaceView,我们用它来写OpenGL代码很方便,就是因为它已经封装好了创建EGL环境的部分了,如果看GLSurfaceView的代码必然可以看到前面讲EGL部分的时候创建EGL环境的相关函数调用。

不过由于GLSurfaceView代码比较多,下面是我自己写的一个类似GLSurfaceView的类,写的很简略,用其来替代系统的GLSurfaceView即可。显然这样做用来了解Java层EGL的使用是足够的。

里面有部分代码我是参考GLSurfaceView的源码的。

package com.example.opengles_circle;
import java.lang.ref.WeakReference;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.egl.EGLSurface;

import android.content.Context;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class MyGLSurfaceView extends SurfaceView implements SurfaceHolder.Callback{
    public SurfaceHolder mHolder;
    public Renderer mRenderer;
    private final WeakReference<MyGLSurfaceView> mThisWeakRef = new WeakReference<MyGLSurfaceView>(this);
    Object mLock = new Object();
    private GLThread mThread;
    public MyGLSurfaceView(Context context) {
        super(context);
        mHolder = getHolder();
        mHolder.addCallback(this);
    }

    public void setRenderer(Renderer renderer) {
        mRenderer = renderer;
        mThread = new GLThread(mThisWeakRef);
        mThread.start();
    }

    public void surfaceCreated(SurfaceHolder holder) {
        mThread.surfaceCreated();
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        mThread.surfaceDestroyed();
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        mThread.surfaceChanged(w, h);
    }

    public void stopRender() {
        // 设置停止标示,notify
    }

    public interface Renderer {
        void onSurfaceCreated();

        void onSurfaceChanged(int width, int height);

        void onDrawFrame();
    }

}

class GLThread extends Thread {
    private Object lock = new Object();
    private EGLHelper mEglHelper;
    private boolean mHasSurface = false;
    public int mWidth;
    public int mHeight;
    private WeakReference<MyGLSurfaceView> mGLSurfaceViewWeakRef;

     GLThread(WeakReference<MyGLSurfaceView> glSurfaceViewWeakRef) {
         super();
         mWidth = 0;
         mHeight = 0;
         mGLSurfaceViewWeakRef = glSurfaceViewWeakRef;
     }

    @Override
    public void run() {
        try {
            doThread();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void doThread() throws InterruptedException {
        mEglHelper = new EGLHelper(mGLSurfaceViewWeakRef);
        MyGLSurfaceView view = mGLSurfaceViewWeakRef.get();
        // 确保有了Surface
        synchronized (lock) {
            if (!mHasSurface) {
                lock.wait();
            }
        }
        mEglHelper.start();
        /*----实际上是在一个循环里面针对不同的事件(onSurfaceCreated/onSurfaceChanged/onDrawFrame/terminal)执行不同的回调----*/
        /*-----所有情况都执行完后,wait,当某个事件发生后标志置位,然后调用notify----*/
        view.mRenderer.onSurfaceCreated();
        // 也要确保在surfaceChanged之后执行
        view.mRenderer.onSurfaceChanged(mWidth, mHeight);
        view.mRenderer.onDrawFrame();
        mEglHelper.swap();
    }

    public void surfaceCreated() {
    }

    public void surfaceChanged(int w, int h) {
        mWidth = w;
        mHeight = h;
        synchronized (lock) {
            mHasSurface = true;
            lock.notifyAll();
        }
    }

    public void surfaceDestroyed() {
        synchronized (lock) {
            mHasSurface = false;
            lock.notifyAll();           
        }
    }
}

class EGLHelper{
    public WeakReference<MyGLSurfaceView> mGLSurfaceViewWeakRef;
    private EGL10 mEgl;
    private EGLDisplay mEglDisplay;
    private EGLConfig mEglConfig;
    private EGLContext mEglContext;
    private EGLSurface mEglSurface;

    public EGLHelper(WeakReference<MyGLSurfaceView> glSurfaceViewWeakRef) {
        mGLSurfaceViewWeakRef = glSurfaceViewWeakRef;
    }

  // start函数中就是创建EGL环境的步骤
    public void start(){
        int[] num_config = new int[1];
        EGLConfig[] configs = new EGLConfig[1];
        int[] configSpec = { EGL10.EGL_RED_SIZE, 8, 
                EGL10.EGL_GREEN_SIZE, 8,
                EGL10.EGL_BLUE_SIZE, 8,
                EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT, EGL10.EGL_NONE };

        this.mEgl = (EGL10) EGLContext.getEGL();

        mEglDisplay = this.mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
        this.mEgl.eglInitialize(mEglDisplay, null);

        this.mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1, num_config);

        mEglConfig = configs[0];
        mEglSurface = this.mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig,
                mGLSurfaceViewWeakRef.get().mHolder, null);

        int[] attrs = {0x3098, 2, EGL10.EGL_NONE}; // EGL_CONTEXT_CLIENT_VERSION
        mEglContext = this.mEgl.eglCreateContext(mEglDisplay, mEglConfig,
                EGL10.EGL_NO_CONTEXT, attrs);

        this.mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext);
    }

    public void swap() {
        mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
    }

    public void destroySurface() {
        if (mEglSurface != null) {
            mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
            mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
            mEglSurface = null;
        }
        if (mEglContext != null) {
            mEgl.eglDestroyContext(mEglDisplay, mEglContext);
            mEglContext = null;
        }
        if (mEglDisplay != null) {
            mEgl.eglTerminate(mEglDisplay);
            mEglDisplay = null;
        }
    }
}

可以看到当调用setRender时就开启了一个子线程GLThread,不过这个线程没有立即执行而是wait直到surface创建成功。

EGLHelper就是一个工具类,里面的start函数就包含了创建EGL环境的步骤,等待EGL上下文创建好了,在执行 view.mRenderer.onDrawFrame();回调中其实就是我们要实现的部分,在里面进行OpenGL绘图,然后在执行EGLHelper的swap函数,将后台缓冲中的内容显示到native_window也就是屏幕上去。

一般来说在这个GLThread线程中是一个无限循环,根据收到的不同状态值,执行对应的操作,每执行一轮就会调用wait直到其他的状态被设置后notify唤醒,在循环执行。

在这里注意一下eglCreateWindowSurface函数中的native_window参数。它是一个Object对象,它的实现在com.google.android.gles_jni.EGLImpl.java(EGLImpl.java),先记住native_window可以传入的参数是SurfaceView、SurfaceHolder或者Surface,最终都转换成了Surface,Surface在Java层代表这一个窗口。

EGLSurface  eglCreateWindowSurface(EGLDisplay display, EGLConfig config, Object native_window, int[] attrib_list) {
  Surface sur = null;
        if (native_window instanceof SurfaceView) {
            SurfaceView surfaceView = (SurfaceView)native_window;
            sur = surfaceView.getHolder().getSurface();
        } else if (native_window instanceof SurfaceHolder) {
            SurfaceHolder holder = (SurfaceHolder)native_window;
            sur = holder.getSurface();
        } else if (native_window instanceof Surface) {

          // .......
}

在native层使用EGL

在native层使用EGL创建EGL环境并且使用C/C++写OpenGL代码,思路就是结合前面两个Demo,在native层创建EGL环境和在Java层是类似的思路,只是有些API可能不一样,并且还用到了很多NDK的知识,

代码在这里https://github.com/qhyuan1992/OpenGL-ES/tree/master/NativeGLESViewWithEGL

核心部分的代码


void Renderer::requestInitEGL(ANativeWindow * pWindow) {
    LOGI(1, "-------requestInitEGL");
    pthread_mutex_lock(&mMutex);
    mWindow = pWindow;
  // mEnumRenderEvent表示事件
    mEnumRenderEvent = RE_SURFACE_CHANGED;
    LOGI(1, "-------mEnumRenderEvent=%d", mEnumRenderEvent);
    pthread_mutex_unlock(&mMutex);
  // 唤醒处于wait状态的线程,继续函数onRenderThreadRun的执行,onRenderThreadRun是线程的入口函数
    pthread_cond_signal(&mCondVar);
}

// 每次需要绘制的时候调用,实际上会调用到
void Renderer::requestRenderFrame() {
    pthread_mutex_lock(&mMutex);
    mEnumRenderEvent = RE_DRAW_FRAME;
    pthread_mutex_unlock(&mMutex);
    pthread_cond_signal(&mCondVar);
}

void Renderer::requestDestroy() {
    pthread_mutex_lock(&mMutex);
    mEnumRenderEvent = RE_EXIT;
    pthread_mutex_unlock(&mMutex);
    pthread_cond_signal(&mCondVar);
}

void Renderer::onRenderThreadRun() {
    mISRenderering = true;
    while(mISRenderering) {
        pthread_mutex_lock(&mMutex);
        // 每完成一个事件就wait在这里直到有其他事件唤醒
        pthread_cond_wait(&mCondVar, &mMutex);

        LOGI(1, "-------this mEnumRenderEvent is %d", mEnumRenderEvent);
        switch (mEnumRenderEvent) {
            case RE_SURFACE_CHANGED:
                LOGI(1, "-------case RE_SURFACE_CHANGED");
                mEnumRenderEvent = RE_NONE;
                pthread_mutex_unlock(&mMutex);
                initEGL();
                nativeSurfaceCreated();
                nativeSurfaceChanged(mWidth, mHeight);
                break;
            case RE_DRAW_FRAME:
                mEnumRenderEvent = RE_NONE;
                pthread_mutex_unlock(&mMutex);
                // draw
                // 这个函数留给真实的GLES绘制操作。
                nativeDraw();
                eglSwapBuffers(mDisplay, mSurface);
                break;
            case RE_EXIT:
                mEnumRenderEvent = RE_NONE;
                pthread_mutex_unlock(&mMutex);
                terminateDisplay();
                mISRenderering = false;
                break;
            default:
                mEnumRenderEvent = RE_NONE;
                pthread_mutex_unlock(&mMutex);
        }
    }
}

下面我也把native层创建EGL环境的部分列出来,显然和Java层是类似的。

void Renderer::initEGL() {
    const EGLint attribs[] = {
            EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
            EGL_BLUE_SIZE, 8,
            EGL_GREEN_SIZE, 8,
            EGL_RED_SIZE, 8,
            EGL_NONE
    };
    EGLint width, height, format;
    EGLint numConfigs;
    EGLConfig config;
    EGLSurface surface;
    EGLContext context;

    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);

    eglInitialize(display, 0, 0);

    eglChooseConfig(display, attribs, &config, 1, &numConfigs);

    surface = eglCreateWindowSurface(display, config, mWindow, NULL);
    EGLint attrs[]= {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
    context = eglCreateContext(display, config, NULL, attrs);

    if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) {
        LOGI(1, "------EGL-FALSE");
        return ;
    }

    eglQuerySurface(display, surface, EGL_WIDTH, &width);
    eglQuerySurface(display, surface, EGL_HEIGHT, &height);

    mDisplay = display;
    mSurface = surface;
    mContext = context;
    mWidth = width;
    mHeight = height;
    LOGI(1, "width:%d, height:%d", mWidth, mHeight);

}

在Java层继承自一个SurfaceView

public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
    public MySurfaceView(Context context) {
        super(context);
        getHolder().addCallback(this);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        nativeRequestRender();
        return super.onTouchEvent(event);
    }
    @Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {
        setRender();
    }
    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
        nativeSurfaceChanged(surfaceHolder.getSurface());
    }
    @Override
    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
        nativeSurfaceDestroyed();
    }
    private void setRender() {
        nativeStartRender();
    }
    private static native void nativeSurfaceChanged(Surface surface);
    private static native void nativeSurfaceDestroyed();
    private static native void nativeStartRender();
    private static native void nativeRequestRender();
    static {
        System.loadLibrary("NativeWithEGL");
    }
}

转而在native层执行SurfaceHolder.Callback的回调,注意和前面的GLSurfaceView.Renderer的回调。

接下来在合适的时刻调用Renderer的request***函数,发出某种消息,比如执行requestRenderFrame函数,也就是发出了要绘制一帧的消息,那么在绘制线程中,就去执行OpenGL ES绘制操作代码。

ANativeWindow * mWindow;
Renderer * mRenderer;
extern "C" {
JNIEXPORT void JNICALL
Java_com_example_weiersyuan_nativeglesviewwithegl_MySurfaceView_nativeStartRender(JNIEnv *env, jclass type) {
    mRenderer = new Renderer();
    mRenderer->start();
}
JNIEXPORT void JNICALL
Java_com_example_weiersyuan_nativeglesviewwithegl_MySurfaceView_nativeSurfaceChanged(JNIEnv *env, jclass type, jobject surface) {
    mWindow = ANativeWindow_fromSurface(env, surface);
    // surfacechange时 发送SurfaceChanged消息,此时创建egl环境的消息
    mRenderer->requestInitEGL(mWindow);
}

JNIEXPORT void JNICALL
Java_com_example_weiersyuan_nativeglesviewwithegl_MySurfaceView_nativeSurfaceDestroyed(JNIEnv *env, jclass type) {
    mRenderer->requestDestroy();
    ANativeWindow_release(mWindow);
    delete mRenderer;
}
JNIEXPORT void JNICALL
Java_com_example_weiersyuan_nativeglesviewwithegl_MySurfaceView_nativeRequestRender(JNIEnv *env, jclass type){
    mRenderer->requestRenderFrame();
}
}

在Native层ANativeWindow对象代表一个窗口,看起来好像和Java层的Surface是对应着的,native层的ANativeWindow的指针是Java层的Surface对象的一个成员变量,这也是持久保存native层数据的常用方式,在NDK中有很多类似于ANativeWindow_fromSurface的函数,将Java层的独享转换为native层的对象,都是类似的方式。

Java代码自己写一个功能类似GLSurfaceView的类

作者:cauchyweierstrass 发表于2016/11/30 23:37:30 原文链接
阅读:95 评论:0 查看评论

图片缓存框架Glide的总结

$
0
0

转载请注明出处:http://blog.csdn.net/li0978/article/details/53415118

前言

前段时间写过一篇图片缓存框架Picasso的用法,对于Picasso有些同学也比较熟悉,采用Lru最近最少缓存策略,并且自带内存和硬盘缓存机制,在图片加载尤其是多图加载着实为大伙省了不少力,在此之前同样也相识有Afinal、Xutil、UniversalImageLoader等优秀的开源框架,今天再总结一个图片加载缓存框架 — Glide,以助自己后边的项目构建舔砖加瓦吧。

Glide简介

Glide是一个快速高效的开源媒体和图片加载框架,他把媒体解码、内存和磁盘二级缓存还有一些资源缓存池封装成一个个简单的接口,使用很方便,并且Glide也是google推荐使用的图片加载框架。Glide支持下载、解码、展示视频快照和图片资源以及GIF动画,Glide支持插件扩展并使用于任何网络网络引擎,默认情况下采用的是HttpUrlconnection网络加载形式,当然也可以采用Google的volley框架和Square的OkHttp来取代。
Glide官方说明:https://github.com/bumptech/glide

Glide特点

  • 使用简单
  • 可配置度高,自适应程度高
  • 支持常见图片格式 Jpg png gif webp
  • 支持多种数据源 网络、本地、资源、Assets 等
  • 高效缓存策略 支持Memory和Disk图片缓存 默认Bitmap格式采用RGB_565内存使用至少减少一半
  • 生命周期集成 根据Activity/Fragment生命周期自动管理请求
  • 高效处理Bitmap 使用Bitmap Pool使Bitmap复用,主动调用recycle回收需要回收的Bitmap,减小系统回收压力

Glide和Picasso对比

Glide和Picasso在使用上非常相似,之前也总结过Picasso,发现在某些地方甚至可以完全模仿Picasso写Glide,不过二者在核心上还是有一定区别的:

  1. Picasso接收的上下文是Context,而Glide传入的上下文可以有Context、Activity、Fragment。Activity和Fragment有生命周期,因此在某个生命周期阶段图片加载也响应收到控制,更灵活。另外在某些情况下也避免了对象未进行引用而造成的内存泄漏问题。
  2. Glide默认的图片格式RGB565而Picasso支持的图片格式ARGB8888,尽管前者没有后者图像更清晰(相差不大),但是在内存开销上却比前者少了一半,加载更快
  3. Glide默认对图片缓存仅仅是展示控件的大小,如果在另外一个不同大小控件上加载相同的图片需要再次下载。Picasso缓存的图片默认是原图,可对原图进行随处展示。
  4. Glide支持媒体解码,支持GIF动画加载,Picasso不能。
    Glide和Picasso区别不止以上这些,以上只是些典型的区别。对于Glide的使用,下边一一道来。

Glide的基础用法

1.glide项目引用
对于Glide这么强大的开源框架,又是google推荐的早已加入到jcenter()仓库中了,所以我们使用的时候只需要在gradle中引用一下仓库的包即可:

compile 'com.github.bumptech.glide:glide:3.7.0'

2.绑定生命周期,让Glide加载图片过程根据生命周期管理。
上边也提到Glide可以根据多种形式绑定上下文,尤其是针对Activity的引用,可用于在生命周期内对图片加载进行控制。

 Glide.with(Context context);// 绑定Context
 Glide.with(Activity activity);// 绑定Activity
 Glide.with(FragmentActivity activity);// 绑定FragmentActivity
 Glide.with(Fragment fragment);// 绑定Fragment

3.简单加载(这里也可以加载本地和asset,可参考Picasso的简单用法)。

Glide.with(this).load(imageUrl).into(imageView);

4.设置加载前和加载失败时的图片

Glide.with(this).load(imageUrl).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).into(imageView);

5.设置下载优先级

Glide.with(this).load(imageUrl).priority(Priority.NORMAL).into(imageView);

6.设置内存缓存(是否进行内存缓存)

Glide.with(this).load(imageUrl).skipMemoryCache(true).into(imageView);

7.设置磁盘缓存

Glide.with(this).load(imageUrl).diskCacheStrategy(DiskCacheStrategy.ALL).into(imageView);

磁盘缓存策略说明:

  • ALL:缓存源资源和转换后的资源
  • NONE:不作任何磁盘缓存
  • SOURCE:缓存源资源
  • RESULT:缓存转换后的资源

8.设置加载动画
api中默认也存在动画,这里可以自己设置,并且也支持属性动画

Glide.with(this).load(imageUrl).animate(R.anim.item_alpha_in).into(imageView);

设置淡入动画

Glide.with(this).load(imageUrl)
.crossFade(1000)        //设置淡入动画,并且淡入过度时间为1秒
.override(80,80)         //最终呈现的像素值80*80
.into(imageView3);

9.设置缩略图

Glide.with(this).load(imageUrl).thumbnail(0.1f).into(imageView); //显示的图片大小为原图的1/10

10.设置加载尺寸(像素)

Glide.with(this).load(imageUrl).override(800, 800).into(imageView);

11.设置图片适配和转换
Api提供了centerCrop()、fitCenter()两种适配方式,前者效果是将图片按照最小边充满,最大边裁剪适配,后者效果是将图片按照最大边充满最小边居中空缺适配。

Glide.with(this).load(imageUrl).centerCrop().into(imageView);

也可以自定义Transformation来设置自己的形状,如设置圆角图形,圆角半径单位是dp:

public class GlideRoundTransform extends BitmapTransformation {
        private float radius = 0f;
        public GlideRoundTransform(Context context) {
            this(context, 4);
        }

        public GlideRoundTransform(Context context, int dp) {
            super(context);
            this.radius = Resources.getSystem().getDisplayMetrics().density * dp;
        }

        @Override
        protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
            return roundCrop(pool, toTransform);
        }

        private Bitmap roundCrop(BitmapPool pool, Bitmap source) {
            if (source == null) return null;

            Bitmap result = pool.get(source.getWidth(), source.getHeight(), Bitmap.Config.ARGB_8888);
            if (result == null) {
                result = Bitmap.createBitmap(source.getWidth(), source.getHeight(), Bitmap.Config.ARGB_8888);
            }
            Canvas canvas = new Canvas(result);
            Paint paint = new Paint();
            paint.setShader(new BitmapShader(source, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
            paint.setAntiAlias(true);
            RectF rectF = new RectF(0f, 0f, source.getWidth(), source.getHeight());
            canvas.drawRoundRect(rectF, radius, radius, paint);
            return result;
        }

        @Override
        public String getId() {
            return getClass().getName() + Math.round(radius);
        }
    }

设置圆角图片:

Glide.with(this).load(imageUrl).transform(new
GlideRoundTransform(this,100)).into(imageView);

也可以自定义圆形图片:

public class GlideCircleTransform extends BitmapTransformation {

    public GlideCircleTransform(Context context) {
        super(context);
    }

    @Override
    protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
        return circleCrop(pool, toTransform);
    }

    private static Bitmap circleCrop(BitmapPool pool, Bitmap source) {
        if (source == null) return null;
        //获取最小边长
        int size = Math.min(source.getWidth(), source.getHeight());
        //获取圆形图片的宽度和高度
        int x = (source.getWidth() - size) / 2;
        int y = (source.getHeight() - size) / 2;

        // TODO this could be acquired from the pool too
        Bitmap squared = Bitmap.createBitmap(source, x, y, size, size);

        Bitmap result = pool.get(size, size, Bitmap.Config.ARGB_8888);
        if (result == null) {
            result = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
        }

        Canvas canvas = new Canvas(result);
        Paint paint = new Paint();
        paint.setShader(new BitmapShader(squared, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
        paint.setAntiAlias(true);
        float r = size / 2f;   //得到圆形半径
        canvas.drawCircle(r, r, r, paint);
        return result;
    }

    @Override
    public String getId() {
        return getClass().getName();
    }
}

设置圆形图片:

Glide.with(this).load(imageUrl).transform(new GlideCircleTransform(this)).into(imageView);

12.设置要下载的内容
有些时候我们不想直接将加载的图片显示到控件上,或者我们想下载这张图片,又或者我们暂时不想在此处展示这张图片,可以这样处理:

Glide.with(this).load(imageUrl).centerCrop().into(new SimpleTarget<GlideDrawable>() {
            @Override
            public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> glideAnimation) {
                //这里可根据resource自行处理(下载...)
            }
        });

13.设置监听请求接口

Glide.with(this).load(imageUrl).listener(new RequestListener<String, GlideDrawable>() {
            @Override
            public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) {
                return false;
            }

            @Override
            public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
                //imageView.setImageDrawable(resource);
                return false;
            }
        }).into(imageView);

14.设置GIF加载方式

Glide.with(this).load(imageUrl).asBitmap().into(imageView);//显示gif静态图片
Glide.with(this).load(imageUrl).asGif().into(imageView);//显示gif动态图片

15.缓存动态清理

Glide.get(this).clearDiskCache(); //清理磁盘缓存
Glide.get(this).clearMemory(); //清理内存缓存

Glide的高级用法

Glide内部有一个GlideModule,是用来全局配置Glide的,可进行设置缓存路径,缓存空间,图片格式,自定义cache指示等操作。

1.GlideModule添加
自定义一个GlideModule :

public class MyGlideModule implements GlideModule {
    @Override public void applyOptions(Context context, GlideBuilder builder) {
        // Apply options to the builder here.
    }

    @Override public void registerComponents(Context context, Glide glide) {
        // register ModelLoaders here.
    }
}

AndroidManifest.xml注册:

<manifest ...>
    <!-- ... permissions -->
    <application ...>
        <meta-data
            android:name="com.mypackage.MyGlideModule"
            android:value="GlideModule" />
        <!-- ... activities and other components -->
    </application>
</manifest>

混淆处理:

-keepnames class com.mypackage.MyGlideModule
# or more generally:
#-keep public class * implements com.bumptech.glide.module.GlideModule

多个GlideModule冲突问题
一般情况下我们一个项目可以有多个library项目,这样就可能有多个GlideModule的存在,但是多个GlideModule存在却会出现冲突,为了避免这种情况发生,一般我们尽量只设置一个GlideModule,当然也可在配置清单中忽略某个GlideMoudle:

<meta-data android:name=”com.mypackage.MyGlideModule” tools:node=”remove” />

2.GlideModule相关配置
设置Glide内存缓存大小:

int maxMemory = (int) Runtime.getRuntime().maxMemory();//获取系统分配给应用的总内存大小
int memoryCacheSize = maxMemory / 8;//设置图片内存缓存占用八分之一
//设置内存缓存大小
builder.setMemoryCache(new LruResourceCache(memoryCacheSize));

有些时候我们也需要获取一下默认的内存缓存大小:

MemorySizeCalculator calculator = new MemorySizeCalculator(context);  
int defaultMemoryCacheSize = calculator.getMemoryCacheSize();  
int defaultBitmapPoolSize = calculator.getBitmapPoolSize(); 

设置Glide磁盘缓存大小和磁盘缓存存放位置:

File cacheDir = context.getExternalCacheDir();//指定的是数据的缓存地址
int diskCacheSize = 1024 * 1024 * 30;//最多可以缓存多少字节的数据
//设置磁盘缓存大小和位置
builder.setDiskCache(new DiskLruCacheFactory(cacheDir.getPath(), "glide", diskCacheSize));

也可以:

//存放在data/data/xxxx/cache/
builder.setDiskCache(new InternalCacheDiskCacheFactory(context, "glide", diskCacheSize));
//存放在外置文件浏览器
builder.setDiskCache(new ExternalCacheDiskCacheFactory(context, "glide", diskCacheSize));

设置图片解码格式:
Glide默认的图片解码格式是RGB_565相比RGB_8888占内存更小,但是却损失了一部分图片质量,需求根据自己定。

//设置图片解码格式
builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);
//设置BitmapPool内存缓存大小
builder.setBitmapPool(new LruBitmapPool(memoryCacheSize));

3.使用ModelLoader自定义数据源:
有些时候我们需要根据不同的情况加载不同格式的图片,可采用工厂模式来进行选取。

定义处理URL接口

public interface IDataModel {
    String buildDataModelUrl(int width, int height);
}

实现处理URL接口
JpgDataModel:

public class JpgDataModel implements IDataModel {
    private String dataModelUrl;

    public JpgDataModel(String dataModelUrl) {
        this.dataModelUrl = dataModelUrl;
    }

    @Override
    public String buildDataModelUrl(int width, int height) {
        //http://78re52.com1.z0.glb.clouddn.com/resource/gogopher.jpg?imageView2/1/w/200/h/200/format/jpg
        return String.format("%s?imageView2/1/w/%d/h/%d/format/jpg", dataModelUrl, width, height);
    }
}

WebpDataModel:

public class WebpDataModel implements IDataModel {
    private String dataModelUrl;

    public WebpDataModel(String dataModelUrl) {
        this.dataModelUrl = dataModelUrl;
    }

    @Override
    public String buildDataModelUrl(int width, int height) {
        //http://78re52.com1.z0.glb.clouddn.com/resource/gogopher.jpg?imageView2/1/w/200/h/200/format/webp
        return String.format("%s?imageView2/1/w/%d/h/%d/format/webp", dataModelUrl, width, height);
    }
}

设置图片加工工厂

public class MyDataLoader extends BaseGlideUrlLoader<IDataModel> {
    public MyDataLoader(Context context) {
        super(context);
    }

    public MyDataLoader(ModelLoader<GlideUrl, InputStream> urlLoader) {
        super(urlLoader, null);
    }

    @Override
    protected String getUrl(IDataModel model, int width, int height) {
        return model.buildDataModelUrl(width, height);
    }

    /**
     */
    public static class Factory implements ModelLoaderFactory<IDataModel, InputStream> {

        @Override
        public ModelLoader<IDataModel, InputStream> build(Context context, GenericLoaderFactory factories) {
            return new MyDataLoader(factories.buildModelLoader(GlideUrl.class, InputStream.class));
        }

        @Override
        public void teardown() {
        }
    }
}

根据不同的要求采用不同的策略加载图片

//加载jpg图片
Glide.with(this).using(new MyDataLoader(this)).load(new JpgDataModel(imageUrl)).into(imageView);
//加载webp图片
Glide.with(this).using(new MyDataLoader(this)).load(new WebpDataModel(imageUrl)).into(imageView);

这样每次加载都要.using(),我们也可以不用.using(),方法就是将MyDataLoader的工厂注册到GlideModel中:

public class MyGlideModule implements GlideModule {
    ...
    @Override
    public void registerComponents(Context context, Glide glide) {
        glide.register(IDataModel.class, InputStream.class, 
            new MyUrlLoader.Factory());
    }
}

调用:

 //加载jpg图片
 Glide.with(this).load(new JpgDataModel(imageUrl)).into(imageView);
 //加载webp图片
 Glide.with(this).load(new WebpDataModel(imageUrl)).into(imageView);

以上是个人对Glide的相关总结,Glide功能不止这些,甚或是还可以自定义图片缓存TAG来实现对图片软删除等操作,这些功能在开发中也是微乎其微了,当然Glide开发团队也是想的周到,以实现程序的健壮性。Glide和Picasso有很多相似之处,了解Picasso的同学可以根据Picasso的相关Api很容易上手Glide,返过来也如此,总之后续继续深入吧。

作者:li0978 发表于2016/11/30 23:40:48 原文链接
阅读:36 评论:0 查看评论

AndroidStudio第一次提交项目到github

$
0
0

虽然使用AndroidStudio(以下简称As)开发并使用git管理代码已经有很长时间,但是第一次提交项目到git依然会很不顺利,网上的文章或许因为所使用版本比较老,并不一定完全凑效,因此写此笔记做下整理。

首先准备工作git客户端和As客户端是不可少的工具,本次使用的是As2.2.2和git2.10.2;

git:https://git-scm.com/downloads

这里我分享两种入门级操作方式来,因为本人git命令也不是很熟,跟多的偏向于傻瓜式的界面交互操作方式。

方法一:先创建项目,后与git连接

1、首先我们创建一个测试项目GitTest,点击VCS--Enable Version Control Integration,在弹出框右边选上Git。这个时候会发现项目会发生几点变化,鼠标右键点击项目出现Git选项;项目文件颜色变为红色;在As右下角出现Git:master;如下图:


图1-1



图1-2

图1-3

2、在github上创建远程代码仓库GitTest,注意这里名字可以和项目名字不一样;登录github,进到个人主页,点击切换到Repositories,然后点new创建。可以写上描述,选择公开或私有,也可以选择为项目添加一个README说明文件,如下图:


图2-1

图2-1

3、切回As,那么在提交项目之前要做一件很重要的事情,就是配置好gitignore文件,我们将项目展开会发现as已经帮我们生成了gitignore文件,在项目根目录有一个,在app目录也有一个;那么在app 模块里面很简单只有一行/build,这个我们不用管,那么在项目根目录的gitignore文件里面的内容我们可以看一下


图3-1

切换到project模式下我们会发现,项目中的文件并不是都变红了,有的是白色的,那么这些没有变红的文件就是被这个gitignore文件忽略掉的,那么我们关心的是哪些文件应该被忽略掉哪些文件不应该忽略?是As能在编译过程中自动生成的文件都不应该提交上去;也就是说我传上去的代码,别人拉取下来通过As编译能够正常跑起来,并且这个时候本地不产生可以提交的文件;这些文件主要包括.idea、.gradle、iml文件、以及配置sdk路径的local.properties等等,As为我们自动生成并不是很全面,这里推荐一个github上列出来的https://github.com/github/gitignore/blob/master/Android.gitignore我们也可以使用这个。

4、准备好了之后下面开始提交代码了,在项目根目录上右键Git-->Add将代码添加到索引库,然后Git-->Commit Directory提交到本地仓库;到此为止我们依然没有和github联系上;

5、将代码push到远程仓库Git-->Repository-->push到远程仓库,在弹出框里面输入之前创建的git远程仓库地址https://github.com/AndSync/GitTest.git,点OK,如果没登录会提示登录,以前登录过就不提示了,完了之后点push。



图5-1

这时候在右上角会有一个提示push rejected ,原因是我们本地仓库的master主线并没有和远程仓库的master主线绑定上,那么首先Git->Repository-->fetch一下,获取到远程master分支,这时候发现右下角有变化了,显示出来了origin/master,


图5-2

然后我们需要通过命令来完成绑定,在Terminal里面可以输入命令,这段命令怎么来的,其实如果你不用As可视化工具 这个时候去 git push 是会提示给你的,告诉你应该输什么命令,git还是很智能的,建议还是多用命令来操作。

git branch --set-upstream-to origin/master

然后我们再去push 这时候可以了,会弹出一个merge提示框,我们点merge又报错了,哦是不是因为创建项目的时候有一个文件README,那我们pull一下看行不行,发现也报错,于是我们要祭出另一句git命令来解决这个问题


图5-3

git pull --allow-unrelated-histories

允许拉取不相关的历史记录,把README拉取过来了,当然如果你创建项目的时候没有创建README可能没这么麻烦,这时再去push就OK了。整个过程到此结束

方法二:先创建连接,后创建项目

这个怎么玩的呢,简要说明一下,之前不是有安装git客户端,应该在任何地方鼠标右键都有一个菜单Git Gui here -->Clone Existing Repository


注意本地路径文件夹GitTest2事先不要创建


clone完了之后,在这个目录下会有一个.git文件夹,和readme文件,那么到这一步我们也可以发现这种方式貌似并不适用于新建项目,因为这个文件夹里面有文件As将无法在这个目录下创建文件,那么这适用于从别的地方考过来一个已有的项目放到这里,之后从As打开项目进行add、commit、push等操作即可。

总结:以上两种方式就介绍完了,第一次写文章,欢迎批评指教,如果对您有一点点帮助那本人荣幸至极。


作者:mylizhimin 发表于2016/11/30 23:55:14 原文链接
阅读:32 评论:0 查看评论

深入探讨Android异步精髓Handler

$
0
0

深入探讨Android异步精髓Handler


站在源码的肩膀上全解Scroller工作机制


Android多分辨率适配框架(1)— 核心基础
Android多分辨率适配框架(2)— 原理剖析
Android多分辨率适配框架(3)— 使用指南


自定义View系列教程00–推翻自己和过往,重学自定义View
自定义View系列教程01–常用工具介绍
自定义View系列教程02–onMeasure源码详尽分析
自定义View系列教程03–onLayout源码详尽分析
自定义View系列教程04–Draw源码分析及其实践
自定义View系列教程05–示例分析
自定义View系列教程06–详解View的Touch事件处理
自定义View系列教程07–详解ViewGroup分发Touch事件
自定义View系列教程08–滑动冲突的产生及其处理


PS:假若您觉得文章偏长,请您浏览本文对应的视频


前言

众所周知,Android的UI是在其主线程中进行刷新的,所以Google建议开发人员切勿在主线程中进行耗时的操作否则很容易导致应用程序无响应(ANR)。鉴于此几乎接近硬性的要求,我们常把耗时的操作(比如网络请求)置于子线程中进行;但是子线程不能直接访问UI。

至此,这个矛盾就凸显出来了:

  • 主线程可以刷新UI,但不能执行耗时操作
  • 子线程可以执行耗时操作 ,但是不能直接刷新UI

嗯哼,那有没有一个东西可以调和并化解这个矛盾呢?当然是有的,Google采用Handler把主线程和子线程精巧地联系起来——子线程中进行耗时的业务逻辑,然后利用Handler通知主线程刷新UI。除此以外,还有别的方式可以实现类似的操作么?答案是肯定的,我们也可以利用AsyncTask或者IntentService进行异步的操作。这两者又是怎么做到的呢?其实,在AsyncTask和IntentService的内部亦使用了Handler实现其主要功能。抛开这两者不谈,当我们打开Android源码的时候也随处可见Handler的身影。所以,Handler是Android异步操作的核心和精髓,它在众多领域发挥着极其重要甚至是不可替代的作用。

在此,对Handler的工作原理和实现机制进行系统的梳理。


ThreadLocal简介及其使用

对于线程Thread大家都挺熟悉的了,但是对于ThreadLocal可能就要陌生许多了。虽然我们对于它不太了解,但是它早在JDK1.2版本中就已问世并且被广泛的使用,比如Hibernate,EventBus,Handler都运用了ThreadLocal进行线程相关的操作。如果单纯地从ThreadLocal这个名字来看,它带着浓浓的“本地线程”的味道; 然而,喝一口之后才发现根本就不是这个味儿。其实,ThreadLocal并不是用来操作什么本地线程而是用于实现不同线程的数据副本。当使用ThreadLocal维护变量时,它会为每个使用该变量的线程提供独立的变量副本;每一个线程都可以独立地改变自己的副本并且不会影响其它线程所持有的对应的副本。所以,ThreadLocal的实际作用并不与它的名字所暗含的意义相吻合,或许改称为ThreadLocalVariable(线程本地变量)会更合适一些。

接下来,我们通过一个实例来瞅瞅ThreadLocal的使用方式

    /**
     * 原创作者:
     * 谷哥的小弟
     *
     * 博客地址:
     * http://blog.csdn.net/lfdfhl
     */
    private void testThreadLocal(){
        mThreadLocal.set("东京热");
        new HotThread1().start();
        new HotThread2().start();
        hot3=mThreadLocal.get();
        try{
            Thread.sleep(1000*4);
            Log.i(TAG,"HotThread1获取到的变量值: "+hot1);
            Log.i(TAG,"HotThread2获取到的变量值: "+hot2);
            Log.i(TAG,"MainThread获取到的变量值: "+hot3);
        }catch (Exception e){

        }
    }

    private class HotThread1  extends Thread{
        @Override
        public void run() {
            super.run();
            mThreadLocal.set("北京热");
            hot1=mThreadLocal.get();
        }
    }

    private class HotThread2  extends Thread{
        @Override
        public void run() {
            super.run();
            mThreadLocal.set("南京热");
            hot2=mThreadLocal.get();
        }
    }

查看输出结果:

HotThread1获取到的变量值: 北京热
HotThread2获取到的变量值: 南京热
MainThread获取到的变量值: 东京热

在这段代码中使用ThreadLocal保存String类型的数据,并且在主线程和两个子线程中为ThreadLocal设置了不同的值,然后再将这些值分别取出。结合输出日志可以发现:在不同的线程中访问了同一个ThreadLocal对象,但是通过mThreadLocal.get()得到的值却是不一样的;也就是说:它们之间没有发生相互的影响而是保持了彼此的独立。明白了ThreadLocal的这个特性之后,我们再去理解Looper的工作机制就会容易得多了。


Looper、线程、消息队列的关系

Google官方建议开发人员使用Handler实现异步刷新UI,我们在平常的工作中也很好地采纳了这个提议:首先在主线程中建立Handler,然后在子线程中利用handler.sendMessage(message)发送消息至主线程,最终消息在handleMessage(Message msg) {}得到相应的处理。这个套路,大家都再熟悉不过了;现在换个角度,我们试试在子线程中建立Handler

private class LooperThread  extends Thread{
        @Override
        public void run() {
            super.run();
            Handler handler=new Handler();
            //doing something
        }
    }

此处的代码很简单:LooperThread继承自Thread,并且在其run( )方法中新建一个Handler。
嗯哼,再运行一下,喔哦,报错了:

Can’t create handler inside thread that has not called Looper.prepare().

咦,有点出师不利呢,刚开始试就出错了…….没事,生活不就是无尽的挫折和希望嘛,这点小事嘛也不算。既然是在调用Handler的构造方法时报的错那就从该构造方法的源码入手,一探究竟:

public Handler() {
    this(null, false);
}

public Handler(Callback callback) {
    this(callback, false);
}


public Handler(Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur");
        }
    }

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException
        ("Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

请注意第20行代码:
如果mLooper == null那么系统就会抛出刚才的错误:Can’t create handler inside thread that has not called Looper.prepare()。这句话的意思是:如果在线程内创建handler必须调用Looper.prepare()。既然这个提示已经提示了我们该怎么做,那就加上这一行代码:

private class LooperThread  extends Thread{
        @Override
        public void run() {
            super.run();
            Looper.prepare();
            Handler handler=new Handler();
            System.out.println("add code : Looper.prepare()");
            //doing something
        }
    }

嘿嘿,果然不再报错了,运行一下:

这里写图片描述

既然Looper.prepare()解决了这个问题,那我们就去瞅瞅在该方法中做了哪些操作:

/**Initialize the current thread as a looper.
 * This gives you a chance to create handlers that then reference
 * this looper, before actually starting the loop. Be sure to call
 * loop() after calling this method, and end it by calling quit().
 */
public static void prepare() {
    prepare(true);
}

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

从这段源码及其注释文档我们可以看出:

  1. 在prepare()中利用一个Looper来初始化当前线程或者说初始化一个带有Looper的线程。
    请注意第14行代码,它是这段源码的核心,现对其详细分析:

    sThreadLocal.set(new Looper(quitAllowed));

    在该行代码中一共执行了两个操作

    (1) 构造Looper

    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

    在Looper的构造方法中初始化了一个消息队列MessageQueue和一个线程Thread。从这可看出:一个Looper对应着一个消息队列以及当前线程。
    当收到消息Message后系统会将其存入消息队列中等候处理。至于Looper,它在Android的消息机制中担负着消息轮询的职责,它会不间断地查看MessageQueue中是否有新的未处理的消息;若有则立刻处理,若无则进入阻塞。

    (2) 将此Looper保存到sThreadLocal中。
    此处的sThreadLocal是定义在Looper类中的一个ThreadLocal类型变量

    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

    Looper是framework中的一个类,sThreadLocal是它的一个static final变量。当在某一个Thread中执行Looper.prepare()时系统就会将与该Thread所对应的Looper保存到sThreadLocal中。不同的线程对着不同的Looper,但它们均由系统保存在sThreadLocal中并且互不影响,相互独立;并且可以通过sThreadLocal.get()获取不同线程所对应的Looper.

  2. 在调用prepare()方法后需要调用loop()方法开始消息的轮询,并且在需要的时候调用quit()方法停止消息的轮询
  3. 假若再次执行Looper.prepare()系统发现sThreadLocal.get()的值不再为null于是抛出异常:
    Only one Looper may be created per thread,一个线程只能创建一个Looper!

小结:

  1. 一个线程对应一个Looper
  2. 一个Looper对应一个消息队列
  3. 一个线程对应一个消息队列
  4. 线程,Looper,消息队列三者一一对应

所以,在一个子线程中使用Handler的方式应该是这样的:

class LooperThread extends Thread { 
    public Handler mHandler;
    public void run() { 
        Looper.prepare(); 
        mHandler = new Handler() { 
            public void handleMessage(Message msg) { 

            } 
        };
        Looper.loop(); 
      } 
  }

看到这个范例,有的人可能心里就犯嘀咕了:为什么我们平常在MainActivity中使用Handler时并没有调用Looper.prepare()也没有报错呢?
这是因为UI线程是主线程,系统会自动调用Looper.prepareMainLooper()方法创建主线程的Looper和消息队列MessageQueue


Message的发送和处理过程

在讨论完Looper、线程、消息队列这三者的关系之后我们再来瞅瞅Android消息机制中对于Message的发送和处理。

平常最常用的方式:
handler.sendMessage(message)——>发送消息
handleMessage(Message msg){}——>处理消息

先来分析消息的入队。
Handler可以通过post()、postAtTime()、postDelayed()、postAtFrontOfQueue()等方法发送消息,除了postAtFrontOfQueue()之外这几个方法均会执行到sendMessageAtTime(Message msg, long uptimeMillis)方法,源码如下:

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}


public final boolean sendMessageAtFrontOfQueue(Message msg) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, 0);
}

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

在这里可以看到sendMessageAtTime()内部又调用了enqueueMessage(),在该方法内的重要操作:

  • 第一步:
    给msg设置了target,请参见代码第25行

    此处的this就是当前Handler对象本身。在这就指明了该msg的来源——它是由哪个Handler发出的,与此同时也指明了该msg的归宿——它该由哪个Handler处理。不难发现,哪个Handler发出了消息就由哪个Handler负责处理。

  • 第二步:
    将消息放入消息队列中,请参见代码第29行

    在enqueueMessage(msg,uptimeMillis)中将消息Message存放进消息队列中,距离触发时间最短的message排在队列最前面,同理距离触发时间最长的message排在队列的最尾端。若调用sendMessageAtFrontOfQueue()方法发送消息它会直接调用该enqueueMessage(msg,uptimeMillis)让消息入队只不过时间为延迟时间为0,也就是说该消息会被插入到消息队列头部优先得到执行。

    直觉告诉我们此处的消息队列mQueue就是该线程所对应的消息队列。可是光有直觉是不够的甚至是不可靠的。我们再回过头瞅瞅Handler的构造方法,从源码中找到确切的依据

    public Handler(Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur");
            }
        }
    
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException
            ("Can't create handler inside thread that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

    (1) 获取Looper,请参见代码第10行
    (2) 利用Looper的消息队列为mQueue赋值,请参见代码第15行
    (3) 为mCallback赋值,,请参见代码第16行
    (4) 为mAsynchronous赋值,,请参见代码第17行

    嗯哼,看到了吧,这个mQueue就是从Looper中取出来的。在之前我们也详细地分析了Looper、线程、消息队列这三者的一一对应关系,所以此处的mQueue正是线程所对应的消息队列。

看完了消息的入队,再来分析消息的出队。
请看Looper中的loop()方法源码:

public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;

    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }

        // This must be in a local variable, in case a UI event sets the logger
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }

        final long traceTag = me.mTraceTag;
        if (traceTag != 0) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }
        try {
            msg.target.dispatchMessage(msg);
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }

        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

        // Make sure that during the course of dispatching the
        // identity of the thread wasn't corrupted.
        final long newIdent = Binder.clearCallingIdentity();
        if (ident != newIdent) {
            Log.wtf(TAG, "Thread identity changed from 0x"
                    + Long.toHexString(ident) + " to 0x"
                    + Long.toHexString(newIdent) + " while dispatching to "
                    + msg.target.getClass().getName() + " "
                    + msg.callback + " what=" + msg.what);
        }

        msg.recycleUnchecked();
    }
}

我们发现关于消息的处理是在一个死循环中就行的,请参见代码第13-55行。也就是说在该段代码中Looper一直在轮询消息队列MessageQueue。假若消息队列中没有未处理的消息(即queue.next()==null)则其进入阻塞block状态,假若消息队列中有待处理消息(即queue.next()!=null)则利用msg.target.dispatchMessage(msg)将该消息派发至对应的Handler。

到了这,可能有的人会有一个疑问:系统怎么知道把消息发送给哪个Handler呢?
嘿嘿,还记不记得enqueueMessage()中系统给msg设置了target从而确定了其目标Handler么?嗯哼,所以只要通过msg.target.dispatchMessage(msg)就可以将消息派发至对应的Handler了。那在dispatchMessage()中又会对消息做哪些操作呢?我们继续跟进源码

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

哇哈,看到这,心情就舒畅多了,基本上回到了我们熟悉的地方;在此处对Message消息进行了处理,我们来瞅瞅主要的步骤

  • 第一步:
    处理Message的回调callback,请参见代码第3行
    比如调用handler.post(Runnable runnable)时,该runnable就会被系统封装为Message的callback。
    关于这点在源码中也有非常直观的体现:

    private static Message getPostMessage(Runnable r) {
       Message m = Message.obtain();
       m.callback = r;
       return m;
    }
  • 第二步:
    处理Handler的回调callback,请参见代码第6行
    比如执行Handler handler=Handler(Callback callback)时就会将callback赋值给mCallback,关于这点已经在介绍Handler构造方法时分析过了,不再赘述。

  • 第三步:
    调用handleMessage()处理消息Message,请参见代码第10行
    handleMessage()的源码如下:

    public void handleMessage(Message msg) {
    
    }

    嗯哼,它是一个空的方法。所以Handler的子类需要覆写该方法,并在其中处理接收到的消息。


梳理Handler工作机制

至此,关于Handler的异步机制及其实现原理已经分析完了。在此,对其作一个全面的梳理和总结。

Android异步消息机制中主要涉及到:Thread、Handler、MessageQueue、Looper,在整个机制中它们扮演着不同的角色也承担着各自的不同责任。

  • Thread

    负责业务逻辑的实施。
    线程中的操作是由各自的业务逻辑所决定的,视具体情况进行。

  • Handler

    负责发送消息和处理消息。
    通常的做法是在主线程中建立Handler并利用它在子线程中向主线程发送消息,在主线程接收到消息后会对其进行处理

  • MessageQueue

    负责保存消息。
    Handler发出的消息均会被保存到消息队列MessageQueue中,系统会根据Message距离触发时间的长短决定该消息在队列中位置。在队列中的消息会依次出队得到相应的处理。

  • Looper

    负责轮询消息队列。
    Looper使用其loop()方法一直轮询消息队列,并在消息出队时将其派发至对应的Handler.

为了更好地理解这几者的相互关系及其作用,请参见如下示图

这里写图片描述


使用Handler的错误姿势及其潜在风险

关于Handler的具体用法,尤其是那些常规的使用方式在此就不再一一列举了。
我们要讨论和分析的是在开发中不恰当地使用Handler的方式及其带来的潜在风险。

第一个问题:
利用handler.post(Runnable runnable)执行耗时操作
请看如下示例:

/**
 * 原创作者:
 * 谷哥的小弟
 *
 * 博客地址:
 * http://blog.csdn.net/lfdfhl
 */
public class MainActivity extends AppCompatActivity {
    private TextView mTextView;
    private Handler mHandler;
    private ImageView mImageView;
    private Resources mResources;
    private static final String TAG="stay4it";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }
    private void init(){
        mResources=getResources();
        mHandler=new Handler();
        mTextView=(TextView) findViewById(R.id.textView);
        mImageView =(ImageView) findViewById(R.id.imageView);
        mImageView.setOnClickListener(new OnClickListenerImpl());
        long threadID=Thread.currentThread().getId();
        Log.i(TAG,"主线程的线程ID="+threadID);
    }


    private class OnClickListenerImpl implements View.OnClickListener {
        @Override
        public void onClick(View v) {
            new TestThread().start();
        }
    }

    private class TestThread extends Thread{
        @Override
        public void run() {
            super.run();
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    String text=mResources.getString(R.string.text);
                    long threadID=Thread.currentThread().getId();
                    Log.i(TAG,"在post(Runnable r)里的run()获取到线程ID="+threadID);
                    mTextView.setText(text);
                }
            });
        }
    }
}

运行一下,观察效果:

这里写图片描述

我们在开发中可能会做如上的操作:在主线程中创建Handler,然后在子线程里利用handler.post(Runnable runnable)执行某些操作甚至是耗时的操作。可是这么做合适么?我们来看看主线程的ID和在Runnable的run()方法里获取到的线程ID,输出日志如下:

主线程的线程ID=1
在post(Runnable r)里的run()获取到线程ID=1

在这里我们发现在两处获得的线程ID是同一个值,也就是说Runnable的run()方法并不是在一个新线程中执行的,而是在主线程中执行的。
为什么明明把handler.post(Runnable runnable)放入到子线程中了但是Runnable的run()却在主线程中执行呢?
其实,这个问题在之前的分析中已经提到了:调用handler.post(Runnable runnable)时,该runnable会被系统封装为Message的callback。所以,handler.post(Runnable runnable)和handler.sendMessage(Message message)这两个不同的方法在本质上是相同的——Handler发送了一条消息。在该示例中handler是在主线程中创建的,所以它当然会在主线程中处理消息;如此以来该Runnable亦会在主线程中执行;所以,在Runnable的run()方法中执行耗时的操作是不可取的容易导致应用程序无响应。

那么,调用view.post(Runnable runnable)会在子线程中执行还是主线程中执行呢?
我们来瞅瞅它的实现:

public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
    getRunQueue().post(action);
    return true;
}

看到这段源码就无需再做过多的解释了,它依然是在主线程中执行的,原理同上。

那么,调用Activity.runOnUiThread(Runnable runnable)方法会在子线程中执行还是主线程中执行呢?

public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        action.run();
    }
}

嗯哼,这段源码就更简单了。如果当前线程是UI线程,那么该Runnable会立即执行;如果当前线程不是UI线程,则使用handler的post()方法将其放入UI线程的消息队列中。

小结:
handler.post(Runnable runnable)、view.post(Runnable runnable)、Activity.runOnUiThread(Runnable runnable)的runnable均会在主线程中执行,所以切勿在其run()方法中执行耗时的操作

第二个问题:
Handler导致的潜在内存泄露
请看如下示例:

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;

/**
 * 原创作者:
 * 谷哥的小弟
 *
 * 博客地址:
 * http://blog.csdn.net/lfdfhl
 */
public class MainActivity extends AppCompatActivity {
    private Handler mHandler;
    private static final String TAG="stay4it";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }

    private void init() {
        mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                Log.i(TAG,"handle message");
            }
        };
        Message message=Message.obtain();
        message.what=9527;
        mHandler.sendMessage(message);

        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {

            }
        }, 1000 * 20);
    }
}

以上是我们在工作中常见的对于Handler的使用方式,为了更形象地说明问题特意把Runnable所延迟时间设置得比较久。如此操作,猛地一看觉得没啥不妥当的地方;但是简单地分析一下这段代码,却发现它存在潜在的内存泄露风险。

  • 内部类new Handler(){}持有外部类MainActivity的引用
  • 内部类new Runnable(){}持有外部类MainActivity的引用
  • new Runnable(){}会被封装成Message的callback且Message会持有Handler的引用
  • handler发送了延迟消息,所以消息队列持该Runnable的引用
  • 综合这几者,可理解为消息间接地持有了MainActivity的引用

现在假设这么一种情况:进入该Activity后在20秒以内的任意时间旋转屏幕,此时会导致Activity重新绘制。但是通过postDelayed()发出的Runnable还未被执行,所以消息队仍列持有Runnable的引用,而Runnable也依然持有Activity的引用,故此时Activity所占内存并不能向期望的那样被回收,这样就可能会造成内存的泄漏。

优化该问题的方式有多种,在此展示其中一种比较好的实现方式:

/**
 * 原创作者:
 * 谷哥的小弟
 *
 * 博客地址:
 * http://blog.csdn.net/lfdfhl
 */
public class MainActivity extends AppCompatActivity {
    private Activity mActivity;
    private static final String TAG="stay4it";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }

    private void init(){
        mActivity=this;
        BetterHandler betterHandler = new BetterHandler(mActivity);
        Message message=Message.obtain();
        message.what=9527;
        betterHandler.sendMessage(message);
        betterHandler.postDelayed(new BetterRunnable(), 1000 * 20);
    }

    private static class BetterRunnable implements Runnable {
        @Override
        public void run() {
            Log.i(TAG,"Runnable run()");
        }
    }

    private static class BetterHandler extends Handler {
        private WeakReference<Activity> activityWeakReference;

        public BetterHandler(Activity activity) {
            activityWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (activityWeakReference.get() != null) {
                Log.i(TAG,"handle message");
            }
        }
    }
}

看到这段代码,我们发现了一个陌生的东西WeakReference。什么是WeakReference呢?它有什么特点呢?

从JDK1.2开始,Java把对象的引用分为四种级别,这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

  1. 强引用
    我们一般使用的就是强引用,垃圾回收器一般都不会对其进行回收操作。当内存空间不足时Java虚拟机宁愿抛出OutOfMemoryError错误使程序异常终止,也不会回收具有强引用的对象

  2. 软引用
    如果一个对象具有软引用(SoftReference),在内存空间足够的时候GC不会回收它,如果内存空间不足了GC就会回收这些对象的内存空间。

  3. 弱引用
    如果一个对象具有弱引用(WeakReference),那么当GC线程扫描的过程中一旦发现某个对象只具有弱引用而不存在强引用时不管当前内存空间足够与否GC都会回收它的内存。由于垃圾回收器是一个优先级较低的线程,所以不一定会很快发现那些只具有弱引用的对象。为了防止内存溢出,在处理一些占用内存大而且生命周期较长的对象时候,可以尽量使用软引用和弱引用.

  4. 虚引用
    虚引用(PhantomReference)与其他三种引用都不同,它并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。所以,虚引用主要用来跟踪对象被垃圾回收器回收的活动,在一般的开发中并不会使用它

嗯哼,在了解这四种引用之后我们继续分析刚才的代码。在该示例中做了如下主要操作:

  • 将Handler和Runnable定义为Activity的静态内部类

    这两者定义为静态内部类后它们就不再持有外部类(Activity)的引用,具体代码请参见示例中BetterHandler和BetterRunnable的实现

  • 在BetterHandler内使用弱引用WeakReference持有Activity

    在完成这两步操作之后,我们再来分析刚才的场景:进入该Activity后在20秒以内的任意时间旋转屏幕导致Activity重新绘制。此时,消息持有Handler的引用,但Handler对象不再持有Activity的强引用,所以系统可以回收该Activity从而避免了内存泄露的发生。对于这样的做法,可能有的人觉得不是特别好理解,那我再换一种直白的通俗的描述:如果直接将Activity传入BetterHandler中并且不对其使用WeakReference那么它依然是一个强引用,这和之前未优化的代码相比是没有任何差别的。假若把Activity传进BetterHandler之后并用WeakReference“包裹”了它,使之不再是一个强引用而变成了一个弱引用。当Activity发生重绘时,GC发现对于这个Activity没有强引用而只存在一个弱引用,那么系统就将其回收。

  • 在handleMessage( )对Activity进行非空判断

    因为Activity可能已经被GC回收,所以在处理消息时要判断Activity是否为null,即if(activityWeakReference.get() != null)从而避免异常的发生。


后语

我想能看到这的人已经不多了。

大家都知道,Handler在Android体系中占有具有举足轻重的作用,只有掌握了Handler的实现原理和工作机制才可能更全面,更深入地掌握Android开发技能。在本文中我力图详细地阐述Handler相关技术,所以文章的篇幅偏长了。如果你耐着性子看到了此处,请为自己点个赞;你的坚持和努力不会白费,它们会让你变得更好。


PS:假若您觉得文章偏长,请您浏览本文对应的视频

作者:lfdfhl 发表于2016/12/1 0:08:41 原文链接
阅读:47 评论:0 查看评论

Android对话框Dialog深度剖析

$
0
0

对话框

对话框是提示用户作出决定或输入额外信息的小窗口。 对话框不会填充屏幕,通常用于需要用户采取行动才能继续执行的模式事件。

对话框设计

这里写图片描述

Dialog 类是对话框的基类,但您应该避免直接实例化 Dialog,而是使用下列子类之一:

AlertDialog

此对话框可显示标题、最多三个按钮、可选择项列表或自定义布局。
DatePickerDialog 或 TimePickerDialog
此对话框带有允许用户选择日期或时间的预定义 UI。
避免使用 ProgressDialog
Android 包括另一种名为 ProgressDialog 的对话框类,可显示具有进度条的对话框。不过,如需指示加载进度或不确定的进度,则应改为遵循进度和 Activity 的设计指南,并在您的布局中使用 ProgressBar。
这些类定义您的对话框的样式和结构,但您应该将 DialogFragment 用作对话框的容器。DialogFragment 类提供您创建对话框和管理其外观所需的所有控件,而不是调用 Dialog 对象上的方法。

使用 DialogFragment 管理对话框可确保它能正确处理生命周期事件,如用户按“返回”按钮或旋转屏幕时。 此外,DialogFragment 类还允许您将对话框的 UI 作为嵌入式组件在较大 UI 中重复使用,就像传统 Fragment 一样(例如,当您想让对话框 UI 在大屏幕和小屏幕上具有不同外观时)。

注:

由于 DialogFragment 类最初是通过 Android 3.0(API 级别 11)添加的,因此本文描述的是如何使用支持库附带的 DialogFragment 类。 通过将该库添加到您的应用,您可以在运行 Android 1.6 或更高版本的设备上使用 DialogFragment 以及各种其他 API。如果您的应用支持的最低版本是 API 级别 11 或更高版本,则可使用 DialogFragment 的框架版本,但请注意,本文中的链接适用于支持库 API。 使用支持库时,请确保您导入的是 android.support.v4.app.DialogFragment 类,而不是 android.app.DialogFragment。

创建对话框片段

您可以完成各种对话框设计—包括自定义布局以及对话框设计指南中描述的布局—通过扩展 DialogFragment 并在 onCreateDialog() 回调方法中创建 AlertDialog。

例如,以下是一个在 DialogFragment 内管理的基础 AlertDialog:

public class FireMissilesDialogFragment extends DialogFragment {
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // Use the Builder class for convenient dialog construction
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setMessage(R.string.dialog_fire_missiles)
               .setPositiveButton(R.string.fire, new DialogInterface.OnClickListener() {
                   public void onClick(DialogInterface dialog, int id) {
                       // FIRE ZE MISSILES!
                   }
               })
               .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
                   public void onClick(DialogInterface dialog, int id) {
                       // User cancelled the dialog
                   }
               });
        // Create the AlertDialog object and return it
        return builder.create();
    }
}

这里写图片描述
图 1. 一个包含消息和两个操作按钮的对话框。

现在,当您创建此类的实例并调用该对象上的 show() 时,对话框将如图 1 所示。

下文将详细描述如何使用 AlertDialog.Builder API 创建对话框。

根据对话框的复杂程度,您可以在 DialogFragment 中实现各种其他回调方法,包括所有基础 片段生命周期方法。

构建提醒对话框

您可以通过 AlertDialog 类构建各种对话框设计,并且该类通常是您需要的唯一对话框类。如图 2 所示,提醒对话框有三个区域:
这里写图片描述

图 2. 对话框的布局。

标题

这是可选项,只应在内容区域被详细消息、列表或自定义布局占据时使用。 如需陈述的是一条简单消息或问题(如图 1 中的对话框),则不需要标题。

内容区域

它可以显示消息、列表或其他自定义布局。

操作按钮

对话框中的操作按钮不应超过三个。
AlertDialog.Builder 类提供的 API 允许您创建具有这几种内容(包括自定义布局)的 AlertDialog。

要想构建 AlertDialog,请执行以下操作:

// 1. Instantiate an AlertDialog.Builder with its constructor
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

// 2. Chain together various setter methods to set the dialog characteristics
builder.setMessage(R.string.dialog_message)
       .setTitle(R.string.dialog_title);

// 3. Get the AlertDialog from create()
AlertDialog dialog = builder.create();

以下主题介绍如何使用 AlertDialog.Builder 类定义各种对话框属性。

添加按钮

要想添加如图 2 所示的操作按钮,请调用 setPositiveButton() 和 setNegativeButton() 方法:

AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
// Add the buttons
builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
           public void onClick(DialogInterface dialog, int id) {
               // User clicked OK button
           }
       });
builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
           public void onClick(DialogInterface dialog, int id) {
               // User cancelled the dialog
           }
       });
// Set other dialog properties
...

// Create the AlertDialog
AlertDialog dialog = builder.create();

set…Button() 方法需要一个按钮标题(由字符串资源提供)和一个 DialogInterface.OnClickListener,后者用于定义用户按下该按钮时执行的操作。

您可以添加三种不同的操作按钮:

肯定
您应该使用此按钮来接受并继续执行操作(“确定”操作)。
否定
您应该使用此按钮来取消操作。
中性
您应该在用户可能不想继续执行操作,但也不一定想要取消操作时使用此按钮。 它出现在肯定按钮和否定按钮之间。 例如,实际操作可能是“稍后提醒我”。
对于每种按钮类型,您只能为 AlertDialog 添加一个该类型的按钮。也就是说,您不能添加多个“肯定”按钮。
这里写图片描述

图 3. 一个包含标题和列表的对话框。

添加列表

可通过 AlertDialog API 提供三种列表:

传统单选列表
永久性单选列表(单选按钮)
永久性多选列表(复选框)
要想创建如图 3 所示的单选列表,请使用 setItems() 方法:

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
    builder.setTitle(R.string.pick_color)
           .setItems(R.array.colors_array, new DialogInterface.OnClickListener() {
               public void onClick(DialogInterface dialog, int which) {
               // The 'which' argument contains the index position
               // of the selected item
           }
    });
    return builder.create();
}

由于列表出现在对话框的内容区域,因此对话框无法同时显示消息和列表,您应该通过 setTitle() 为对话框设置标题。要想指定列表项,请调用setItems() 来传递一个数组。或者,您也可以使用 setAdapter() 指定一个列表。 这样一来,您就可以使用 ListAdapter 以动态数据(如来自数据库的数据)支持列表。

如果您选择通过 ListAdapter 支持列表,请务必使用 Loader,以便内容以异步方式加载。使用适配器构建布局和加载程序指南中对此做了进一步描述。

注:默认情况下,触摸列表项会清除对话框,除非您使用的是下列其中一种永久性选择列表。

这里写图片描述
图 4. 多选项列表。

添加永久性多选列表或单选列表
要想添加多选项(复选框)或单选项(单选按钮)列表,请分别使用 setMultiChoiceItems() 或 setSingleChoiceItems() 方法。

例如,以下示例展示了如何创建如图 4 所示的多选列表,将选定项保存在一个 ArrayList 中:

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    mSelectedItems = new ArrayList();  // Where we track the selected items
    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
    // Set the dialog title
    builder.setTitle(R.string.pick_toppings)
    // Specify the list array, the items to be selected by default (null for none),
    // and the listener through which to receive callbacks when items are selected
           .setMultiChoiceItems(R.array.toppings, null,
                      new DialogInterface.OnMultiChoiceClickListener() {
               @Override
               public void onClick(DialogInterface dialog, int which,
                       boolean isChecked) {
                   if (isChecked) {
                       // If the user checked the item, add it to the selected items
                       mSelectedItems.add(which);
                   } else if (mSelectedItems.contains(which)) {
                       // Else, if the item is already in the array, remove it
                       mSelectedItems.remove(Integer.valueOf(which));
                   }
               }
           })
    // Set the action buttons
           .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
               @Override
               public void onClick(DialogInterface dialog, int id) {
                   // User clicked OK, so save the mSelectedItems results somewhere
                   // or return them to the component that opened the dialog
                   ...
               }
           })
           .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
               @Override
               public void onClick(DialogInterface dialog, int id) {
                   ...
               }
           });

    return builder.create();
}

尽管传统列表和具有单选按钮的列表都能提供“单选”操作,但如果您想持久保存用户的选择,则应使用 {@linkandroid.app.AlertDialog.Builder#setSingleChoiceItems(int,int,DialogInterface.OnClickListener) setSingleChoiceItems()}。也就是说,如果稍后再次打开对话框时系统应指示用户的当前选择,那么您就需要创建一个具有单选按钮的列表。

创建自定义布局

这里写图片描述
图 5. 自定义对话框布局。

如果您想让对话框具有自定义布局,请创建一个布局,然后通过调用 AlertDialog.Builder 对象上的 setView() 将其添加到 AlertDialog。

默认情况下,自定义布局会填充对话框窗口,但您仍然可以使用 AlertDialog.Builder 方法来添加按钮和标题。

例如,以下是图 5 中对话框的布局文件:

res/layout/dialog_signin.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <ImageView
        android:src="@drawable/header_logo"
        android:layout_width="match_parent"
        android:layout_height="64dp"
        android:scaleType="center"
        android:background="#FFFFBB33"
        android:contentDescription="@string/app_name" />
    <EditText
        android:id="@+id/username"
        android:inputType="textEmailAddress"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:layout_marginLeft="4dp"
        android:layout_marginRight="4dp"
        android:layout_marginBottom="4dp"
        android:hint="@string/username" />
    <EditText
        android:id="@+id/password"
        android:inputType="textPassword"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:layout_marginLeft="4dp"
        android:layout_marginRight="4dp"
        android:layout_marginBottom="16dp"
        android:fontFamily="sans-serif"
        android:hint="@string/password"/>
</LinearLayout>

提示:默认情况下,当您将 EditText 元素设置为使用 “textPassword” 输入类型时,字体系列将设置为固定宽度。因此,您应该将其字体系列更改为 “sans-serif”,以便两个文本字段都使用匹配的字体样式。

要扩展 DialogFragment 中的布局,请通过 getLayoutInflater() 获取一个 LayoutInflater 并调用 inflate(),其中第一个参数是布局资源 ID,第二个参数是布局的父视图。然后,您可以调用 setView() 将布局放入对话框。

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
    // Get the layout inflater
    LayoutInflater inflater = getActivity().getLayoutInflater();

    // Inflate and set the layout for the dialog
    // Pass null as the parent view because its going in the dialog layout
    builder.setView(inflater.inflate(R.layout.dialog_signin, null))
    // Add action buttons
           .setPositiveButton(R.string.signin, new DialogInterface.OnClickListener() {
               @Override
               public void onClick(DialogInterface dialog, int id) {
                   // sign in the user ...
               }
           })
           .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
               public void onClick(DialogInterface dialog, int id) {
                   LoginDialogFragment.this.getDialog().cancel();
               }
           });
    return builder.create();
}

提示:如果您想要自定义对话框,可以改用对话框的形式显示 Activity,而不是使用 Dialog API。 只需创建一个 Activity,并在 清单文件元素中将其主题设置为 Theme.Holo.Dialog:

<activity android:theme="@android:style/Theme.Holo.Dialog" >

就这么简单。Activity 现在会显示在一个对话框窗口中,而非全屏显示。
将事件传递回对话框的宿主
当用户触摸对话框的某个操作按钮或从列表中选择某一项时,您的 DialogFragment 可能会自行执行必要的操作,但通常您想将事件传递给打开该对话框的 Activity 或片段。 为此,请定义一个界面,为每种点击事件定义一种方法。然后在从该对话框接收操作事件的宿主组件中实现该界面。

例如,以下 DialogFragment 定义了一个界面,通过该界面将事件传回给宿主 Activity:

public class NoticeDialogFragment extends DialogFragment {

    /* The activity that creates an instance of this dialog fragment must
     * implement this interface in order to receive event callbacks.
     * Each method passes the DialogFragment in case the host needs to query it. */
    public interface NoticeDialogListener {
        public void onDialogPositiveClick(DialogFragment dialog);
        public void onDialogNegativeClick(DialogFragment dialog);
    }

    // Use this instance of the interface to deliver action events
    NoticeDialogListener mListener;

    // Override the Fragment.onAttach() method to instantiate the NoticeDialogListener
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        // Verify that the host activity implements the callback interface
        try {
            // Instantiate the NoticeDialogListener so we can send events to the host
            mListener = (NoticeDialogListener) activity;
        } catch (ClassCastException e) {
            // The activity doesn't implement the interface, throw exception
            throw new ClassCastException(activity.toString()
                    + " must implement NoticeDialogListener");
        }
    }
    ...
}

对话框的宿主 Activity 会通过对话框片段的构造函数创建一个对话框实例,并通过实现的 NoticeDialogListener 界面接收对话框的事件:

public class MainActivity extends FragmentActivity
                          implements NoticeDialogFragment.NoticeDialogListener{
    ...

    public void showNoticeDialog() {
        // Create an instance of the dialog fragment and show it
        DialogFragment dialog = new NoticeDialogFragment();
        dialog.show(getSupportFragmentManager(), "NoticeDialogFragment");
    }

    // The dialog fragment receives a reference to this Activity through the
    // Fragment.onAttach() callback, which it uses to call the following methods
    // defined by the NoticeDialogFragment.NoticeDialogListener interface
    @Override
    public void onDialogPositiveClick(DialogFragment dialog) {
        // User touched the dialog's positive button
        ...
    }

    @Override
    public void onDialogNegativeClick(DialogFragment dialog) {
        // User touched the dialog's negative button
        ...
    }
}
由于宿主 Activity 会实现 NoticeDialogListener—由以上显示的 onAttach() 回调方法强制执行 — 因此对话框片段可以使用界面回调方法向 Activity 传递点击事件:

public class NoticeDialogFragment extends DialogFragment {
    ...

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // Build the dialog and set up the button click handlers
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setMessage(R.string.dialog_fire_missiles)
               .setPositiveButton(R.string.fire, new DialogInterface.OnClickListener() {
                   public void onClick(DialogInterface dialog, int id) {
                       // Send the positive button event back to the host activity
                       mListener.onDialogPositiveClick(NoticeDialogFragment.this);
                   }
               })
               .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
                   public void onClick(DialogInterface dialog, int id) {
                       // Send the negative button event back to the host activity
                       mListener.onDialogNegativeClick(NoticeDialogFragment.this);
                   }
               });
        return builder.create();
    }
}

显示对话框

如果您想显示对话框,请创建一个 DialogFragment 实例并调用 show(),以传递对话框片段的 FragmentManager 和标记名称。

您可以通过从 FragmentActivity 调用 getSupportFragmentManager() 或从 Fragment 调用 getFragmentManager() 来获取 FragmentManager。例如:

public void confirmFireMissiles() {
    DialogFragment newFragment = new FireMissilesDialogFragment();
    newFragment.show(getSupportFragmentManager(), "missiles");
}

第二个参数 “missiles” 是系统用于保存片段状态并在必要时进行恢复的唯一标记名称。 该标记还允许您通过调用 findFragmentByTag() 获取片段的句柄。

全屏显示对话框或将其显示为嵌入式片段

您可能采用以下 UI 设计:您想让一部分 UI 在某些情况下显示为对话框,但在其他情况下全屏显示或显示为嵌入式片段(也许取决于设备使用大屏幕还是小屏幕)。DialogFragment 类便具有这种灵活性,因为它仍然可以充当嵌入式 Fragment。

但在这种情况下,您不能使用 AlertDialog.Builder 或其他 Dialog 对象来构建对话框。如果您想让 DialogFragment 具有嵌入能力,则必须在布局中定义对话框的 UI,然后在 onCreateView() 回调中加载布局。

以下示例 DialogFragment 可以显示为对话框或嵌入式片段(使用名为 purchase_items.xml 的布局):

public class CustomDialogFragment extends DialogFragment {
    /** The system calls this to get the DialogFragment's layout, regardless
        of whether it's being displayed as a dialog or an embedded fragment. */
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        // Inflate the layout to use as dialog or embedded fragment
        return inflater.inflate(R.layout.purchase_items, container, false);
    }

    /** The system calls this only when creating the layout in a dialog. */
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // The only reason you might override this method when using onCreateView() is
        // to modify any dialog characteristics. For example, the dialog includes a
        // title by default, but your custom layout might not need it. So here you can
        // remove the dialog title, but you must call the superclass to get the Dialog.
        Dialog dialog = super.onCreateDialog(savedInstanceState);
        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        return dialog;
    }
}

以下代码可根据屏幕尺寸决定将片段显示为对话框还是全屏 UI:

public void showDialog() {
    FragmentManager fragmentManager = getSupportFragmentManager();
    CustomDialogFragment newFragment = new CustomDialogFragment();

    if (mIsLargeLayout) {
        // The device is using a large layout, so show the fragment as a dialog
        newFragment.show(fragmentManager, "dialog");
    } else {
        // The device is smaller, so show the fragment fullscreen
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        // For a little polish, specify a transition animation
        transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
        // To make it fullscreen, use the 'content' root view as the container
        // for the fragment, which is always the root view for the activity
        transaction.add(android.R.id.content, newFragment)
                   .addToBackStack(null).commit();
    }
}

如需了解有关执行片段事务的详细信息,请参阅片段指南。

在本示例中,mIsLargeLayout 布尔值指定当前设备是否应该使用应用的大布局设计(进而将此片段显示为对话框,而不是全屏显示)。 设置这种布尔值的最佳方法是声明一个布尔资源值,其中包含适用于不同屏幕尺寸的备用资源值。 例如,以下两个版本的布尔资源适用于不同的屏幕尺寸:

res/values/bools.xml
<!-- Default boolean values -->
<resources>
    <bool name="large_layout">false</bool>
</resources>
res/values-large/bools.xml
<!-- Large screen boolean values -->
<resources>
    <bool name="large_layout">true</bool>
</resources>

然后,您可以在 Activity 的 onCreate() 方法执行期间初始化 mIsLargeLayout 值:

boolean mIsLargeLayout;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mIsLargeLayout = getResources().getBoolean(R.bool.large_layout);
}

将 Activity 显示为大屏幕上的对话框

相对于在小屏幕上将对话框显示为全屏 UI,您可以通过在大屏幕上将 Activity 显示为对话框来达到相同的效果。您选择的方法取决于应用设计,但当应用已经针对小屏幕进行设计,而您想要通过将短生存期 Activity 显示为对话框来改善平板电脑体验时,将 Activity 显示为对话框往往很有帮助。

要想仅在大屏幕上将 Activity 显示为对话框,请将 Theme.Holo.DialogWhenLarge 主题应用于 activity 清单文件元素:

<activity android:theme="@android:style/Theme.Holo.DialogWhenLarge" >

如需了解有关通过主题设置 Activity 样式的详细信息,请参阅样式和主题指南。

清除对话框

当用户触摸使用 AlertDialog.Builder 创建的任何操作按钮时,系统会为您清除对话框。

系统还会在用户触摸某个对话框列表项时清除对话框,但列表使用单选按钮或复选框时除外。 否则,您可以通过在 DialogFragment 上调用dismiss() 来手动清除对话框。

如需在对话框消失时执行特定操作,则可以在您的 DialogFragment 中实现 onDismiss() 方法。

您还可以取消对话框。这是一个特殊事件,它表示用户显式离开对话框,而不完成任务。 如果用户按“返回”按钮,触摸对话框区域外部的屏幕,或者您在 Dialog 上显式调用 cancel()(例如,为了响应对话框中的“取消”按钮),就会发生这种情况。

如上例所示,您可以通过在您的 DialogFragment 类中实现onCancel() 来响应取消事件。

注:系统会在每个调用 onCancel() 回调的事件发生时立即调用 onDismiss()。不过,如果您调用 Dialog.dismiss() 或 DialogFragment.dismiss(),系统会调用 onDismiss(),而不会调用 onCancel()。因此,当用户在对话框中按“肯定”按钮,从视图中移除对话框时,您通常应该调用 dismiss()。

我的微信二维码如下,欢迎交流讨论

这里写图片描述

欢迎关注《IT面试题汇总》微信订阅号。每天推送经典面试题和面试心得技巧

微信订阅号二维码如下:

这里写图片描述

作者:u010321471 发表于2016/12/1 22:52:11 原文链接
阅读:108 评论:0 查看评论

美团城市选择源码解析

$
0
0

源码地址:https://github.com/helloworld107/CitySelect

效果图


源码分析

   先从简单的来吧,先说数据,对于一个城市而言名字必须有的,其次因为控件还会有相关的导航字母,所以还需要每个城市的拼音,这样一个城市的实体类就完成了,因为城市数据量庞大,显然装在了一个数据库中,这样我们通过sqlite获取数据和查找也非常方便


数据库放在asset文件下,app运行后我们会以流的形式存到sd卡文件夹下,使用时从内存卡读取到集合中使用,相关管理类代码

public class DBManager {
    private static final String ASSETS_NAME = "china_cities.db";
    private static final String DB_NAME = "china_cities.db";
    private static final String TABLE_NAME = "city";
    private static final String NAME = "name";
    private static final String PINYIN = "pinyin";
    private static final int BUFFER_SIZE = 1024;
    private String DB_PATH;
    private Context mContext;

//    public static DBManager init(){
//        if (mInstance == null){
//            synchronized (DBManager.class){
//                if (mInstance != null){
//                    mInstance = new DBManager();
//                }
//            }
//        }
//        return mInstance;
//    }

    public DBManager(Context context) {
        this.mContext = context;
        DB_PATH = File.separator + "data"
                + Environment.getDataDirectory().getAbsolutePath() + File.separator
                + context.getPackageName() + File.separator + "databases" + File.separator;
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    public void copyDBFile(){
        File dir = new File(DB_PATH);
        if (!dir.exists()){
            dir.mkdirs();
        }
        File dbFile = new File(DB_PATH + DB_NAME);
        if (!dbFile.exists()){
            InputStream is;
            OutputStream os;
            try {
                is = mContext.getResources().getAssets().open(ASSETS_NAME);
                os = new FileOutputStream(dbFile);
                byte[] buffer = new byte[BUFFER_SIZE];
                int length;
                while ((length = is.read(buffer, 0, buffer.length)) > 0){
                    os.write(buffer, 0, length);
                }
                os.flush();
                os.close();
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 读取所有城市
     * @return
     */
    public List<City> getAllCities(){
	//有了数据干什么都soeasy啊
        SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(DB_PATH + DB_NAME, null);
        Cursor cursor = db.rawQuery("select * from " + TABLE_NAME, null);
        List<City> result = new ArrayList<>();
        City city;
        while (cursor.moveToNext()){
            String name = cursor.getString(cursor.getColumnIndex(NAME));
            String pinyin = cursor.getString(cursor.getColumnIndex(PINYIN));
            city = new City(name, pinyin);
            result.add(city);
        }
        cursor.close();
        db.close();
        Collections.sort(result, new CityComparator());
        return result;
    }

    /**
     * 通过名字或者拼音搜索
     * @param keyword
     * @return
     */
    public List<City> searchCity(final String keyword){
        SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(DB_PATH + DB_NAME, null);
        Cursor cursor = db.rawQuery("select * from " + TABLE_NAME +" where name like \"%" + keyword
                + "%\" or pinyin like \"%" + keyword + "%\"", null);
        List<City> result = new ArrayList<>();
        City city;
        while (cursor.moveToNext()){
            String name = cursor.getString(cursor.getColumnIndex(NAME));
            String pinyin = cursor.getString(cursor.getColumnIndex(PINYIN));
            city = new City(name, pinyin);
            result.add(city);
        }
        cursor.close();
        db.close();
        Collections.sort(result, new CityComparator());
        return result;
    }

    /**
     * a-z排序
     */
    private class CityComparator implements Comparator<City>{
        @Override
        public int compare(City lhs, City rhs) {
            String a = lhs.getPinyin().substring(0, 1);
            String b = rhs.getPinyin().substring(0, 1);
            return a.compareTo(b);
        }
    }
}
之后看看搜索布局,显然列表是一个listview,多了一个右侧的导航条,并且点击时中间还会出现中的大方块字母,其实一直存在于总布局中,只不过我们只在点击的时候让它显示,右边的导航条目是个自定义控件,略有难度,上代码,其实就是把26个字母加特殊符号打印了下来,并且设置了点击相应的位置的接口回调

ublic class SideLetterBar extends View {
    private static final String[] b = {"定位", "热门", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
    private int choose = -1;
    private Paint paint = new Paint();
    private boolean showBg = false;
    private OnLetterChangedListener onLetterChangedListener;
    private TextView overlay;

    public SideLetterBar(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public SideLetterBar(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SideLetterBar(Context context) {
        super(context);
    }

    /**
     * 设置悬浮的textview
     * @param overlay
     */
    public void setOverlay(TextView overlay){
        this.overlay = overlay;
    }

    @SuppressWarnings("deprecation")
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (showBg) {
            canvas.drawColor(Color.TRANSPARENT);
        }
        //画出索引字母
        int height = getHeight();
        int width = getWidth();
        int singleHeight = height / b.length;
        for (int i = 0; i < b.length; i++) {
            paint.setTextSize(getResources().getDimension(R.dimen.side_letter_bar_letter_size));
            paint.setColor(getResources().getColor(R.color.gray));
            paint.setAntiAlias(true);
            //如果手指戳到相应位置,选中颜色变深 字母比较小,看的不明显
            if (i == choose) {
                paint.setColor(getResources().getColor(R.color.gray_deep));
                paint.setFakeBoldText(true);  //加粗
            }
            //计算相应字母的距离居中
            float xPos = width / 2 - paint.measureText(b[i]) / 2;
            float yPos = singleHeight * i + singleHeight;
            canvas.drawText(b[i], xPos, yPos, paint);
            paint.reset();
        }

    }

//    设置中间显示的结果
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        final int action = event.getAction();
        final float y = event.getY();
        final int oldChoose = choose;
        final OnLetterChangedListener listener = onLetterChangedListener;
        //相应高度的比例乘以字符数组长度就是我们数组对应角标
        final int c = (int) (y / getHeight() * b.length);

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                showBg = true;
                if (oldChoose != c && listener != null) {
                    if (c >= 0 && c < b.length) {
                        listener.onLetterChanged(b[c]);
                        choose = c;
                        invalidate();
                        if (overlay != null){
                            overlay.setVisibility(VISIBLE);
                            overlay.setText(b[c]);
                        }
                    }
                }

                break;
            case MotionEvent.ACTION_MOVE:
                if (oldChoose != c && listener != null) {
                    if (c >= 0 && c < b.length) {
                        listener.onLetterChanged(b[c]);
                        choose = c;
                        invalidate();
                        if (overlay != null){
                            overlay.setVisibility(VISIBLE);
                            overlay.setText(b[c]);
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                showBg = false;
                choose = -1;
                invalidate();
                if (overlay != null){
                    overlay.setVisibility(GONE);
                }
                break;
        }
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

    public void setOnLetterChangedListener(OnLetterChangedListener onLetterChangedListener) {
        this.onLetterChangedListener = onLetterChangedListener;
    }
//点击事件交给外部类调用
    public interface OnLetterChangedListener {
        void onLetterChanged(String letter);
    }

}
总界面

public class CityPickerActivity extends AppCompatActivity implements View.OnClickListener {
    public static final int REQUEST_CODE_PICK_CITY = 2333;
    public static final String KEY_PICKED_CITY = "picked_city";

    private ListView mListView;
    private ListView mResultListView;
    private SideLetterBar mLetterBar;
    private EditText searchBox;
    private ImageView clearBtn;
    private ImageView backBtn;
    private ViewGroup emptyView;

    private CityListAdapter mCityAdapter;
    private ResultListAdapter mResultAdapter;
    private List<City> mAllCities;
    private DBManager dbManager;

    private AMapLocationClient mLocationClient;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_city_list);

        initData();//从数据拿到城市集合,并且设置城市列表适配器
        initView();//初始化布局,设置相关监听
        initLocation();//定位功能根据自己使用的第三方api来使用,这里不考虑
    }

    private void initLocation() {
        mLocationClient = new AMapLocationClient(this);
        AMapLocationClientOption option = new AMapLocationClientOption();
        option.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy);
        option.setOnceLocation(true);
        mLocationClient.setLocationOption(option);
        mLocationClient.setLocationListener(new AMapLocationListener() {
            @Override
            public void onLocationChanged(AMapLocation aMapLocation) {
                if (aMapLocation != null) {
                    if (aMapLocation.getErrorCode() == 0) {
                        String city = aMapLocation.getCity();
                        String district = aMapLocation.getDistrict();
                        Log.e("onLocationChanged", "city: " + city);
                        Log.e("onLocationChanged", "district: " + district);
                        String location = StringUtils.extractLocation(city, district);
                        mCityAdapter.updateLocateState(LocateState.SUCCESS, location);
                    } else {
                        //定位失败
                        mCityAdapter.updateLocateState(LocateState.FAILED, null);
                    }
                }
            }
        });
        mLocationClient.startLocation();
    }

    private void initData() {
        dbManager = new DBManager(this);
        dbManager.copyDBFile();
        mAllCities = dbManager.getAllCities();
        mCityAdapter = new CityListAdapter(this, mAllCities);
        mCityAdapter.setOnCityClickListener(new CityListAdapter.OnCityClickListener() {
            @Override
            public void onCityClick(String name) {
                back(name);//点击吐司
            }

            @Override
            public void onLocateClick() {
                Log.e("onLocateClick", "重新定位...");
                mCityAdapter.updateLocateState(LocateState.LOCATING, null);
                mLocationClient.startLocation();
            }
        });
	//搜索框用了另外一个列表,更加简单,这个列表跟原来的列表是重叠的,两者根据业务逻辑只显示其中之一
        mResultAdapter = new ResultListAdapter(this, null);
    }

    private void initView() {
        mListView = (ListView) findViewById(R.id.listview_all_city);
        mListView.setAdapter(mCityAdapter);

        TextView overlay = (TextView) findViewById(R.id.tv_letter_overlay);
        mLetterBar = (SideLetterBar) findViewById(R.id.side_letter_bar);
        mLetterBar.setOverlay(overlay);
        mLetterBar.setOnLetterChangedListener(new SideLetterBar.OnLetterChangedListener() {
            @Override
            public void onLetterChanged(String letter) {
		//通过自定义导航的接口回调就控制了列表的选择项
                int position = mCityAdapter.getLetterPosition(letter);
                mListView.setSelection(position);
            }
        });
	//搜索框使用了另外一个列表跟适配器,也非常简单,两者列表位置一样,根据逻辑只能显示其中一个
        searchBox = (EditText) findViewById(R.id.et_search);
        searchBox.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {}

            @Override
            public void afterTextChanged(Editable s) {
                String keyword = s.toString();
                if (TextUtils.isEmpty(keyword)) {
                    clearBtn.setVisibility(View.GONE);
                    emptyView.setVisibility(View.GONE);
                    mResultListView.setVisibility(View.GONE);
                } else {
                    clearBtn.setVisibility(View.VISIBLE);
                    mResultListView.setVisibility(View.VISIBLE);
                    List<City> result = dbManager.searchCity(keyword);
                    if (result == null || result.size() == 0) {
                        emptyView.setVisibility(View.VISIBLE);
                    } else {
                        emptyView.setVisibility(View.GONE);
                        mResultAdapter.changeData(result);
                    }
                }
            }
        });

        emptyView = (ViewGroup) findViewById(R.id.empty_view);
        mResultListView = (ListView) findViewById(R.id.listview_search_result);
        mResultListView.setAdapter(mResultAdapter);
        mResultListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                back(mResultAdapter.getItem(position).getName());
            }
        });

        clearBtn = (ImageView) findViewById(R.id.iv_search_clear);
        backBtn = (ImageView) findViewById(R.id.back);

        clearBtn.setOnClickListener(this);
        backBtn.setOnClickListener(this);
    }

    private void back(String city){
        ToastUtils.showToast(this, "点击的城市:" + city);
//        Intent data = new Intent();
//        data.putExtra(KEY_PICKED_CITY, city);
//        setResult(RESULT_OK, data);
//        finish();
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.iv_search_clear:
                searchBox.setText("");
                clearBtn.setVisibility(View.GONE);
                emptyView.setVisibility(View.GONE);
                mResultListView.setVisibility(View.GONE);
                break;
            case R.id.back:
                finish();
                break;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mLocationClient.stopLocation();
    }
}
看看最初界面最复杂的适配器
public class CityListAdapter extends BaseAdapter {
    private static final int VIEW_TYPE_COUNT = 3;

    private Context mContext;
    private LayoutInflater inflater;
    private List<City> mCities;
    //导航字母,因为每个拼音只有一个,所以我们需要记住每个导航的具体位置,需要键值对集合封装
    private HashMap<String, Integer> letterIndexes;
    //此业务暂时不需要,请无视
    private String[] sections;
    private OnCityClickListener onCityClickListener;
    private int locateState = LocateState.LOCATING;
    private String locatedCity;

    public CityListAdapter(Context mContext, List<City> mCities) {
        this.mContext = mContext;
        this.mCities = mCities;
        this.inflater = LayoutInflater.from(mContext);
        if (mCities == null){
            mCities = new ArrayList<>();
        }
        //强行补了两个数据为了增加类型,热门和定位,对于普通条目无意义
        mCities.add(0, new City("定位", "0"));
        mCities.add(1, new City("热门", "1"));
        int size = mCities.size();
        letterIndexes = new HashMap<>();
        sections = new String[size];
//通过比较两个条目的拼音是否一样即可确定需要几个导航
        for (int index = 0; index < size; index++){
            //当前城市拼音首字母
            String currentLetter = PinyinUtils.getFirstLetter(mCities.get(index).getPinyin());
            //上个首字母,如果不存在设为""
            String previousLetter = index >= 1 ? PinyinUtils.getFirstLetter(mCities.get(index - 1).getPinyin()) : "";
            if (!TextUtils.equals(currentLetter, previousLetter)){
                letterIndexes.put(currentLetter, index);
                sections[index] = currentLetter;
            }
        }
    }

    /**
     * 更新定位状态
     * @param state
     */
    public void updateLocateState(int state, String city){
        this.locateState = state;
        this.locatedCity = city;
        notifyDataSetChanged();
    }

    /**
     * 获取字母索引的位置
     * @param letter
     * @return
     */
    public int getLetterPosition(String letter){
        Integer integer = letterIndexes.get(letter);
        return integer == null ? -1 : integer;
    }

    @Override
    public int getViewTypeCount() {
        return VIEW_TYPE_COUNT;
    }

    @Override
    public int getItemViewType(int position) {
        return position < VIEW_TYPE_COUNT - 1 ? position : VIEW_TYPE_COUNT - 1;
    }

    @Override
    public int getCount() {
        return mCities == null ? 0: mCities.size();
    }

    @Override
    public City getItem(int position) {
        return mCities == null ? null : mCities.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(final int position, View view, ViewGroup parent) {
        CityViewHolder holder;
        int viewType = getItemViewType(position);
        switch (viewType){
            case 0:     //定位
                view = inflater.inflate(R.layout.view_locate_city, parent, false);
                ViewGroup container = (ViewGroup) view.findViewById(R.id.layout_locate);
                TextView state = (TextView) view.findViewById(R.id.tv_located_city);
                switch (locateState){
                    case LocateState.LOCATING:
                        state.setText(mContext.getString(R.string.locating));
                        break;
                    case LocateState.FAILED:
                        state.setText(R.string.located_failed);
                        break;
                    case LocateState.SUCCESS:
                        state.setText(locatedCity);
                        break;
                }
                container.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (locateState == LocateState.FAILED){
                            //重新定位
                            if (onCityClickListener != null){
                                onCityClickListener.onLocateClick();
                            }
                        }else if (locateState == LocateState.SUCCESS){
                            //返回定位城市
                            if (onCityClickListener != null){
                                onCityClickListener.onCityClick(locatedCity);
                            }
                        }
                    }
                });
                break;
            case 1:     //热门城市
                view = inflater.inflate(R.layout.view_hot_city, parent, false);
                WrapHeightGridView gridView = (WrapHeightGridView) view.findViewById(R.id.gridview_hot_city);
                final HotCityGridAdapter hotCityGridAdapter = new HotCityGridAdapter(mContext);
                gridView.setAdapter(hotCityGridAdapter);
                gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                    @Override
                    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                        if (onCityClickListener != null){
                            onCityClickListener.onCityClick(hotCityGridAdapter.getItem(position));
                        }
                    }
                });
                break;
            case 2:     //正常条目
                if (view == null){
                    //默认布局每一个布局都是在导航首字母的,只不过通过判断前一个删除掉了
                    view = inflater.inflate(R.layout.item_city_listview, parent, false);
                    holder = new CityViewHolder();
                    holder.letter = (TextView) view.findViewById(R.id.tv_item_city_listview_letter);
                    holder.name = (TextView) view.findViewById(R.id.tv_item_city_listview_name);
                    view.setTag(holder);
                }else{
                    holder = (CityViewHolder) view.getTag();
                }
                if (position >= 1){
                    final String city = mCities.get(position).getName();
                    holder.name.setText(city);
                    String currentLetter = PinyinUtils.getFirstLetter(mCities.get(position).getPinyin());
                    String previousLetter = position >= 1 ? PinyinUtils.getFirstLetter(mCities.get(position - 1).getPinyin()) : "";
                    //如果跟上一个字母拼音不一样就显示导航字母,否则就删除,大部分都是删除
                    if (!TextUtils.equals(currentLetter, previousLetter)){
                        holder.letter.setVisibility(View.VISIBLE);
                        holder.letter.setText(currentLetter);
                    }else{
                        holder.letter.setVisibility(View.GONE);
                    }
                    holder.name.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            if (onCityClickListener != null){
                                onCityClickListener.onCityClick(city);
                            }
                        }
                    });
                }
                break;
        }
        return view;
    }

    public static class CityViewHolder{
        TextView letter;
        TextView name;
    }

    public void setOnCityClickListener(OnCityClickListener listener){
        this.onCityClickListener = listener;
    }

    public interface OnCityClickListener{
        void onCityClick(String name);
        void onLocateClick();
    }
}










作者:AndroidFlying007 发表于2016/12/1 22:53:26 原文链接
阅读:75 评论:0 查看评论

android 数据解析总结(各种解析)

$
0
0

从事android开发以来,解析过不少数据,都是根据所谓的协议来的。其实,这些协议都是双方约定或者一方约定的数据格式。

1,标准的gga坐标数据解析

例如:$GPGGA,033744,2446.5241,N,12100.1536,E,1,10,0.8,133.4,M,,,,*1F

看看geomap源码是怎么对坐标数据的解析的 NMEA 0183

/*
 * OpenNMEA - A Java library for parsing NMEA 0183 data sentences
 * Copyright (C)2006 Joey Gannon
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

package com.opennmea;

/**
 * An object for storing and comparing geographic latitude and longitude
 * 
 * @author Joey Gannon
 * @version 1.00, 08/11/06
 * @since OpenNMEA v0.1
 */
public class Geocoordinate implements Comparable<Geocoordinate>
{
	private int latdeg,londeg,latmin,lonmin;
	private double latsec,lonsec,lattot,lontot;
	private char mode;
	private boolean valid=true;

	/**
	 * Constructs a Geocoordinate from latitude and longitude strings, with cardinal direction
	 * indicated by separate strings
	 * <p>
	 * This constructor defaults to the decimal minutes output mode.
	 * <p>
	 * If the strings passed to this constructor are malformed, the values will default to
	 * <code>{0.0, 0.0}</code>. 
	 * @param lat Latitude, in the form DDmm[.mmmm]
	 * @param ns North/South ("N" or "S")
	 * @param lon Longitude, in the form DDDmm[.mmmm]
	 * @param ew East/West ("E" or "W")
	 */
	public Geocoordinate(String lat,String ns,String lon,String ew)
	{
		this(lat.equals("")?0.0:(Integer.parseInt(lat.substring(0,2))+Double.parseDouble(lat.substring(2))/60)*(ns.equals("N")?1:-1),lon.equals("")?0.0:(Integer.parseInt(lon.substring(0,3))+Double.parseDouble(lon.substring(3))/60)*(ew.equals("E")?1:-1));
		if((lat.equals(""))||(lon.equals("")))
			valid=false;
		mode='M';
	}

	/**
	 * Constructs a Geocoordinate from floating-point latitude and longitude values, with
	 * cardinal direction indicated by numerical sign
	 * <p>
	 * This constructor defaults to the decimal degrees output mode.
	 * @param lat Latitude, in decimal degrees (south is negative)
	 * @param lon Longitude, in decimal degrees (west is negative)
	 */
	public Geocoordinate(double lat,double lon)
	{
		while(lat<-90)
			lat+=180;
		while(lat>90)
			lat-=180;
		while(lon<=-180)
			lat+=360;
		while(lon>180)
			lat-=360;
		lattot=lat;
		lontot=lon;
		latdeg=(int)lat;
		londeg=(int)lon;
		latmin=(int)(60*(lat-latdeg));
		lonmin=(int)(60*(lon-londeg));
		latsec=60*(60*(lat-latdeg)-latmin);
		lonsec=60*(60*(lon-londeg)-lonmin);
		mode='D';
	}

	/**
	 * Sets the output mode to decimal degrees
	 */
	public void toDegrees()
	{
		mode='D';
	}

	/**
	 * Sets the output mode to degrees and decimal minutes
	 */
	public void toMinutes()
	{
		mode='M';
	}

	/**
	 * Sets the output mode to degrees, minutes, and decimal seconds
	 */
	public void toSeconds()
	{
		mode='S';
	}

	/**
	 * Tells where the current mode applies the fractional part of the latitude
	 * and longitude values
	 * <p>
	 * Possible modes are degrees (<code>'D'</code>), minutes (<code>'M'</code>),
	 * or seconds (<code>'S'</code>).
	 * @return the current mode
	 */
	public char getMode()
	{
		return mode;
	}

	/**
	 * Returns the latitude, formatted according to the current mode
	 * <p>
	 * If the latitude stored by this Geocoordinate was 12.3456, then
	 * the array returned for each mode would be:<br>
	 * Degrees mode: <code>[12.3456]</code><br>
	 * Minutes mode: <code>[12.0, 20.736]</code><br>
	 * Seconds mode: <code>[12.0, 20.0, 44.16]</code>
	 * @return the latitude
	 * @see Geocoordinate#getLatitudeDegrees()
	 */
	public double[] getLatitude()
	{
		double[] array;
		if(mode=='D')
		{
			array=new double[1];
			array[0]=lattot;
		}
		else if(mode=='M')
		{
			array=new double[2];
			array[0]=(double)latdeg;
			array[1]=latmin+latsec/60;
		}
		else
		{
			array=new double[3];
			array[0]=(double)latdeg;
			array[1]=(double)latmin;
			array[2]=latsec;
		}
		return array;
	}

	/**
	 * Returns the latitude in decimal degrees
	 * <p>
	 * This is equivalent to <code>getLatitude()[0]</code> in Degrees mode.
	 * @return the latitude
	 * @see Geocoordinate#getLatitude()
	 */
	public double getLatitudeDegrees()
	{
		return lattot; 
	}

	/**
	 * Returns the longitude, formatted according to the current mode
	 * <p>
	 * If the longitude stored by this Geocoordinate was 12.3456, then
	 * the array returned for each mode would be:<br>
	 * Degrees mode: <code>[12.3456]</code><br>
	 * Minutes mode: <code>[12.0, 20.736]</code><br>
	 * Seconds mode: <code>[12.0, 20.0, 44.16]</code>
	 * @return the longitude
	 * @see Geocoordinate#getLongitudeDegrees()
	 */
	public double[] getLongitude()
	{
		double[] array;
		if(mode=='D')
		{
			array=new double[1];
			array[0]=lontot;
		}
		else if(mode=='M')
		{
			array=new double[2];
			array[0]=(double)londeg;
			array[1]=lonmin+lonsec/60;
		}
		else
		{
			array=new double[3];
			array[0]=(double)londeg;
			array[1]=(double)lonmin;
			array[2]=lonsec;
		}
		return array;
	}

	/**
	 * Returns the longitude in decimal degrees
	 * <p>
	 * This is equivalent to <code>getLongitude()[0]</code> in Degrees mode.
	 * @return the longitude
	 * @see Geocoordinate#getLongitude()
	 */
	public double getLongitudeDegrees()
	{
		return lontot;
	}
	
	/**
	 * Determines if a coordinate is valid
	 * <p>
	 * An invalid coordinate could be generated by the constructor if
	 * passed invalid strings. This method is helpful in filtering out
	 * such coordinates.
	 * @return true if the coordinate is valid, false otherwise
	 */
	public boolean isValid()
	{
		return valid;
	}

	/**
	 * Returns the distance between two Geocoordinates on Earth, in meters
	 * <p>
	 * In order to fulfill the contract of <code>Comparable</code>, this method returns
	 * a negative distance if <code>this</code> is farther south than <code>g</code>. If
	 * <code>this</code> and <code>g</code> are at the same latitude, then this method
	 * returns a negative distance if <code>this</code> is farther west than <code>g</code>.
	 * @return the distance between two Geocoordinates
	 * @see java.lang.Comparable#compareTo(java.lang.Object)
	 */
	public int compareTo(Geocoordinate g)
	{
		return (int)(Math.toDegrees(Math.acos(Math.sin(Math.toRadians(lattot))*Math.sin(Math.toRadians(g.getLatitudeDegrees()))+Math.cos(Math.toRadians(lattot))*Math.cos(Math.toRadians(g.getLatitudeDegrees()))*Math.cos(Math.toRadians(lontot-g.getLongitudeDegrees()))))*111189.577)*(lattot==g.getLatitudeDegrees()?(lontot>g.getLongitudeDegrees()?1:-1):(lattot>g.getLatitudeDegrees()?1:-1));
	}

	/**
	 * Determines if this Geocoordinate is equal to another object
	 * <p>
	 * A comparison only makes sense if the object being compared is also a Geocoordinate,
	 * so this method returns false if the parameter is not an instance of Geocoordinate.
	 * @return true if the objects are equal, false otherwise
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object o)
	{
		if(!(o instanceof Geocoordinate))
			return false;
		return ((lattot==((Geocoordinate)o).getLatitudeDegrees())&&(lontot==((Geocoordinate)o).getLongitudeDegrees()));
	}

	/**
	 * Returns a string representation of the coordinate
	 * @return a string representation of the coordinate
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString()
	{
		if(mode=='D')
			return (latdeg+latmin/60)+"бу"+' '+(londeg+lonmin/60)+"бу";
		else if(mode=='M')
			return latdeg+"бу"+Math.abs(latmin)+'\''+' '+londeg+"бу"+Math.abs(lonmin)+'\'';
		else
			return latdeg+"бу"+Math.abs(latmin)+'\''+Math.abs(latsec)+'\"'+' '+londeg+"бу"+Math.abs(lonmin)+'\''+Math.abs(lonsec)+'\"';
	}
}

其中经纬度都是有正负,以及南北半球,东西半球之分的

上面的代码主要就是

纬度解析:lat.equals("")?0.0:(Integer.parseInt(lat.substring(0,2))+Double.parseDouble(lat.substring(2))/60)*(ns.equals("N")?1:-1)

经度解析:lon.equals("")?0.0:(Integer.parseInt(lon.substring(0,3))+Double.parseDouble(lon.substring(3))/60)*(ew.equals("E")?1:-1)

关于其他的mnea 0183经纬度解析可参照:GPS NMEA-0183标准详解(常用的精度以及经纬度坐标)

2,数据解析

数据解析,收到的数据如下
数据分析,前面的006是没什么用的。Ed开始,后面跟着一个空格,或者是回车换行。
1.811后面也是回车换行。

006Ed 0010,0,0.000,0.0,-99998.446,-99998.102,1.811

解出来需要的数据如下:-99998.446
-99998.102
1.811

代码解析:

		private void totalStationData(int nLength, byte[] data){
			try {
				String dyString1 = new String(data, 0, nLength);
				if (dyString1.contains("006\r\n")) {
					return;
				}
					strData = strData + dyString1;
					if (strData.contains("Ed" )&& strData.substring(strData.length()-2,strData.length()).contains("\r\n")&& strData.length()>15) {
						String[] srcData = strData.split("Ed");
						if (srcData[1] != null && srcData[1].length() >=20) {
							Log.i("Show", strData);
							
							String[] srcStrings =  srcData[1].split(",");
							if (srcStrings != null && srcStrings.length >= 5) {
								if (srcStrings[4].substring(0, 1).equals("-")) {
									mdTotalStationCoord[0] = -Double.valueOf(srcStrings[4].substring(1,srcStrings[4].length()));
								}else {
									mdTotalStationCoord[0] = Double.valueOf(srcStrings[4]);
								}
								Log.i("Show",String.valueOf(mdTotalStationCoord[0]));
								
								if (srcStrings[5].substring(0, 1).equals("-")) {
									mdTotalStationCoord[1] = -Double.valueOf(srcStrings[5].substring(1,srcStrings[5].length()));
								}else {
									mdTotalStationCoord[1] = Double.valueOf(srcStrings[5]);
								}
								Log.i("Show", String.valueOf(mdTotalStationCoord[1]));
								
								if (srcStrings[6].substring(0, 1).equals("-")) {
									mdTotalStationCoord[2] = -Double.valueOf(srcStrings[6].substring(1,6));
								}else {
									mdTotalStationCoord[2] = Double.valueOf(srcStrings[6].substring(0, 5));
								}
								
								Log.i("Show", String.valueOf(mdTotalStationCoord[2]));
								strData = "";
							}	
						}
				}				
			} catch (Exception e) {
				strData = "";
			}
		}

需要注意地方是,数据接收时,是断断续续从缓冲区取出来,不是一次性的。可能间隔就是几百毫秒。
这种时候,需要特别了解每种情况接收到最后的数据时什么样。找出每条完整数据的共性。

抓住主要特点,想到各种情况。
会遇到很多很坑的数据解析
这里只提供一种参考思路。

3,物联网一般数据解析

跟硬件那边传输数据,一般都是十六进制传输的数据,所以一般先解析出来十六进制的字符串后者十六进制的字符数组

收到原始数据:

byte[] buffer = new byte[] {104, 56, 56, 104, 0, 114, 120, 85, 52, 18, 67, 35, 1, 7, 0, 0, 0, 0, 12, 19, 120, 86, 52, 18, 12,

59, 120, 52, 18, 12, 38, 120, 86, 52, 18, 11, 89, 69, 35, 0, 2, -3, 23, 0, 0, 22};

	/**
	 * byte数组转换成16进制字符数组
	 * 
	 * @param src
	 * @return
	 */
	public static String[] bytesToHexStrings(byte[] src) {
		if (src == null || src.length <= 0) {
			return null;
		}

		String[] str = new String[src.length];

		for (int i = 0; i < src.length; i++) {
			str[i] = String.format("%02X", src[i]);
		}
		
		return str;
	}


然后就是根据不同位数提取有用的数据

			//总流量
			if (strResult.length != 0) {
				if (strResult[19].equalsIgnoreCase("0C") && strResult[20].equalsIgnoreCase("13")) {
					String strWater = strResult[24] + strResult[23] + strResult[22] + strResult[21];
					double dWater = Double.valueOf(strWater);
					mtextViewShow1.setText(String.valueOf(dWater/1000) + "m³");
				}

				//流速
				if (strResult[25].equalsIgnoreCase("0C") && strResult[26].equalsIgnoreCase("3B")) {
					String strFlow = strResult[30]+strResult[29]+strResult[28]+strResult[27];
					double dFlow = Double.valueOf(strFlow);
					
					mtextViewShow2.setText(String.valueOf(dFlow/1000) + "m³/h");					
				}

				//温度
				if (strResult[37].equalsIgnoreCase("0B") && strResult[38].equalsIgnoreCase("59")) {
					String strTemp = strResult[41]+strResult[40]+strResult[39];
					double dTemp = Double.valueOf(strTemp);
					
					mtextViewShow5.setText(String.format("%.2f", dTemp/100) + "℃");
				}

				if (strResult[42].equalsIgnoreCase("02")
						&& strResult[43].equalsIgnoreCase("FD") && strResult[44].equalsIgnoreCase("17")) {
					String hexResult = StringUtil.reverseString(StringUtil.hexStringToBinary(strResult[45]));
					
					if(hexResult == null){
						return;
					}
					
					String str = hexResult.substring(5, 6);
					
					//0表示管道正常           倒数第三位
					if (str.equalsIgnoreCase("0")) {
						mtextViewShow3.setText("Normal");
					}
					//1表示空管;
					else if (str.equalsIgnoreCase("1")) {
						mtextViewShow3.setText("Empty");
					}
					
					// 最后两位
					String str1 = hexResult.substring(6, 8);
					//01 表示静止
					if (str1.equalsIgnoreCase("01") || str1.equalsIgnoreCase("11")) {
						mtextViewShow4.setText("Flowing");
					}
					//00 表示启动
					if (str1.equalsIgnoreCase("00")) {
						mtextViewShow4.setText("Start");
					}
					//10,11表示流动
					if (str1.equalsIgnoreCase("10")) {
						mtextViewShow4.setText("Stagnant");
					}
				}
			}

都是根据约定提取数据啦

最后附上一个常用进制转换工具类

package com.aufw.util;

import java.io.ByteArrayOutputStream;

public class StringUtil {
    //  十六进制的字符串转换成byte数组	
	public static byte[] HexCommandtoByte(byte[] data) {
		if (data == null) {
			return null;
		}
		int nLength = data.length; 
		
		String strTemString = new String(data, 0, nLength);
		String[] strings = strTemString.split(" ");
		nLength = strings.length;
		data = new byte[nLength];			
		for (int i = 0; i < nLength; i++) {
			if (strings[i].length() != 2) {
				data[i] = 00;
				continue;
			}
			try {
				data[i] = (byte)Integer.parseInt(strings[i], 16);
			} catch (Exception e) {
				data[i] = 00;
				continue;
			}
		}
	
		return data;
	}
	
	/**
	 * byte数组转换成16进制字符数组
	 * 
	 * @param src
	 * @return
	 */
	public static String[] bytesToHexStrings(byte[] src) {
		if (src == null || src.length <= 0) {
			return null;
		}

		String[] str = new String[src.length];

		for (int i = 0; i < src.length; i++) {
			str[i] = String.format("%02X", src[i]);
		}
		
		return str;
	}
	
    /**
     * 十六进制字符串装十进制
     * 
     * @param hex
     *            十六进制字符串
     * @return 十进制数值
     */
    public static int hexStringToAlgorism(String hex) {
    	if (hex == null) {
    		return 0;
		}
        hex = hex.toUpperCase();
        int max = hex.length();
        int result = 0;
        for (int i = max; i > 0; i--) {
            char c = hex.charAt(i - 1);
            int algorism = 0;
            if (c >= '0' && c <= '9') {
                algorism = c - '0';
            } else {
                algorism = c - 55;
            }
            result += Math.pow(16, max - i) * algorism;
        }
        return result;
    }
	
	// 十六进制的字符串转化为String
	private static String hexString = "0123456789ABCDEF";
	public static String decode(String bytes) {
		ByteArrayOutputStream baos = new ByteArrayOutputStream(
				bytes.length() / 2);
		// 将每2位16进制整数组装成一个字节
		for (int i = 0; i < bytes.length(); i += 2)
			baos.write((hexString.indexOf(bytes.charAt(i)) << 4 | hexString
					.indexOf(bytes.charAt(i + 1))));
		return new String(baos.toByteArray());
	}
	
	  /**
     * 十六转二进制
     * 
     * @param hex
     *            十六进制字符串
     * @return 二进制字符串
     */
    public static String hexStringToBinary(String hex) {
       	if (hex == null) {
    		return null;
		}
        hex = hex.toUpperCase();
        String result = "";
        int max = hex.length();
        for (int i = 0; i < max; i++) {
            char c = hex.charAt(i);
            switch (c) {
            case '0':
                result += "0000";
                break;
            case '1':
                result += "0001";
                break;
            case '2':
                result += "0010";
                break;
            case '3':
                result += "0011";
                break;
            case '4':
                result += "0100";
                break;
            case '5':
                result += "0101";
                break;
            case '6':
                result += "0110";
                break;
            case '7':
                result += "0111";
                break;
            case '8':
                result += "1000";
                break;
            case '9':
                result += "1001";
                break;
            case 'A':
                result += "1010";
                break;
            case 'B':
                result += "1011";
                break;
            case 'C':
                result += "1100";
                break;
            case 'D':
                result += "1101";
                break;
            case 'E':
                result += "1110";
                break;
            case 'F':
                result += "1111";
                break;
            }
        }
        return result;
    }
    
    /** 
     * 倒置字符串 
     *  
     * @param str 
     * @return 
     */  
    public static String reverseString(String str)  
    {  
        char[] arr=str.toCharArray();  
        int middle = arr.length>>1;//EQ length/2  
        int limit = arr.length-1;  
        for (int i = 0; i < middle; i++) {  
            char tmp = arr[i];  
            arr[i]=arr[limit-i];  
            arr[limit-i]=tmp;  
        }  
        return new String(arr);  
    } 
}

关于物联网的解析数据可查看这个

android byte字节数组转换十六进制字符串(物联网开发总结)


作者:qq_16064871 发表于2016/12/1 23:16:02 原文链接
阅读:54 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>