月度归档: 2021 年 7 月

苹果应用内支付(iOS IAP)的流程与常用攻击方式

苹果应用内支付(iOS IAP)的流程与常用攻击方式

常见支付流程

iap(in app purchase)指苹果应用内支付, 目前主要有两种方式。

  1. 1. 客户端直接verify苹果的receipt 如果verify成功 自行发放商品
  2. 2. 客户端将receipt传给server,由server进行验证并发放商品

按照安全性原则, 客户端的所有信息都是不可信的,而且支付是业务中的核心模块,所以应该选择第二种。

下面简要介绍下,第二种方式的简单流程。

  1. 1. 客户端支付成功,拿到receipt
  2. 2. 客户端将receipt传到服务端
  3. 3. 服务端去apple验证receipt 如果验证成功 就发放receipt中的商品

支付安全性

作为支付,安全性是*位的,下面简要分析一下常用的攻击手段。

  1. 劫持apple server攻击 => 通过dns污染,让客户端支付走到假的apple_server,并返回验证成功的response。 这个主要针对支付方式一 如果是支付方式二 就无效。
  2. 重复验证攻击 => 一个receipt重复使用多次
  3. 跨app攻击 => 别的app的receipt用到我们app中来
  4. 换价格攻击 => 低价商品代替高价商品
  5. 歧义攻击 => iap支付之前的status=0表示verify成功 而现在变为status=0只能表示receipt合法 具体支付详情需要通过in_app字段决定 For iOS 6 style transaction receipts, the status code reflects the status of the specific transaction’s receipt.
  6. 中间人攻击 => 伪造apple_server,如果用户支付就将

劫持apple server攻击

通过dns污染,让客户端通过假的apple_server进行verify,从而认为自己支付成功。这个主要针对**支付方式一**,如果是支付方式二,就没效果了。常见的iap hack软件@iAPFree @iAP Cracker 就是用的类似原理。

重复验证攻击

  1. 因为同一个receipt,如果*次验证成功,那么之后每次验证都会成功。如果服务端没有判重机制,就会导致一个receipt被当做多次充值处理。
  2. 为了预防这种情况,我们可以将receipt做一次md5得到receipt_md5, 每次发送充值请求的时候就按照receipt_md5判重,如果重复就停止商品发放。

跨app攻击

  1. 通过在别的app中拿到receipt,然后发送到我们app中。因为这个receipt是合法的而且apple不会验证请求的源,所以这个receipt是可以验证通过的。
  2. 对于这种情况,我们可以判断apple verify的返回值apple_callback_data中对应的bundle_id和我们app的bundle_id是否一样来进行验证。

换价格攻击

  1. 在同一个app中,用低价商品的receipt伪造购买高价商品。这时候bundle_id和我们app的bundle_id是一致的。
  2. 针对这种情况, 我们可以从apple verify的返回值apple_callback_data中拿到对应的product_id, 并按照product_id来进行充值。 **不要信任客户端的product_id**

歧义攻击

  1. 在iOS6的时候,status=0表示此次支付成功,而现在变为status=0只表示receipt**整体上**合法。
  2. 所以,对iOS7即使是一个过期订单,也会返回status=0,如果还按照iOS6的逻辑处理,就会导致假充值。针对iOS7,我们应该不只通过status,还要通过in_app中的内容,来决定如何发放商品。
  3. “`
  4. For iOS 6 style transaction receipts, the status code reflects the status of the specific transaction’s receipt.
  5. For iOS 7 style app receipts, the status code is reflects the status of the app receipt as a whole. For example, if you send a valid app receipt that contains an expired subscription, the response is 0 because the receipt as a whole is valid.

中间人攻击

  1. 伪造apple server,将我们的支付请求转发到真的apple_server,拿到合法的receipt,并弄个假的receipt给客户端。这样就拿到一个合法的凭证。利用这个合法的receipt,伪造别人充值的请求,从而达到帮别人充值的目的。
  2. 针对中间人攻击,*重要的是保证a用户的支付receipt,不能被b用户使用。但是apple为了保护隐私,receipt中没有任何用户的个人信息,这就需要我们自己来保证。目前我们用加密的手段来做这个保证。

iOS支付的详细流程

  1. 客户端拿到apple的receipt 并发送到server
  2. server拿到这个receipt,向苹果验证得到apple_callback_data
  3. 如果apple_callback_data的status是21007,说明是沙盒模式(不用花钱就可以购买) 要根据具体需求判断处理逻辑,需要注意的是,ios的审核在支付的时候就采用的沙盒模式。
  4. 如果apple_callback_data的status是0,就要从apple_callback_data[‘receipt’][‘in_app’]这个list中拿到所有的记录,每一个进行充值。然后记录transaction_id和original_transaction_id来防止同一个transaction被重复使用。

    https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/Restoring.html

    https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1 => Original Transaction Identifier

  5. 返回所有充值成功和重复的transaction_id, 有client来complete transaction

summary

支付作为核心模块,除了技术上的保证,商务也应该每周进行一次对账。如果发现apple上的收入和服务端记录的收入有比较大的差距,就应该抓紧查看原因。

*后给出一个apple_callback_data的例子

  1. {
  2. “status”: 0,
  3. “environment”: “Production”,
  4. “receipt”: {
  5. “download_id”: 75017873837267,
  6. “adam_id”: 1149703708,
  7. “request_date”: “2017-01-13 06:57:20 Etc/GMT”,
  8. “app_item_id”: 1149703708,
  9. “original_purchase_date_pst”: “2016-11-17 18:57:09 America/Los_Angeles”,
  10. “version_external_identifier”: 820252187,
  11. “receipt_creation_date”: “2017-01-13 05:04:52 Etc/GMT”,
  12. “in_app”: [
  13. {
  14. “is_trial_period”: “false”,
  15. “purchase_date_pst”: “2017-01-12 21:04:52 America/Los_Angeles”,
  16. “original_purchase_date_pst”: “2017-01-12 21:04:52 America/Los_Angeles”,
  17. “product_id”: “com.lucky917.live.gold.1.555”,
  18. “original_transaction_id”: “350000191094279”,
  19. “original_purchase_date”: “2017-01-13 05:04:52 Etc/GMT”,
  20. “original_purchase_date_ms”: “1484283892000”,
  21. “purchase_date”: “2017-01-13 05:04:52 Etc/GMT”,
  22. “purchase_date_ms”: “1484283892000”,
  23. “transaction_id”: “350000191094279”,
  24. “quantity”: “1”
  25. }
  26. ],
  27. “original_purchase_date_ms”: “1479437829000”,
  28. “original_application_version”: “26”,
  29. “original_purchase_date”: “2016-11-18 02:57:09 Etc/GMT”,
  30. “request_date_ms”: “1484290640800”,
  31. “bundle_id”: “com.lucky917.ios.Live”,
  32. “receipt_creation_date_pst”: “2017-01-12 21:04:52 America/Los_Angeles”,
  33. “application_version”: “32”,
  34. “request_date_pst”: “2017-01-12 22:57:20 America/Los_Angeles”,
  35. “receipt_creation_date_ms”: “1484283892000”,
  36. “receipt_type”: “Production”
  37. }

ios 苹果支付(IAP)

一.简介
什么是IAP,即in-app-purchase。(虚拟商品如:课程、视频、音乐等数字产品只能走apple 的内购),苹果要扣除30%的销售额,再扣除一点相关的交易税,用户到手将不到7成。

官方参考文档

内购流程
1.获取内购列表(从App内读取或从自己服务器读取)
2.App Store请求可用的内购列表
3.向用户展示内购列表
4.用户选择了内购列表,再发个购买请求,收到购买完成的回调(购买完成后会把钱打给申请内购的银行卡内)
5.购买流程结束后, 向服务器发起验证凭证以及支付结果的请求
6.自己的服务器将支付结果信息返回给前端并发放虚拟产品
7.服务端的工作比较简单,分4步:
7.1.接收ios端发过来的购买凭证。
7.2.判断凭证是否已经存在或验证过,然后存储该凭证。
7.3.将该凭证发送到苹果的服务器验证,并将验证结果返回给客户端。
7.4.如果需要,修改用户相应的会员权限。
7.5.考虑到网络异常情况,服务器的验证应该是一个可恢复的队列,如果网络失败了,应该进行重试。
简单来说就是将该购买凭证用Base64编码,然后POST给苹果的验证服务器,苹果将验证结果以JSON形式返回。

二、流程
1.登录苹果开发者网站新建一个App应用:

%title插图%num
2.填写协议、税务、银行信息:

%title插图%num
3.创建内购商品:
(1).商品名称根据你的消费道具的实际意义来说明,比如“100颗宝石”,“100金币”等。

(2).产品ID是比较重要的,由项目自定义,只要唯一即可,我一般都是用App的bundleID加一个后缀来表示,这样既跟项目关联又具有唯一性。

(3).价格等级的话“查看价格表”中有对应的说明,可以对照着表中每个国家的货币价格与等级来选择。

%title插图%num

4.添加沙盒测试者
沙盒测试环境下苹果不会抽成,购买成功后直接返回商品。

%title插图%num

appid 需要时从未与apple id 从未关联过的账号
appid 可以是任何有效的邮箱地址

%title插图%num

5.内购代码封装IapManager.swift:

//
// IapManager.swift
// tai_chi
//
// Created by vincent on 2019/10/9.
// Copyright © 2019 vincent. All rights reserved.
//

import Foundation
import StoreKit

//内购协议
@objc protocol IapManagerProtocol {

//商品请求结果回调
func productsResponse(_ response:SKProductsResponse?,error:Error?)

//交易成功回调
func completedTransaction(_ transaction:SKPaymentTransaction)

//交易失败回调
@objc optional func transactionFail(transaction:SKPaymentTransaction)

}

//内购管理
class IapManager : NSObject,SKProductsRequestDelegate,SKPaymentTransactionObserver {

private static var instance:IapManager?

var delegate:IapManagerProtocol?

var requestResponse:SKProductsResponse?

var requestErr:Error?

override init() {

super.init()
SKPaymentQueue.default().add(self)

}

//静态方法
static func shared() -> IapManager{

if instance == nil {
instance = IapManager()
}
return instance!
}

//判断app 是否允许apple pay
func canPayments() -> Bool{
return SKPaymentQueue.canMakePayments()
}

//请求商品
//productIds 内购商品id 集合
func requestProducts(productIds:Set<String>){
let request = SKProductsRequest(productIdentifiers: productIds)
request.delegate = self
request.start()

}

//购买商品
func addPayment(_ payment:SKPayment){
SKPaymentQueue.default().add(payment)
}

//监听商品返回信息,然后使用返回的商品信息发起购买请求
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print(“————–收到产品反馈消息———————“)
self.requestResponse = response

}

func request(_ request: SKRequest, didFailWithError error: Error) {
print(“————–收到产品反馈错误消息———————“)
print(error)
self.requestErr = error

}

func requestDidFinish(_ request: SKRequest) {
print(“————–反馈消息结束———————“)
self.delegate?.productsResponse(self.requestResponse, error: self.requestErr)
}

//获取内购成功后apple server 返回给客户端的数据
func receiptData() -> NSData? {

let url = Bundle.main.appStoreReceiptURL
var data:NSData? = nil

if url != nil {
data = NSData(contentsOf: url!)

}
return data
}

//监听购买结果
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {

for transaction in transactions{

if transaction.transactionState == .purchasing {
print(“————–updatedTransactions———————purchasing”)

}
else if transaction.transactionState == .purchased {
print(“————–updatedTransactions———————purchased”)
SKPaymentQueue.default().finishTransaction(transaction)
self.delegate?.completedTransaction(transaction)

}
else if transaction.transactionState == .failed {
print(“————–updatedTransactions———————failed”)
SKPaymentQueue.default().finishTransaction(transaction)
self.delegate?.transactionFail?(transaction: transaction)
}
else if transaction.transactionState == .restored {
print(“————–updatedTransactions———————restored”)
SKPaymentQueue.default().finishTransaction(transaction)

}
else if transaction.transactionState == .deferred {
print(“————–updatedTransactions———————deferred”)

}
}

}

deinit {
SKPaymentQueue.default().remove(self)
}
}

6.交易相关代码:

ViewController

func completedTransaction(_ transaction: SKPaymentTransaction) {

let data = IapManager.shared().receiptData()
if data != nil {
//获取交易成功凭证
let base64Str = (data! as NSData).base64EncodedString()

//本地记录充值成功 以防后台请求失败时下次进入app 时再网络请求
UserDefaults.standard.set(base64Str, forKey: “iap_transaction_key”)

//网络通知服务器和苹果服务器验证是否交易成功 并给相关账号充值

}

}

AppDelegate 检测是否有漏单请求,并把漏单通知服务器

//用户内购成功后需要通知服务器,此过程可能网络请求失败
//检测是否有充值还未提交给服务器
if let pams = UserDefaults.standard.dictionary(forKey:”iap_transaction_key” ) {

//网络通知服务器

}

三.测试:
1.沙盒环境测试appStore内购流程的时候,请使用没越狱的设备。

2.请务必使用真机来测试,一切以真机为准。

3.项目的Bundle identifier需要与您申请AppID时填写的bundleID一致,不然会无法请求到商品信息。

4.退成App Store 账号:

%title插图%num

测试截屏如下:

%title插图%num

%title插图%num

bat脚本自动上传文件到Linux FTP服务器上

环境说明:

在wind7环境下,通过bat脚本将指定的文件上传到远程的Linux FTP服务器上。

上传文件目录:F:\crawlerServer

上传脚本:ftp-put.bat

上传脚本配置文件:ftp.cfg

Linux FTP服务器:192.168.13.21

1.ftp-put.bat脚本:

@echo off

set /p upload=请选择是否将打包后的文件上传到服务器中?(Y/N):

if %upload%==Y (
echo 开始上传文件……
ftp -n -s:”ftp.cfg” >> ftp1.log
echo 上传文件结束….
)
2.ftp.cfg配置文件:

此文件其实是一个ftp操作命令文件,一行就表示一个命令,如下所示:

open 192.168.13.21 #远程FTP服务器IP
user
liuzx #登录用户名
Password1 #登录密码
bin #传输类型 二进制传输:bin Ascii传输:ascii
cd public/docs/deploy_package/winmarket #切换远程FTP的目录,用于存放上传的文件,请确保上面的用户对该目录有写权限
put crawlerServer_0.0.0.1.tar.gz #上传文件
bye #结束与远程FTP服务器的连接

3.运行ftp-put.bat脚本:
效果如下:
%title插图%num

4.查看ftp1.log日志文件,如下是部分日志信息:

230 Login successful.
ftp> bin
200 Switching to Binary mode.
ftp> cd public/docs/deploy_package/winmarket
250 Directory successfully changed.
ftp> put crawlerServer_0.0.0.1.tar.gz
200 PORT command successful. Consider using PASV.
150 Ok to send data.
226 Transfer complete.
ftp> 0.7710713.90bye
221 Goodbye.

5.如果对上面配置文件中的目录没有写权限,日志信息如下:
ftp> cd public/docs/deploy_package/winmarket
250 Directory successfully changed.
ftp> put crawlerServer_0.0.0.1.tar.gz
200 PORT command successful. Consider using PASV.
553 Could not create file.

ftp> bye
221 Goodbye.

上传文件到linux服务器bat脚本

新建bat文件

@echo off
:: 设置颜色
color b0
echo ‘上传中…’
:: 打印空行
echo.

:: pscp配置参数
:: -p 拷贝文件的时候保留源文件建立的时间。
:: -v 拷贝文件时,显示提示信息。
:: -pw 指定密码
:: -r 拷贝目录
:: 多文件 直接空格即可

:: 上传 ./index.html目录到/mnt/web目录下面
pscp -v -r -p -pw 123456 ./index.html root@192.168.1.10:/mnt/web

pause

pscp 是用来上传文件的,window默认是没有这个的,需要下载exe,并放到C:\WINDOWS\system32 目录

远程服务器上传下载文件及执行脚本(bat putty )

1.bat
1.1以管理员权限运行
%1 mshta vbscript:CreateObject(“Shell.Application”).ShellExecute(“cmd.exe”,”/c %~s0 ::”,””,”runas”,1)(window.close)&&exit cd /d “%~dp0″
1.2设置变量
title json转换脚本
set File_Path=%~dp0
set ip=”192.168.1.101″
set pw=”123456”
1.3检查网络
echo —检查远程服务器网络,请等待—
ping %ip% -n 2 > ping.txt
findstr “TTL” ping.txt
if %errorlevel% == 0 (
echo —network is ok—
goto ok
)
if %errorlevel% == 1 (
echo —网络连接失败,请检查与服务器网络—
pause
ping.txt
del /f /s /q ping.txt
exit
)
1.4解压压缩包
解压出json格式文件

“C:\Program Files\WinRAR\WinRAR.exe” x -ad *.zip *.json
2.putty
2.1通过putty上传文件
pscp.exe -P 22 -pw %pw% -l root %File_Path%poi.json root@%ip%:/opt/
pscp.exe -P <端口> -pw <密码> 文件 root@<ip>:<存放路径>

2.2远程执行脚本
plink.exe -l root -P 22 -pw %pw% -ssh -no-antispoof root@%ip% cd /opt/;sh switch.sh
2.3回传文件
pscp.exe -P 22 -l root -pw %pw% root@%ip%:/opt/cms.json %File_Path%
3.脚本
%1 mshta vbscript:CreateObject(“Shell.Application”).ShellExecute(“cmd.exe”,”/c %~s0 ::”,””,”runas”,1)(window.close)&&exit cd /d “%~dp0″
@echo off
title json转换脚本
set File_Path=%~dp0
set ip=”192.168.1.101″
set pw=”123456”
cd %~dp0
echo —检查远程服务器网络,请等待—
ping %ip% -n 2 > ping.txt
findstr “TTL” ping.txt
if %errorlevel% == 0 (
echo —network is ok—
goto ok
)
if %errorlevel% == 1 (
echo —网络连接失败,请检查与服务器网络—
pause
ping.txt
del /f /s /q ping.txt
exit
)
:ok
echo —开始解压—
echo —获取压缩包名称—
for /f “delims=” %%j in (‘dir /b/a-d/oN *.zip*’) do set name=%%~nj
echo 解压地图名为 %name%
echo ——-开始检查是否已存在地图文件—–
if exist %name% (
echo —存在—
echo —删除—
rd /s /q %name%
)

echo ——-开始检查是否已存在cms.json—–
if exist cms.json (
echo —存在—
echo —删除—
del /f /s /q cms.json
)

“C:\Program Files\WinRAR\WinRAR.exe” x -ad *.zip *.json
move %name%\semantics_poi_layer\poi.json

echo —删除地图文件夹—
rd /s /q %name%

echo —上传Json文件至服务器—
pscp.exe -P 22 -pw %pw% -l root %File_Path%poi.json root@%ip%:/opt/
echo —远程执行脚本—
plink.exe -l root -P 22 -pw %pw% -ssh -no-antispoof root@%ip% cd /opt/;sh switch.sh
echo —回传json文件—
pscp.exe -P 22 -l root -pw %pw% root@%ip%:/opt/cms.json %File_Path%
del /f /s /q poi.json
del /f /s /q ping.txt
echo 完成转换,按任意键退出
pause
cms.json

cmd远程连接上传下载文件

远程连接上传下载文件问题

上传文件
put filename

下载文件
get filename

下载文件夹
首先在远程新建一个文件(命名和本地相同) mkdir hello_python 并且cd到这个文件

%title插图%num

接着切换到本机要上传的文件目录(我的这个是在桌面) lcd C:\Users\xiao\Desktop
上传文件夹 put -r hello_python
ls 显示上传成功

%title插图%num

下载文件夹
文件夹一般不直接下载,可以先cd 到这个文件夹,然后get文件

cmd常用命令

get 获取、下载远程文件
put 远程上传文件
mget 一次性批量下载远程文件
mput 远程上传批量文件
ls 显示目录中文件名字的列表
rm 删除文件
mv 重命名文件或将它移到新的目录
cd 切换到想要的目录
pwd 查看远程主机所在的确切路径
lcd 切换本地目录
lpwd 查询本地目录
close 终止SFTP会话,但不会退出PSFTP
open 建立到给定主机的STFP连接

命令行客户端链接服务器

连接服务端

另一种方式,打开cmd程序,进入到mysql安装目录的bin目录下

  1. 1、进入mysql的bin目录
  2. cd C:\Program Files (x86)\MySQL\MySQL Server 5.1\bin
  3. 2、连接mysql
  4. mysql -uroot -p

%title插图%num

数据库
查看所有数据库
show databases;
使用数据库
use 数据库名;
查看当前使用的数据库
select database();
创建数据库
create database 数据库名 charset=utf8;
例:
create database ceshi charset=utf8;
删除数据库
drop database 数据库名;
例:
drop database ceshi;
数据表
查看当前数据库中所有表
show tables;
查看表结构
desc 表名;
查看表的创建语句

show create table 表名;
例:
show create table students;
备份
以管理员身份运行cmd程序

%title插图%num

运行mysqldump命令
cd C:\Program Files (x86)\MySQL\MySQL Server 5.1\bin

mysqldump –uroot –p 数据库名 > ceshi.sql

# 按提示输入mysql的密码
恢复
先创建新的数据库
mysql -uroot –p 新数据库名 < ceshi.sql
# 根据提示输入mysql密码

Android Context完全解析,你不知道的Context的各种细节

Context相信所有的Android开发人员基本上每天都在接触,因为它太常见了。但是这并不代表Context没有什么东西好讲的,实际上Context有太多小的细节并不被大家所关注,那么今天我们就来学习一下那些你所不知道的细节。

 

Context类型

我们知道,Android应用都是使用Java语言来编写的,那么大家可以思考一下,一个Android程序和一个Java程序,他们*大的区别在哪里?划分界限又是什么呢?其实简单点分析,Android程序不像Java程序一样,随便创建一个类,写个main()方法就能跑了,而是要有一个完整的Android工程环境,在这个环境下,我们有像Activity、Service、BroadcastReceiver等系统组件,而这些组件并不是像一个普通的Java对象new一下就能创建实例的了,而是要有它们各自的上下文环境,也就是我们这里讨论的Context。可以这样讲,Context是维持Android程序中各组件能够正常工作的一个核心功能类。

 

下面我们来看一下Context的继承结构:

%title插图%num

Context的继承结构还是稍微有点复杂的,可以看到,直系子类有两个,一个是ContextWrapper,一个是ContextImpl。那么从名字上就可以看出,ContextWrapper是上下文功能的封装类,而ContextImpl则是上下文功能的实现类。而ContextWrapper又有三个直接的子类,ContextThemeWrapper、Service和Application。其中,ContextThemeWrapper是一个带主题的封装类,而它有一个直接子类就是Activity。

 

那么在这里我们至少看到了几个所比较熟悉的面孔,Activity、Service、还有Application。由此,其实我们就已经可以得出结论了,Context一共有三种类型,分别是Application、Activity和Service。这三个类虽然分别各种承担着不同的作用,但它们都属于Context的一种,而它们具体Context的功能则是由ContextImpl类去实现的。

 

那么Context到底可以实现哪些功能呢?这个就实在是太多了,弹出Toast、启动Activity、启动Service、发送广播、操作数据库等等等等都需要用到Context。由于Context的具体能力是由ContextImpl类去实现的,因此在*大多数场景下,Activity、Service和Application这三种类型的Context都是可以通用的。不过有几种场景比较特殊,比如启动Activity,还有弹出Dialog。出于安全原因的考虑,Android是不允许Activity或Dialog凭空出现的,一个Activity的启动必须要建立在另一个Activity的基础之上,也就是以此形成的返回栈。而Dialog则必须在一个Activity上面弹出(除非是System Alert类型的Dialog),因此在这种场景下,我们只能使用Activity类型的Context,否则将会出错。

 

Context数量

那么一个应用程序中到底有多少个Context呢?其实根据上面的Context类型我们就已经可以得出答案了。Context一共有Application、Activity和Service三种类型,因此一个应用程序中Context数量的计算公式就可以这样写:

[plain] view plain copy
  1. Context数量 = Activity数量 + Service数量 + 1

上面的1代表着Application的数量,因为一个应用程序中可以有多个Activity和多个Service,但是只能有一个Application。

 

Application Context的设计

基本上每一个应用程序都会有一个自己的Application,并让它继承自系统的Application类,然后在自己的Application类中去封装一些通用的操作。其实这并不是Google所推荐的一种做法,因为这样我们只是把Application当成了一个通用工具类来使用的,而实际上使用一个简单的单例类也可以实现同样的功能。但是根据我的观察,有太多的项目都是这样使用Application的。当然这种做法也并没有什么副作用,只是说明还是有不少人对于Application理解的还有些欠缺。那么这里我们先来对Application的设计进行分析,讲一些大家所不知道的细节,然后再看一下平时使用Application的问题。

 

首先新建一个MyApplication并让它继承自Application,然后在AndroidManifest.xml文件中对MyApplication进行指定,如下所示:

[html] view plain copy
  1. <application  
  2.     android:name=“.MyApplication”  
  3.     android:allowBackup=“true”  
  4.     android:icon=“@drawable/ic_launcher”  
  5.     android:label=“@string/app_name”  
  6.     android:theme=“@style/AppTheme” >  
  7.     ……
  8. </application>  

指定完成后,当我们的程序启动时Android系统就会创建一个MyApplication的实例,如果这里不指定的话就会默认创建一个Application的实例。

 

前面提到过,现在很多的Application都是被当作通用工具类来使用的,那么既然作为一个通用工具类,我们要怎样才能获取到它的实例呢?如下所示:

[java] view plain copy
  1. public class MainActivity extends Activity {  
  2.     @Override  
  3.     protected void onCreate(Bundle savedInstanceState) {  
  4.         super.onCreate(savedInstanceState);  
  5.         setContentView(R.layout.activity_main);
  6.         MyApplication myApp = (MyApplication) getApplication();
  7.         Log.d(“TAG”, “getApplication is ” + myApp);  
  8.     }
  9. }

可以看到,代码很简单,只需要调用getApplication()方法就能拿到我们自定义的Application的实例了,打印结果如下所示:

 

%title插图%num

那么除了getApplication()方法,其实还有一个getApplicationContext()方法,这两个方法看上去好像有点关联,那么它们的区别是什么呢?我们将代码修改一下:

[java] view plain copy
  1. public class MainActivity extends Activity {  
  2.     @Override  
  3.     protected void onCreate(Bundle savedInstanceState) {  
  4.         super.onCreate(savedInstanceState);  
  5.         setContentView(R.layout.activity_main);
  6.         MyApplication myApp = (MyApplication) getApplication();
  7.         Log.d(“TAG”, “getApplication is ” + myApp);  
  8.         Context appContext = getApplicationContext();
  9.         Log.d(“TAG”, “getApplicationContext is ” + appContext);  
  10.     }
  11. }

同样,我们把getApplicationContext()的结果打印了出来,现在重新运行代码,结果如下图所示:

 

%title插图%num

 

咦?好像打印出的结果是一样的呀,连后面的内存地址都是相同的,看来它们是同一个对象。其实这个结果也很好理解,因为前面已经说过了,Application本身就是一个Context,所以这里获取getApplicationContext()得到的结果就是MyApplication本身的实例。

 

那么有的朋友可能就会问了,既然这两个方法得到的结果都是相同的,那么Android为什么要提供两个功能重复的方法呢?实际上这两个方法在作用域上有比较大的区别。getApplication()方法的语义性非常强,一看就知道是用来获取Application实例的,但是这个方法只有在Activity和Service中才能调用的到。那么也许在*大多数情况下我们都是在Activity或者Service中使用Application的,但是如果在一些其它的场景,比如BroadcastReceiver中也想获得Application的实例,这时就可以借助getApplicationContext()方法了,如下所示:

[java] view plain copy
  1. public class MyReceiver extends BroadcastReceiver {  
  2.     @Override  
  3.     public void onReceive(Context context, Intent intent) {  
  4.         MyApplication myApp = (MyApplication) context.getApplicationContext();
  5.         Log.d(“TAG”, “myApp is ” + myApp);  
  6.     }
  7. }

也就是说,getApplicationContext()方法的作用域会更广一些,任何一个Context的实例,只要调用getApplicationContext()方法都可以拿到我们的Application对象。

那么更加细心的朋友会发现,除了这两个方法之外,其实还有一个getBaseContext()方法,这个baseContext又是什么东西呢?我们还是通过打印的方式来验证一下:

 

%title插图%num

 

哦?这次得到的是不同的对象了,getBaseContext()方法得到的是一个ContextImpl对象。这个ContextImpl是不是感觉有点似曾相识?回去看一下Context的继承结构图吧,ContextImpl正是上下文功能的实现类。也就是说像Application、Activity这样的类其实并不会去具体实现Context的功能,而仅仅是做了一层接口封装而已,Context的具体功能都是由ContextImpl类去完成的。那么这样的设计到底是怎么实现的呢?我们还是来看一下源码吧。因为Application、Activity、Service都是直接或间接继承自ContextWrapper的,我们就直接看ContextWrapper的源码,如下所示:

[java] view plain copy
  1. /** 
  2.  * Proxying implementation of Context that simply delegates all of its calls to 
  3.  * another Context.  Can be subclassed to modify behavior without changing 
  4.  * the original Context. 
  5.  */  
  6. public class ContextWrapper extends Context {  
  7.     Context mBase;
  8.     /** 
  9.      * Set the base context for this ContextWrapper.  All calls will then be 
  10.      * delegated to the base context.  Throws 
  11.      * IllegalStateException if a base context has already been set. 
  12.      *  
  13.      * @param base The new base context for this wrapper. 
  14.      */  
  15.     protected void attachBaseContext(Context base) {  
  16.         if (mBase != null) {  
  17.             throw new IllegalStateException(“Base context already set”);  
  18.         }
  19.         mBase = base;
  20.     }
  21.     /** 
  22.      * @return the base context as set by the constructor or setBaseContext 
  23.      */  
  24.     public Context getBaseContext() {  
  25.         return mBase;  
  26.     }
  27.     @Override  
  28.     public AssetManager getAssets() {  
  29.         return mBase.getAssets();  
  30.     }
  31.     @Override  
  32.     public Resources getResources() {  
  33.         return mBase.getResources();  
  34.     }
  35.     @Override  
  36.     public ContentResolver getContentResolver() {  
  37.         return mBase.getContentResolver();  
  38.     }
  39.     @Override  
  40.     public Looper getMainLooper() {  
  41.         return mBase.getMainLooper();  
  42.     }
  43.     @Override  
  44.     public Context getApplicationContext() {  
  45.         return mBase.getApplicationContext();  
  46.     }
  47.     @Override  
  48.     public String getPackageName() {  
  49.         return mBase.getPackageName();  
  50.     }
  51.     @Override  
  52.     public void startActivity(Intent intent) {  
  53.         mBase.startActivity(intent);
  54.     }
  55.     @Override  
  56.     public void sendBroadcast(Intent intent) {  
  57.         mBase.sendBroadcast(intent);
  58.     }
  59.     @Override  
  60.     public Intent registerReceiver(  
  61.         BroadcastReceiver receiver, IntentFilter filter) {
  62.         return mBase.registerReceiver(receiver, filter);  
  63.     }
  64.     @Override  
  65.     public void unregisterReceiver(BroadcastReceiver receiver) {  
  66.         mBase.unregisterReceiver(receiver);
  67.     }
  68.     @Override  
  69.     public ComponentName startService(Intent service) {  
  70.         return mBase.startService(service);  
  71.     }
  72.     @Override  
  73.     public boolean stopService(Intent name) {  
  74.         return mBase.stopService(name);  
  75.     }
  76.     @Override  
  77.     public boolean bindService(Intent service, ServiceConnection conn,  
  78.             int flags) {  
  79.         return mBase.bindService(service, conn, flags);  
  80.     }
  81.     @Override  
  82.     public void unbindService(ServiceConnection conn) {  
  83.         mBase.unbindService(conn);
  84.     }
  85.     @Override  
  86.     public Object getSystemService(String name) {  
  87.         return mBase.getSystemService(name);  
  88.     }
  89.     ……
  90. }

由于ContextWrapper中的方法还是非常多的,我就进行了一些筛选,只贴出来了部分方法。那么上面的这些方法相信大家都是非常熟悉的,getResources()、getPackageName()、getSystemService()等等都是我们经常要用到的方法。那么所有这些方法的实现又是什么样的呢?其实所有ContextWrapper中方法的实现都非常统一,就是调用了mBase对象中对应当前方法名的方法。

 

那么这个mBase对象又是什么呢?我们来看第16行的attachBaseContext()方法,这个方法中传入了一个base参数,并把这个参数赋值给了mBase对象。而attachBaseContext()方法其实是由系统来调用的,它会把ContextImpl对象作为参数传递到attachBaseContext()方法当中,从而赋值给mBase对象,之后ContextWrapper中的所有方法其实都是通过这种委托的机制交由ContextImpl去具体实现的,所以说ContextImpl是上下文功能的实现类是非常准确的。

 

那么另外再看一下我们刚刚打印的getBaseContext()方法,在第26行。这个方法只有一行代码,就是返回了mBase对象而已,而mBase对象其实就是ContextImpl对象,因此刚才的打印结果也得到了印证。

 

使用Application的问题

虽说Application的用法确实非常简单,但是我们平时的开发工作当中也着实存在着不少Application误用的场景,那么今天就来看一看有哪些比较容易犯错的地方是我们应该注意的。

 

Application是Context的其中一种类型,那么是否就意味着,只要是Application的实例,就能随时使用Context的各种方法呢?我们来做个实验试一下就知道了:

[java] view plain copy
  1. public class MyApplication extends Application {  
  2.     public MyApplication() {  
  3.         String packageName = getPackageName();
  4.         Log.d(“TAG”, “package name is ” + packageName);  
  5.     }
  6. }

这是一个非常简单的自定义Application,我们在MyApplication的构造方法当中获取了当前应用程序的包名,并打印出来。获取包名使用了getPackageName()方法,这个方法就是由Context提供的。那么上面的代码能正常运行吗?跑一下就知道了,你将会看到如下所示的结果:

 

%title插图%num

 

应用程序一启动就立刻崩溃了,报的是一个空指针异常。看起来好像挺简单的一段代码,怎么就会成空指针了呢?但是如果你尝试把代码改成下面的写法,就会发现一切正常了:

[java] view plain copy
  1. public class MyApplication extends Application {  
  2.     @Override  
  3.     public void onCreate() {  
  4.         super.onCreate();  
  5.         String packageName = getPackageName();
  6.         Log.d(“TAG”, “package name is ” + packageName);  
  7.     }
  8. }

运行结果如下所示:

 

%title插图%num

 

在构造方法中调用Context的方法就会崩溃,在onCreate()方法中调用Context的方法就一切正常,那么这两个方法之间到底发生了什么事情呢?我们重新回顾一下ContextWrapper类的源码,ContextWrapper中有一个attachBaseContext()方法,这个方法会将传入的一个Context参数赋值给mBase对象,之后mBase对象就有值了。而我们又知道,所有Context的方法都是调用这个mBase对象的同名方法,那么也就是说如果在mBase对象还没赋值的情况下就去调用Context中的任何一个方法时,就会出现空指针异常,上面的代码就是这种情况。Application中方法的执行顺序如下图所示:

 

%title插图%num

 

Application中在onCreate()方法里去初始化各种全局的变量数据是一种比较推荐的做法,但是如果你想把初始化的时间点提前到*致,也可以去重写attachBaseContext()方法,如下所示:

[java] view plain copy
  1. public class MyApplication extends Application {  
  2.     @Override  
  3.     protected void attachBaseContext(Context base) {  
  4.         // 在这里调用Context的方法会崩溃  
  5.         super.attachBaseContext(base);  
  6.         // 在这里可以正常调用Context的方法  
  7.     }
  8. }

以上是我们平时在使用Application时需要注意的一个点,下面再来介绍另外一种非常普遍的Application误用情况。

 

其实Android官方并不太推荐我们使用自定义的Application,基本上只有需要做一些全局初始化的时候可能才需要用到自定义Application,官方文档描述如下:

%title插图%num

但是就我的观察而言,现在自定义Application的使用情况基本上可以达到100%了,也就是我们平时自己写测试demo的时候可能不会使用,正式的项目几乎全部都会使用自定义Application。可是使用归使用,有不少项目对自定义Application的用法并不到位,正如官方文档中所表述的一样,多数项目只是把自定义Application当成了一个通用工具类,而这个功能并不需要借助Application来实现,使用单例可能是一种更加标准的方式。

 

不过自定义Application也并没有什么副作用,它和单例模式二选一都可以实现同样的功能,但是我见过有一些项目,会把自定义Application和单例模式混合到一起使用,这就让人大跌眼镜了。一个非常典型的例子如下所示:

[java] view plain copy
  1. public class MyApplication extends Application {  
  2.     private static MyApplication app;  
  3.     public static MyApplication getInstance() {  
  4.         if (app == null) {  
  5.             app = new MyApplication();  
  6.         }
  7.         return app;  
  8.     }
  9. }

就像单例模式一样,这里提供了一个getInstance()方法,用于获取MyApplication的实例,有了这个实例之后,就可以调用MyApplication中的各种工具方法了。

 

但是这种写法对吗?这种写法是大错特错!因为我们知道Application是属于系统组件,系统组件的实例是要由系统来去创建的,如果这里我们自己去new一个MyApplication的实例,它就只是一个普通的Java对象而已,而不具备任何Context的能力。有很多人向我反馈使用 LitePal 时发生了空指针错误其实都是由于这个原因,因为你提供给LitePal的只是一个普通的Java对象,它无法通过这个对象来进行Context操作。

 

那么如果真的想要提供一个获取MyApplication实例的方法,比较标准的写法又是什么样的呢?其实这里我们只需谨记一点,Application全局只有一个,它本身就已经是单例了,无需再用单例模式去为它做多重实例保护了,代码如下所示:

[java] view plain copy
  1. public class MyApplication extends Application {  
  2.     private static MyApplication app;  
  3.     public static MyApplication getInstance() {  
  4.         return app;  
  5.     }
  6.     @Override  
  7.     public void onCreate() {  
  8.         super.onCreate();  
  9.         app = this;  
  10.     }
  11. }

getInstance()方法可以照常提供,但是里面不要做任何逻辑判断,直接返回app对象就可以了,而app对象又是什么呢?在onCreate()方法中我们将app对象赋值成this,this就是当前Application的实例,那么app也就是当前Application的实例了。

 

4、Context的应用场景

%title插图%num

 

 

start activity :    需要提供一个task  除了activity其他的context都没有task,     如果在intent 的flag 添加NEW_TASK  属性创建一个新的task,其他context也是可以start activity的

show dialog  :    dialog需要依附一个窗口 只有activity自带一个窗口,如果设置dialog为system.alert  使dialog依附系统窗口 ,其他context也是可以show dialog的

layout inflate :   在这些类中去layout inflate是合法的,但是会使用系统默认的主题样式,因为只有activity 是继承的ContextThemeWrapper带有theme。

其他context可以通过ContextThemeWrapper mContextThemeWrapper = new ContextThemeWrapper(context, theme);

创建一个带theme的context对象ContextThemeWrapper使用,这样其他context也可创建带theme的layout

 

 

大家注意看到有一些NO上添加了一些数字,其实这些从能力上来说是YES,但是为什么说是NO呢?下面一个一个解释:

数字1:启动Activity在这些类中是可以的,但是需要创建一个新的task。一般情况不推荐。

数字2:在这些类中去layout inflate是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。

数字3:在receiver为null时允许,在4.2或以上的版本中,用于获取黏性广播的当前值。(可以无视)

注:ContentProvider、BroadcastReceiver之所以在上述表格中,是因为在其内部方法中都有一个context用于使用。

 

 

好了,关于Context的介绍就到这里吧,内容还是比较简单易懂的,希望大家通过这篇文章可以理解Context更多的细节,并且不要去犯使用Context时的一些低级错误。

关于Android Context 你必须知道的一切

1、Context概念

其实一直想写一篇关于Context的文章,但是又怕技术不如而误人子弟,于是参考了些资料,今天准备整理下写出来,如有不足,请指出,参考资料会在醒目地方标明。

Context,相信不管是*天开发Android,还是开发Android的各种老鸟,对于Context的使用一定不陌生~~你在加载资源、启动一个新的Activity、获取系统服务、获取内部文件(夹)路径、创建View操作时等都需要Context的参与,可见Context的常见性。大家可能会问到底什么是Context,Context字面意思上下文,或者叫做场景,也就是用户与操作系统操作的一个过程,比如你打电话,场景包括电话程序对应的界面,以及隐藏在背后的数据;

但是在程序的角度Context又是什么呢?在程序的角度,我们可以有比较权威的答案,Context是个抽象类,我们可以直接通过看其类结构来说明答案:

%title插图%num

可以看到Activity、Service、Application都是Context的子类;

也就是说,Android系统的角度来理解:Context是一个场景,代表与操作系统的交互的一种过程。从程序的角度上来理解:Context是个抽象类,而Activity、Service、Application等都是该类的一个实现。

在仔细看一下上图:Activity、Service、Application都是继承自ContextWrapper,而ContextWrapper内部会包含一个base context,由这个base context去实现了*大多数的方法。

先扯这么多,有能力了会从别的角度去审视Context,加油~

 

2、Context与ApplicationContext

看了标题,千万不要被误解,ApplicationContext并没有这个类,其实更应该叫做:Activity与Application在作为Context时的区别。嗯,的确是这样的,大家在需要Context的时候,如果是在Activity中,大多直接传个this,当在匿名内部类的时候,因为this不能用,需要写XXXActivity.this,很多哥们会偷懒,直接就来个getApplicationContext。那么大家有没有想过,XXXActivity.this和getApplicationContext的区别呢?

XXXActivity和getApplicationContext返回的肯定不是一个对象,一个是当前Activity的实例,一个是项目的Application的实例。既然区别这么明显,那么各自的使用场景肯定不同,乱使用可能会带来一些问题。

下面开始介绍在使用Context时,需要注意的问题。

 

3、引用的保持

大家在编写一些类时,例如工具类,可能会编写成单例的方式,这些工具类大多需要去访问资源,也就说需要Context的参与。

在这样的情况下,就需要注意Context的引用问题。

例如以下的写法:

[java] view plain copy
  1. package com.mooc.shader.roundimageview;  
  2. import android.content.Context;  
  3. public class CustomManager  
  4. {
  5.     private static CustomManager sInstance;  
  6.     private Context mContext;  
  7.     private CustomManager(Context context)  
  8.     {
  9.         this.mContext = context;  
  10.     }
  11.     public static synchronized CustomManager getInstance(Context context)  
  12.     {
  13.         if (sInstance == null)  
  14.         {
  15.             sInstance = new CustomManager(context);  
  16.         }
  17.         return sInstance;  
  18.     }
  19.     //some methods   
  20.     private void someOtherMethodNeedContext()  
  21.     {
  22.     }
  23. }

对于上述的单例,大家应该都不陌生(请别计较getInstance的效率问题),内部保持了一个Context的引用;

这么写是没有问题的,问题在于,这个Context哪来的我们不能确定,很大的可能性,你在某个Activity里面为了方便,直接传了个this;这样问题就来了,我们的这个类中的sInstance是一个static且强引用的,在其内部引用了一个Activity作为Context,也就是说,我们的这个Activity只要我们的项目活着,就没有办法进行内存回收。而我们的Activity的生命周期肯定没这么长,所以造成了内存泄漏。

那么,我们如何才能避免这样的问题呢?

有人会说,我们可以软引用,嗯,软引用,假如被回收了,你不怕NullPointException么。

把上述代码做下修改:

[java] view plain copy
  1. public static synchronized CustomManager getInstance(Context context)  
  2.     {
  3.         if (sInstance == null)  
  4.         {
  5.             sInstance = new CustomManager(context.getApplicationContext());  
  6.         }
  7.         return sInstance;  
  8.     }

这样,我们就解决了内存泄漏的问题,因为我们引用的是一个ApplicationContext,它的生命周期和我们的单例对象一致。

这样的话,可能有人会说,早说嘛,那我们以后都这么用不就行了,很遗憾的说,不行。上面我们已经说过,Context和Application Context的区别是很大的,也就是说,他们的应用场景(你也可以认为是能力)是不同的,并非所有Activity为Context的场景,Application Context都能搞定。

下面就开始介绍各种Context的应用场景。

 

4、Context的应用场景

%title插图%num

 

大家注意看到有一些NO上添加了一些数字,其实这些从能力上来说是YES,但是为什么说是NO呢?下面一个一个解释:

数字1:启动Activity在这些类中是可以的,但是需要创建一个新的task。一般情况不推荐。

数字2:在这些类中去layout inflate是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。

数字3:在receiver为null时允许,在4.2或以上的版本中,用于获取黏性广播的当前值。(可以无视)

注:ContentProvider、BroadcastReceiver之所以在上述表格中,是因为在其内部方法中都有一个context用于使用。

 

好了,这里我们看下表格,重点看Activity和Application,可以看到,和UI相关的方法基本都不建议或者不可使用Application,并且,前三个操作基本不可能在Application中出现。实际上,只要把握住一点,凡是跟UI相关的,都应该使用Activity做为Context来处理;其他的一些操作,Service,Activity,Application等实例都可以,当然了,注意Context引用的持有,防止内存泄漏。

 

5、总结

好了,到此,Context的分析基本完成了,希望大家在以后的使用过程中,能够稍微考虑下,这里使用Activity合适吗?会不会造成内存泄漏?这里传入Application work吗?

 

Android属性动画:动画流控制

今天的文章里,我将会和大家讨论对动画流的控制。我们可以通过Animator系列的API来控制动画的开始、停止和取消。在 KitKat也就是API level 19中,我们还可以控制动画的暂停和恢复。在本文中,我将会带你体验整个动画流的控制,并且通过一些函数方法来让你能够观察到动画的状态。

动画流介绍

在之前的教程中,我们已经使用过多次Animator.start这个方法。这个方法是用来让动画从*帧开始播放。该方法只是动画流控制方法集中的一个方法而已,完整的方法集合如下所示:

start这个方法顾名思义是用来让动画从开头开始播放的。如果动画设置了一个大于0的播放延迟(startDelay),那么调用该方法后还需要等到延迟的时间过去才回开始播放。
我们有两种停止动画的方法,你可以用end方法抑或cancel方法来停止一个播放着的动画。在两种方式中动画都会终止并且只有再次调用start方法才会重新开始播放。两者的区别则在于停止后动画所在的状态,当你使用cancel方法来停止动画后,动画只是停止了它的时间轴,动画的状态会停在一个中间态(intermediate state)。如果通过end 方法来停止一个动画,那么动画会直接快进到该动画*后一帧并且停止,所有的对象都会保持在动画*终结束后的状态。
在 Kitkat 中增加的没有怎么被大家关注到的新API则是带来了动画可以暂停和恢复的能力。在那之前,一个动画如果被取消并且停留在当前的中间态,此时你用start方法去重启动画,动画只会从一开始重新播放。现在,你则可以调用pause方法来暂停当前播放中的动画,pause也会有和cancel方法一样的功效让动画停留在中间态,但是当你使用resume 方法去恢复这个动画的时候,动画会从这个状态继续播放下去。
现在,让我们来实践下看看效果。我们新建一个 Activity 并且包含一个 私有的动画对象。并且在 onCreate 回调中初始化这个动画。

我们的动画是一个简单的旋转动画,它将把一个图片完整旋转360度五次。被旋转的图片会在 layout 的 XML 文件中定义好,并且给予一个叫some_image的id。这个layout同时也包含了五个按钮分别是:Start、End、Cancel、Pause以及Resume。这五个按钮分别代表了动画的五个调用方法。

这些方法只是简单的调用了相应的动画控制调用。下面会有两张gif图片来演示效果。上边的动画展示了end方法和cancel方法的区别。可以注意到,cancel让图片保持在停止时的中间态,但是end则让动画到了*后的状态。
下边的动画则演示了pause和resume方法。可以注意到,pause 和cancel都暂停了动画的时间轴。但是现在我们可以用resume 调用去恢复动画的时间轴了。

PropertyAnim_EndCancel

PropertyAnim_PauseRestart

动画状态的查询

有些时候,我们需要去查询当前动画的状态,这个需求可以通过下面这些方法来完成。

如果当前动画已经调用了start函数并且还没播放完成也没有被取消掉,那么isStarted方法会返回true。请注意,isStarted 方法*低的 API 需求是 14.同时,就算是在动画播放延迟中,该方法依然会返回 true。这就是这个方法和 isRunning 方法的不同点,isRunning 方法只会在动画确实在播放并且还没停止的时候返回 true。

在 API 19 的时候,isPaused 方法被加入进来。这是由于那时候动画可以被暂停和恢复了,如果 isPaused 返回了 true,那么说明当前动画是在暂停状态下,反之亦然。

为了演示这些观察方法的效果,我们会通过一个包含三个显示动画状态文本框的例子来解释。这三个文本框都会在Activity中做为TextView成员变量存在。

于此同时,我们在onCreate中添加下列三行代码来把这些文本框从layout中获取到。

我们还创建了一个根据当前动画状态来更新这些文本框的方法。

我们在初始化以后每次修改动画流的时候都去调用setStatusTexts方法,比如说,当我没调用cancelAnimation的时候,代码是这样的:

演示的结果我们可以从下面上方的动画中看到。这里我也创建了两个动画,一个演示End和cancel效果,另一个演示pause和 Resume效果。可以注意到,动画的运行状态在你点击了End和cancel之后是相同的,就算视觉上看两者并不一样。这说明了,你没法通过动画的状态来区分出当前动画是通过 End 调用还是cancel调用来停止的。这两者情况下,isStarted和isRunning都会返回false。

PropertyAnim_EndCancelStatus

PropertyAnim_PauseResumeStatus

而下边的动画则显示了pause和resume调用的效果。当我们通过pause调用去暂停动画时,isPaused会返回true。然后通过 resume调用去恢复动画播放后,isPaused也会变成false。可以注意到,图中如果动画是自然结束的,动画的状态并没有改变。当然,动画停止播放后,isStarted和isRunning肯定应该是返回false的。实际上,在动画自然停止后,如果我们再去调用 isStarted和isRunning他们的的确确会返回false。而动画中的例子我们并没有在动画自然停止后去更新状态,所以我们并不知道当前的状态,自然那几个文本框也就没有更新。为了能够更新到*新的状态,我们可以在动画中添加AnimatorListener和 AnimatorPauseListener这两个监听,这两个监听具体的使用方法会是下一讲的主要内容。

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速