日期: 2021 年 9 月 16 日

iOS屏蔽WKWebView长按菜单里的查询、学习、共享等按钮,UIMenuController菜单

需求:
WKWebView里的文字如果长按的话,会出现UIMenuController菜单快捷键包括:拷贝、粘贴、全选、翻译、查询、学习、共享等功能。当然,苹果给我们提供如此便捷的菜单是好事,我们能不能屏蔽一下不需要的菜单呢?

方案一:
可以通过css、js等屏蔽长按操作。贴代码:

// 禁止选择CSS
NSString *css = @”body{-webkit-user-select:none;-webkit-user-drag:none;}”;

// CSS选中样式取消
NSMutableString *javascript = [NSMutableString string];
[javascript appendString:@”var style = document.createElement(‘style’);”];
[javascript appendString:@”style.type = ‘text/css’;”];
[javascript appendFormat:@”var cssContent = document.createTextNode(‘%@’);”, css];
[javascript appendString:@”style.appendChild(cssContent);”];
[javascript appendString:@”document.body.appendChild(style);”];

// javascript注入
WKUserScript *noneSelectScript = [[WKUserScript alloc] initWithSource:javascript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
[userContentController addUserScript:noneSelectScript];
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
configuration.userContentController = userContentController;
好处是彻底封杀长按菜单,坏处是无法保留我们想要的拷贝等基本功能。

方案二:
重写WKWebView的canPerformAction:withSender:方法,将需要的返回YES,不需要的返回NO。

重点:是重写WKWebView的方法哦!

#import <WebKit/WebKit.h>

@interface HXWebView : WKWebView

@end

@implementation HXWebView

– (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
//NSLog(@”action:%@”,NSStringFromSelector(action));

//我们在这里不要去处理私有API的方法,会被苹果检测出来的哦!
if(action ==@selector(copy:) ||

action ==@selector(selectAll:)||

action ==@selector(cut:)||

action ==@selector(select:)||

action ==@selector(paste:)) {

//记得调用super的方法哦!
return [super canPerformAction:action withSender:sender];

}

return NO;
}

@end
我们在这里不要去处理私有API的方法,会被苹果检测出来的哦!
_define:
_promptForReplace:
_share:
_transliterateChinese:
_insertDrawing:
_showTextStyleOptions:
_lookup:
_addShortcut:
_accessibilitySpeak:
_accessibilitySpeakLanguageSelection:
_accessibilityPauseSpeaking:

*后是记得调用super的方法哦!要知道这里面的处理逻辑还是很复杂的呢!

iOS解析–自动无限循环UIScrollView原理

今天下午突然想起来上一次的面试经历,当时我是去面试美丽说IOS工程师,中午12点出发,2点就到了新中观大厦,没想到该公司是10:30上班,刚好人们吃午饭回来。笔试题很简单,半小时后一个姓赵的哥们来面试我。记得有一个问题是如何实现自动无限循环的UIScrollView。当时虽然思路想正确了,但还是回答的不够好。现在好好分析了一下网上开源的代码,有所收获,特此分享给大家。

无限循环:

我们都知道UIScrollView有一种很流畅的切换效果,结合UIPageControl的辅助展示效果,就可以完成一个很不错的产品介绍功能页面。那么像一些购物app中,商品展示页面无限滚动的效果是如何实现的呢?

方法1:前后+1方法,这也*常见的一种做法。假如我们有四张需要展示的图片,我们创建了一个数组来保存图片名字,此时数组中保存的是按顺序的1.png,2.png,3.png,4.png,这四个图片名字。要实现无限循环的效果,我们需要改动一下这个数组为:4.png,1.png,2.png,3.png,4.png,1.png,我们发现在原来数组的前后分别加入了一个图片名字,即将要循环展示的下一张图片的名字。当你滑动到4.png的时候,下一张会是1.png。当你在1.png往回滑动的时候,将要出现4.png。

好了,下面是我们的核心内容:我们发现目前数组中有6个图片,当我们从3.png滑动到4.png,又从4.png滑动到1.png的时候,我们要神不知鬼不觉的迅速切换到排在第二位的1.png。反向滑动的时候也是如此,从1.png滑动到4.png的时候,我们要神不知鬼不觉的切换到拍到倒数第二位的4.png。那么怎么样才能实现神不知鬼不觉呢?

看下面这两个UIScrollView的实例方法:

– (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated;
– (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated;
这两个方法可以在animated参数为NO的时候,帮助我们迅速切换视图。

当每一次滑动结束的时候,UIScrollViewDelegate会有一个回调方法:
– (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;

此时我们来检测是否是滑动到了我们将要出发的1.png和4.png,如果是的话,那么就悄悄调用上面两个方法中的任意一个来实现视图切换。

实现代码如下所示:
int currentPage = (int)self.scrollView.contentOffset.x/320;
if (currentPage==0)
{
[self.scrollView scrollRectToVisible:CGRectMake(320 * [slideImages count]-2,0,320,460) animated:NO]; // 序号0,跳转到*后1页
}
else if (currentPage==[slideImages count]-1)
{
[self.scrollView scrollRectToVisible:CGRectMake(320,0,320,460) animated:NO]; // *后+1,循环第1页
}

方法2:瞒天过海。此方法中无论数据源有多少个,UIScrollView只保留其中三个视图,其实这是方法1的变种。当你滑动UIScrollView的时候,无非是向前滑动,或者是向后滑动,所以能够组成无限循环的基本条件就是前、中、后 三个视图。当你每次滑动的时候我都神不知鬼不觉的切换一下这三个视图。这也是和方法1 的*主要区别。

看看下面的区别:

– (void)scrollViewDidEndDecelerating:(UIScrollView *)aScrollView {

[_scrollView setContentOffset:CGPointMake(_scrollView.frame.size.width, 0) animated:YES];

}

我们发现每一次滑动完成之后,UIScrollView总是重新切换回默认的中这一个视图。下面这个代理方法将要实现重置这三个视图:
– (void)scrollViewDidScroll:(UIScrollView *)aScrollView {

int x = aScrollView.contentOffset.x;
//往下翻一张
if(x >= (2*self.frame.size.width)) {
[self loadData];
}
//往上翻
if(x <= 0) {
[self loadData];
}
}

loadData() 的实现原理非常简单,现将UIScrollView的所有视图移除,在重新根据数据源绘图来加载到UIScrollView中。
– (void)loadData
{
//从scrollView上移除所有的subview
NSArray *subViews = [_scrollView subviews];
if([subViews count] != 0) {
[subViews makeObjectsPerformSelector:@selector(removeFromSuperview)];
}

for (int i = 0; i < 3; i++) { //只有3个视图
UIView *v = [_curViews objectAtIndex:i];
v.frame = CGRectOffset(v.frame, v.frame.size.width * i, 0);
[_scrollView addSubview:v];
}

[_scrollView setContentOffset:CGPointMake(_scrollView.frame.size.width, 0)];
}

至于你怎么绘图就是你的事情了,我这里只是简单的分析一下。

自动无限循环

刚才讲解了无限循环的两种不同实现方法,下面来讲解一下如何让它自动滑动。我想大家都用过NSTimer,没错,用它来实现简单的计时器*好不过了。

在适当位置初始化一个NSTimer,设定3秒执行一次runTimePage()方法:

// 定时器 循环
[NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(runTimePage) userInfo:nil repeats:YES];

在runTimePage()方法中,我们将要实现获取当前page页数,然后跳转到下一个page。

– (void)runTimePage
{
int page = pageControl.currentPage; // 获取当前的page
page++;
pageControl.currentPage = page > [slideImages count]-2 ? 0 : page ;
[self.scrollView setContentOffset:CGPointMake(320*(page+1),0) animated:YES];
}

是不是很简单呢~~

SocketIO — iOS使用篇

上个月有个项目正好用到了即时通讯功能,于是研究到了websocket技术,后来发现了可以在web、Android、iOS上使用的解决方案,那就是SocketIO。其实现原理啥的不做介绍了,直接贴上IOS项目可能会用到的一些资源。

首先是官网:http://socket.io/ 这个大家都不会陌生吧~~

1、官方推荐的Android项目:http://socket.io/blog/native-socket-io-and-android/

github上的相应地址为:https://github.com/nkzawa/socket.io-android-chat

2、官方推荐的IOS–swift项目:http://socket.io/blog/socket-io-on-ios/

github上的相应地址为:https://github.com/socketio/socket.io-client-swift

另外如果你需要oc版本的,可以参考这里:socket.io v0.7.2 — 0.9.x for iOS and OS X https://github.com/pkyeck/socket.IO-objc

3、官方推荐的C++项目:http://socket.io/blog/socket-io-cpp/

github上的相应地址为:https://github.com/socketio/socket.io-client-cpp

另外还有一个匿名的聊天室可以让你测试用:http://chat.socket.io/

 

iOS 15 适配 UITableView 内容自动下移22像素解决办法

iOS 15 的 UITableView又新增了一个新属性:sectionHeaderTopPadding 会给每一个section header 增加一个默认高度,当我们 使用 UITableViewStylePlain 初始化 UITableView的时候,就会发现,系统给section header增高了22像素。

解决办法很简单:

if (@available(iOS 15.0, *)) {
tableView.sectionHeaderTopPadding = 0;
}

测试版ios15怎么信任软件,苹果ios15信任的描述文件在哪?苹果ios15授权信任怎么设置?…

苹果ios15信任的描述文件在哪?ios15系统发布了,虽说只是一个测试的版本,但是就目前小伙伴们的升级体验来说的话,ios15目前建议还是等一下正式版本,测试版还是与不少bug的,很多朋友不清楚ios15信任的描述文件在哪,苹果ios15授权信任怎么设置?下面跟随小编一起来看看内容吧!

2ce71719e2c41aad77fd9d7251dd66e9.png

苹果ios15信任的描述文件在哪?

首先需要了解的是要想找到苹果ios15的信任文件的话是需要找到设备管理,但是这个前提是要安装了带有描述文件的应用才会显示的,所以大家在下载描述文件的时候还是需要注意地,如果没安装是默认不显示的。下面在具体看一下步骤:

1,首先打开【设置】,然后点击【通用】-【描述文件】;

b34f17368996902350139c6ff55b0e94.png

第2步:接着在【描述文件】的详情页面下点击想要安装的应用程序,再点击【信任】进行继续安装。

aa72a0d3c01967a42cd1e547fa1c7b36.png

第3步:安装成功后,【设备管理】会自动出现在【通用】中,在【设备管理】的详情页面中可以对已安装的描述文件进行【删除应用】或【验证应用】的操作。

8d13f87b7eb14361f4d19c81939f84df.png

苹果ios15授权信任怎么设置?

1,首先我们点击手机中的设置,找到通用按钮点击。

cfa1d667b39770d567807650ed5f3bf8.png

2,找到设备管理按钮点击。

51e0d443c0bd82e9af1a0c85f0d392f9.png

3,找到需要信任的程序点击。

a5a63cc9803618d5526474f996d60f81.png

4,接着点击信任。

cdfe1891dccd6c79a32cbbb49513ab58.png

这样就设置好了,软件可以正常打开了!

Android属性动画完全解析(下),Interpolator和ViewPropertyAnimator的用法

大家好,欢迎继续回到Android属性动画完全解析。在上一篇文章当中我们学习了属性动画的一些进阶技巧,包括ValueAnimator和ObjectAnimator的高级用法,那么除了这些之外,当然还有一些其它的高级技巧在等着我们学习,因此本篇文章就对整个属性动画完全解析系列收个尾,来学习一下剩下的非常重要的高级技巧。

另外,本篇文章中使用的代码是建立在上篇文章基础之上的,如果你还没有阅读过前面的文章,建议先去参考阅读一下 Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法 。

Interpolator的用法

Interpolator这个东西很难进行翻译,直译过来的话是补间器的意思,它的主要作用是可以控制动画的变化速率,比如去实现一种非线性运动的动画效果。那么什么叫做非线性运动的动画效果呢?就是说动画改变的速率不是一成不变的,像加速运动以及减速运动都属于非线性运动。

不过Interpolator并不是属性动画中新增的技术,实际上从Android 1.0版本开始就一直存在Interpolator接口了,而之前的补间动画当然也是支持这个功能的。只不过在属性动画中新增了一个TimeInterpolator接口,这个接口是用于兼容之前的Interpolator的,这使得所有过去的Interpolator实现类都可以直接拿过来放到属性动画当中使用,那么我们来看一下现在TimeInterpolator接口的所有实现类,如下图所示:

可以看到,TimeInterpolator接口已经有非常多的实现类了,这些都是Android系统内置好的并且我们可以直接使用的Interpolator。每个Interpolator都有它各自的实现效果,比如说AccelerateInterpolator就是一个加速运动的Interpolator,而DecelerateInterpolator就是一个减速运动的Interpolator。

我觉得细心的朋友应该早已经发现了,在前面两篇文章当中我们所学到的所有属性动画,其实都不是在进行一种线程运动。比如说在“上”篇文章中使用ValueAnimator所打印的值如下所示:

可以看到,一开始的值变化速度明显比较慢,仅0.0开头的就打印了4次,之后开始加速,*后阶段又开始减速,因此我们可以很明显地看出这一个先加速后减速的Interpolator。

那么再来看一下在“中”篇文章中完成的小球移动加变色的功能,如下图所示:

从上图中我们明显可以看出,小球一开始运动速度比较慢,然后逐渐加速,中间的部分运动速度就比较快,接下来开始减速,*后缓缓停住。另外颜色变化也是这种规律,一开始颜色变化的比较慢,中间颜色变化的很快,*后阶段颜色变化的又比较慢。

从以上几点我们就可以总结出一个结论了,使用属性动画时,系统默认的Interpolator其实就是一个先加速后减速的Interpolator,对应的实现类就是AccelerateDecelerateInterpolator。

当然,我们也可以很轻松地修改这一默认属性,将它替换成任意一个系统内置好的Interpolator。就拿“中”篇文章中的代码来举例吧,MyAnimView中的startAnimation()方法是开启动画效果的入口,这里我们对Point对象的坐标稍做一下修改,让它变成一种垂直掉落的效果,代码如下所示:

private void startAnimation() {
Point startPoint = new Point(getWidth() / 2, RADIUS);
Point endPoint = new Point(getWidth() / 2, getHeight() – RADIUS);
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (Point) animation.getAnimatedValue();
invalidate();
}
});
anim.setDuration(2000);
anim.start();
}
这里主要是对Point构造函数中的坐标值进行了一下改动,那么现在小球运动的动画效果应该是从屏幕正中央的顶部掉落到底部。但是现在默认情况下小球的下降速度肯定是先加速后减速的,这不符合物理的常识规律,如果把小球视为一个自由落体的话,那么下降的速度应该是越来越快的。我们怎样才能改变这一默认行为呢?其实很简单,调用Animator的setInterpolator()方法就可以了,这个方法要求传入一个实现TimeInterpolator接口的实例,那么比如说我们想要实现小球下降越来越快的效果,就可以使用AccelerateInterpolator,代码如下所示:
private void startAnimation() {
Point startPoint = new Point(getWidth() / 2, RADIUS);
Point endPoint = new Point(getWidth() / 2, getHeight() – RADIUS);
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (Point) animation.getAnimatedValue();
invalidate();
}
});
anim.setInterpolator(new AccelerateInterpolator(2f));
anim.setDuration(2500);
anim.start();
}
代码很简单,这里调用了setInterpolator()方法,然后传入了一个AccelerateInterpolator的实例,注意AccelerateInterpolator的构建函数可以接收一个float类型的参数,这个参数是用于控制加速度的。现在运行一下代码,效果如下图所示:

OK,效果非常明显,说明我们已经成功替换掉了默认的Interpolator,AccelerateInterpolator确实是生效了。但是现在的动画效果看上去仍然是怪怪的,因为一个小球从很高的地方掉落到地面上直接就静止了,这也是不符合物理规律的,小球撞击到地面之后应该要反弹起来,然后再次落下,接着再反弹起来,又再次落下,以此反复,*后静止。这个功能我们当然可以自己去写,只不过比较复杂,所幸的是,Android系统中已经提供好了这样一种Interpolator,我们只需要简单地替换一下就可以完成上面的描述的效果,代码如下所示:

private void startAnimation() {
Point startPoint = new Point(getWidth() / 2, RADIUS);
Point endPoint = new Point(getWidth() / 2, getHeight() – RADIUS);
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (Point) animation.getAnimatedValue();
invalidate();
}
});
anim.setInterpolator(new BounceInterpolator());
anim.setDuration(3000);
anim.start();
}
可以看到,我们只是将设置的Interpolator换成了BounceInterpolator的实例,而BounceInterpolator就是一种可以模拟物理规律,实现反复弹起效果的Interpolator。另外还将整体的动画时间稍微延长了一点,因为小球反复弹起需要比之前更长的时间。现在重新运行一下代码,效果如下图所示:

OK!效果还是非常不错的。那么这里我们只是选了几个系统实现好的Interpolator,由于内置Interpolator非常多,就不一一进行讲解了,大家可以自己去使用一下其它的几种Interpolator来看一看效果。

但是,只会用一下系统提供好的Interpolator,我们显然对自己的要求就太低了,既然是学习属性动画的高级用法,那么自然要将它研究透了。下面我们就来看一下Interpolator的内部实现机制是什么样的,并且来尝试写一个自定义的Interpolator。

首先看一下TimeInterpolator的接口定义,代码如下所示:

/**
* A time interpolator defines the rate of change of an animation. This allows animations
* to have non-linear motion, such as acceleration and deceleration.
*/
public interface TimeInterpolator {

/**
* Maps a value representing the elapsed fraction of an animation to a value that represents
* the interpolated fraction. This interpolated value is then multiplied by the change in
* value of an animation to derive the animated value at the current elapsed animation time.
*
* @param input A value between 0 and 1.0 indicating our current point
* in the animation where 0 represents the start and 1.0 represents
* the end
* @return The interpolation value. This value can be more than 1.0 for
* interpolators which overshoot their targets, or less than 0 for
* interpolators that undershoot their targets.
*/
float getInterpolation(float input);
}
OK,接口还是非常简单的,只有一个getInterpolation()方法。大家有兴趣可以通过注释来对这个接口进行详解的了解,这里我就简单解释一下,getInterpolation()方法中接收一个input参数,这个参数的值会随着动画的运行而不断变化,不过它的变化是非常有规律的,就是根据设定的动画时长匀速增加,变化范围是0到1。也就是说当动画一开始的时候input的值是0,到动画结束的时候input的值是1,而中间的值则是随着动画运行的时长在0到1之间变化的。

说到这个input的值,我觉得有不少朋友可能会联想到我们在“中”篇文章中使用过的fraction值。那么这里的input和fraction有什么关系或者区别呢?答案很简单,input的值决定了fraction的值。input的值是由系统经过计算后传入到getInterpolation()方法中的,然后我们可以自己实现getInterpolation()方法中的算法,根据input的值来计算出一个返回值,而这个返回值就是fraction了。

因此,*简单的情况就是input值和fraction值是相同的,这种情况由于input值是匀速增加的,因而fraction的值也是匀速增加的,所以动画的运动情况也是匀速的。系统中内置的LinearInterpolator就是一种匀速运动的Interpolator,那么我们来看一下它的源码是怎么实现的:

/**
* An interpolator where the rate of change is constant
*/
@HasNativeInterpolator
public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {

public LinearInterpolator() {
}

public LinearInterpolator(Context context, AttributeSet attrs) {
}

public float getInterpolation(float input) {
return input;
}

/** @hide */
@Override
public long createNativeInterpolator() {
return NativeInterpolatorFactoryHelper.createLinearInterpolator();
}
}
这里我们只看getInterpolation()方法,这个方法没有任何逻辑,就是把参数中传递的input值直接返回了,因此fraction的值就是等于input的值的,这就是匀速运动的Interpolator的实现方式。

当然这是*简单的一种Interpolator的实现了,我们再来看一个稍微复杂一点的。既然现在大家都知道了系统在默认情况下使用的是AccelerateDecelerateInterpolator,那我们就来看一下它的源码吧,如下所示:

/**
* An interpolator where the rate of change starts and ends slowly but
* accelerates through the middle.
*
*/
@HasNativeInterpolator
public class AccelerateDecelerateInterpolator implements Interpolator, NativeInterpolatorFactory {
public AccelerateDecelerateInterpolator() {
}

@SuppressWarnings({“UnusedDeclaration”})
public AccelerateDecelerateInterpolator(Context context, AttributeSet attrs) {
}

public float getInterpolation(float input) {
return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
}

/** @hide */
@Override
public long createNativeInterpolator() {
return NativeInterpolatorFactoryHelper.createAccelerateDecelerateInterpolator();
}
}
代码虽然没有变长很多,但是getInterpolation()方法中的逻辑已经明显变复杂了,不再是简单地将参数中的input进行返回,而是进行了一个较为复杂的数学运算。那这里我们来分析一下它的算法实现,可以看到,算法中主要使用了余弦函数,由于input的取值范围是0到1,那么cos函数中的取值范围就是π到2π。而cos(π)的结果是-1,cos(2π)的结果是1,那么这个值再除以2加上0.5之后,getInterpolation()方法*终返回的结果值还是在0到1之间。只不过经过了余弦运算之后,*终的结果不再是匀速增加的了,而是经历了一个先加速后减速的过程。我们可以将这个算法的执行情况通过曲线图的方式绘制出来,结果如下图所示:

可以看到,这是一个S型的曲线图,当横坐标从0变化到0.2的时候,纵坐标的变化幅度很小,但是之后就开始明显加速,*后横坐标从0.8变化到1的时候,纵坐标的变化幅度又变得很小。

OK,通过分析LinearInterpolator和AccelerateDecelerateInterpolator的源码,我们已经对Interpolator的内部实现机制有了比较清楚的认识了,那么接下来我们就开始尝试编写一个自定义的Interpolator。

编写自定义Interpolator*主要的难度都是在于数学计算方面的,由于我数学并不是很好,因此这里也就写一个简单点的Interpolator来给大家演示一下。既然属性动画默认的Interpolator是先加速后减速的一种方式,这里我们就对它进行一个简单的修改,让它变成先减速后加速的方式。新建DecelerateAccelerateInterpolator类,让它实现TimeInterpolator接口,代码如下所示:

public class DecelerateAccelerateInterpolator implements TimeInterpolator{

@Override
public float getInterpolation(float input) {
float result;
if (input <= 0.5) {
result = (float) (Math.sin(Math.PI * input)) / 2;
} else {
result = (float) (2 – Math.sin(Math.PI * input)) / 2;
}
return result;
}

}
这段代码是使用正弦函数来实现先减速后加速的功能的,因为正弦函数初始弧度的变化值非常大,刚好和余弦函数是相反的,而随着弧度的增加,正弦函数的变化值也会逐渐变小,这样也就实现了减速的效果。当弧度大于π/2之后,整个过程相反了过来,现在正弦函数的弧度变化值非常小,渐渐随着弧度继续增加,变化值越来越大,弧度到π时结束,这样从0过度到π,也就实现了先减速后加速的效果。

同样我们可以将这个算法的执行情况通过曲线图的方式绘制出来,结果如下图所示:

可以看到,这也是一个S型的曲线图,只不过曲线的方向和刚才是相反的。从上图中我们可以很清楚地看出来,一开始纵坐标的变化幅度很大,然后逐渐变小,横坐标到0.5的时候纵坐标变化幅度趋近于零,之后随着横坐标继续增加纵坐标的变化幅度又开始变大,的确是先减速后加速的效果。

那么现在我们将DecelerateAccelerateInterpolator在代码中进行替换,如下所示:

private void startAnimation() {
Point startPoint = new Point(getWidth() / 2, RADIUS);
Point endPoint = new Point(getWidth() / 2, getHeight() – RADIUS);
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (Point) animation.getAnimatedValue();
invalidate();
}
});
anim.setInterpolator(new DecelerateAccelerateInterpolator());
anim.setDuration(3000);
anim.start();
}
非常简单,就是将DecelerateAccelerateInterpolator的实例传入到setInterpolator()方法当中。重新运行一下代码,效果如下图所示:

OK!小球的运动确实是先减速后加速的效果,说明我们自定义的Interpolator已经可以正常工作了。通过这样一个程度的学习,相信大家对属性动画Interpolator的理解和使用都达到了一个比较深刻的层次了。

ViewPropertyAnimator的用法

ViewPropertyAnimator其实算不上什么高级技巧,它的用法格外的简单,只不过和前面所学的所有属性动画的知识不同,它并不是在3.0系统当中引入的,而是在3.1系统当中附增的一个新的功能,因此这里我们把它作为整个属性动画系列的收尾部分。

我们都知道,属性动画的机制已经不是再针对于View而进行设计的了,而是一种不断地对值进行操作的机制,它可以将值赋值到指定对象的指定属性上。但是,在*大多数情况下,我相信大家主要都还是对View进行动画操作的。Android开发团队也是意识到了这一点,没有为View的动画操作提供一种更加便捷的用法确实是有点太不人性化了,于是在Android 3.1系统当中补充了ViewPropertyAnimator这个机制。

那我们先来回顾一下之前的用法吧,比如我们想要让一个TextView从常规状态变成透明状态,就可以这样写:

ObjectAnimator animator = ObjectAnimator.ofFloat(textview, “alpha”, 0f);
animator.start();
看上去复杂吗?好像也不怎么复杂,但确实也不怎么容易理解。我们要将操作的view、属性、变化的值都一起传入到ObjectAnimator.ofFloat()方法当中,虽然看上去也没写几行代码,但这不太像是我们平时使用的面向对象的思维。

那么下面我们就来看一下如何使用ViewPropertyAnimator来实现同样的效果,ViewPropertyAnimator提供了更加易懂、更加面向对象的API,如下所示:

textview.animate().alpha(0f);
果然非常简单!不过textview.animate()这个方法是怎么回事呢?animate()方法就是在Android 3.1系统上新增的一个方法,这个方法的返回值是一个ViewPropertyAnimator对象,也就是说拿到这个对象之后我们就可以调用它的各种方法来实现动画效果了,这里我们调用了alpha()方法并转入0,表示将当前的textview变成透明状态。

怎么样?比起使用ObjectAnimator,ViewPropertyAnimator的用法明显更加简单易懂吧。除此之外,ViewPropertyAnimator还可以很轻松地将多个动画组合到一起,比如我们想要让textview运动到500,500这个坐标点上,就可以这样写:

textview.animate().x(500).y(500);
可以看出,ViewPropertyAnimator是支持连缀用法的,我们想让textview移动到横坐标500这个位置上时调用了x(500)这个方法,然后让textview移动到纵坐标500这个位置上时调用了y(500)这个方法,将所有想要组合的动画通过这种连缀的方式拼接起来,这样全部动画就都会一起被执行。

那么怎样去设定动画的运行时长呢?很简单,也是通过连缀的方式设定即可,比如我们想要让动画运行5秒钟,就可以这样写:

textview.animate().x(500).y(500).setDuration(5000);
除此之外,本篇文章*部分所学的Interpolator技术我们也可以应用在ViewPropertyAnimator上面,如下所示:
textview.animate().x(500).y(500).setDuration(5000)
.setInterpolator(new BounceInterpolator());
用法很简单,同样也是使用连缀的方式。相信大家现在都已经体验出来了,ViewPropertyAnimator其实并没有什么太多的技巧可言,用法基本都是大同小异的,需要用到什么功能就连缀一下,因此更多的用法大家只需要去查阅一下文档,看看还支持哪些功能,有哪些接口可以调用就可以了。

那么除了用法之外,关于ViewPropertyAnimator有几个细节还是值得大家注意一下的:

整个ViewPropertyAnimator的功能都是建立在View类新增的animate()方法之上的,这个方法会创建并返回一个ViewPropertyAnimator的实例,之后的调用的所有方法,设置的所有属性都是通过这个实例完成的。
大家注意到,在使用ViewPropertyAnimator时,我们自始至终没有调用过start()方法,这是因为新的接口中使用了隐式启动动画的功能,只要我们将动画定义完成之后,动画就会自动启动。并且这个机制对于组合动画也同样有效,只要我们不断地连缀新的方法,那么动画就不会立刻执行,等到所有在ViewPropertyAnimator上设置的方法都执行完毕后,动画就会自动启动。当然如果不想使用这一默认机制的话,我们也可以显式地调用start()方法来启动动画。
ViewPropertyAnimator的所有接口都是使用连缀的语法来设计的,每个方法的返回值都是它自身的实例,因此调用完一个方法之后可以直接连缀调用它的另一个方法,这样把所有的功能都串接起来,我们甚至可以仅通过一行代码就完成任意复杂度的动画功能。

好的,那么到这里为止,整个Android属性动画完全解析的系列就全部结束了,感谢大家有耐心看到*后。

 

Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法

Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法

大家好,在上一篇文章当中,我们学习了Android属性动画的基本用法,当然也是*常用的一些用法,这些用法足以覆盖我们平时大多情况下的动画需求了。但是,正如上篇文章当中所说到的,属性动画对补间动画进行了很大幅度的改进,之前补间动画可以做到的属性动画也能做到,补间动画做不到的现在属性动画也可以做到了。因此,今天我们就来学习一下属性动画的高级用法,看看如何实现一些补间动画所无法实现的功能。

 

阅读本篇文章需要你对属性动画有一定的了解,并且掌握属性动画的基本用法,如果你还对属性动画不够了解的话,建议先去阅读 Android属性动画完全解析(上),初识属性动画的基本用法 。

 

ValueAnimator的高级用法

在上篇文章中介绍补间动画缺点的时候有提到过,补间动画是只能对View对象进行动画操作的。而属性动画就不再受这个限制,它可以对任意对象进行动画操作。那么大家应该还记得在上篇文章当中我举的一个例子,比如说我们有一个自定义的View,在这个View当中有一个Point对象用于管理坐标,然后在onDraw()方法当中就是根据这个Point对象的坐标值来进行绘制的。也就是说,如果我们可以对Point对象进行动画操作,那么整个自定义View的动画效果就有了。OK,下面我们就来学习一下如何实现这样的效果。

 

在开始动手之前,我们还需要掌握另外一个知识点,就是TypeEvaluator的用法。可能在大多数情况下我们使用属性动画的时候都不会用到TypeEvaluator,但是大家还是应该了解一下它的用法,以防止当我们遇到一些解决不掉的问题时能够想起来还有这样的一种解决方案。

 

那么TypeEvaluator的作用到底是什么呢?简单来说,就是告诉动画系统如何从初始值过度到结束值。我们在上一篇文章中学到的ValueAnimator.ofFloat()方法就是实现了初始值与结束值之间的平滑过度,那么这个平滑过度是怎么做到的呢?其实就是系统内置了一个FloatEvaluator,它通过计算告知动画系统如何从初始值过度到结束值,我们来看一下FloatEvaluator的代码实现:

  1. public class FloatEvaluator implements TypeEvaluator {  
  2.     public Object evaluate(float fraction, Object startValue, Object endValue) {  
  3.         float startFloat = ((Number) startValue).floatValue();  
  4.         return startFloat + fraction * (((Number) endValue).floatValue() – startFloat);  
  5.     }
  6. }

可以看到,FloatEvaluator实现了TypeEvaluator接口,然后重写evaluate()方法。evaluate()方法当中传入了三个参数,*个参数fraction非常重要,这个参数用于表示动画的完成度的,我们应该根据它来计算当前动画的值应该是多少,第二第三个参数分别表示动画的初始值和结束值。那么上述代码的逻辑就比较清晰了,用结束值减去初始值,算出它们之间的差值,然后乘以fraction这个系数,再加上初始值,那么就得到当前动画的值了。

 

好的,那FloatEvaluator是系统内置好的功能,并不需要我们自己去编写,但介绍它的实现方法是要为我们后面的功能铺路的。前面我们使用过了ValueAnimator的ofFloat()和ofInt()方法,分别用于对浮点型和整型的数据进行动画操作的,但实际上ValueAnimator中还有一个ofObject()方法,是用于对任意对象进行动画操作的。但是相比于浮点型或整型数据,对象的动画操作明显要更复杂一些,因为系统将完全无法知道如何从初始对象过度到结束对象,因此这个时候我们就需要实现一个自己的TypeEvaluator来告知系统如何进行过度。

 

下面来先定义一个Point类,如下所示:

  1. public class Point {  
  2.     private float x;  
  3.     private float y;  
  4.     public Point(float x, float y) {  
  5.         this.x = x;  
  6.         this.y = y;  
  7.     }
  8.     public float getX() {  
  9.         return x;  
  10.     }
  11.     public float getY() {  
  12.         return y;  
  13.     }
  14. }

Point类非常简单,只有x和y两个变量用于记录坐标的位置,并提供了构造方法来设置坐标,以及get方法来获取坐标。接下来定义PointEvaluator,如下所示:

  1. public class PointEvaluator implements TypeEvaluator{  
  2.     @Override  
  3.     public Object evaluate(float fraction, Object startValue, Object endValue) {  
  4.         Point startPoint = (Point) startValue;
  5.         Point endPoint = (Point) endValue;
  6.         float x = startPoint.getX() + fraction * (endPoint.getX() – startPoint.getX());  
  7.         float y = startPoint.getY() + fraction * (endPoint.getY() – startPoint.getY());  
  8.         Point point = new Point(x, y);  
  9.         return point;  
  10.     }
  11. }

可以看到,PointEvaluator同样实现了TypeEvaluator接口并重写了evaluate()方法。其实evaluate()方法中的逻辑还是非常简单的,先是将startValue和endValue强转成Point对象,然后同样根据fraction来计算当前动画的x和y的值,*后组装到一个新的Point对象当中并返回。

 

这样我们就将PointEvaluator编写完成了,接下来我们就可以非常轻松地对Point对象进行动画操作了,比如说我们有两个Point对象,现在需要将Point1通过动画平滑过度到Point2,就可以这样写:

  1. Point point1 = new Point(0, 0);  
  2. Point point2 = new Point(300, 300);  
  3. ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), point1, point2);  
  4. anim.setDuration(5000);  
  5. anim.start();

代码很简单,这里我们先是new出了两个Point对象,并在构造函数中分别设置了它们的坐标点。然后调用ValueAnimator的ofObject()方法来构建ValueAnimator的实例,这里需要注意的是,ofObject()方法要求多传入一个TypeEvaluator参数,这里我们只需要传入刚才定义好的PointEvaluator的实例就可以了。

 

好的,这就是自定义TypeEvaluator的全部用法,掌握了这些知识之后,我们就可以来尝试一下如何通过对Point对象进行动画操作,从而实现整个自定义View的动画效果。

 

新建一个MyAnimView继承自View,代码如下所示:

  1. public class MyAnimView extends View {  
  2.     public static final float RADIUS = 50f;  
  3.     private Point currentPoint;  
  4.     private Paint mPaint;  
  5.     public MyAnimView(Context context, AttributeSet attrs) {  
  6.         super(context, attrs);  
  7.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  8.         mPaint.setColor(Color.BLUE);
  9.     }
  10.     @Override  
  11.     protected void onDraw(Canvas canvas) {  
  12.         if (currentPoint == null) {  
  13.             currentPoint = new Point(RADIUS, RADIUS);  
  14.             drawCircle(canvas);
  15.             startAnimation();
  16.         } else {  
  17.             drawCircle(canvas);
  18.         }
  19.     }
  20.     private void drawCircle(Canvas canvas) {  
  21.         float x = currentPoint.getX();  
  22.         float y = currentPoint.getY();  
  23.         canvas.drawCircle(x, y, RADIUS, mPaint);
  24.     }
  25.     private void startAnimation() {  
  26.         Point startPoint = new Point(RADIUS, RADIUS);  
  27.         Point endPoint = new Point(getWidth() – RADIUS, getHeight() – RADIUS);  
  28.         ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);  
  29.         anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
  30.             @Override  
  31.             public void onAnimationUpdate(ValueAnimator animation) {  
  32.                 currentPoint = (Point) animation.getAnimatedValue();
  33.                 invalidate();
  34.             }
  35.         });
  36.         anim.setDuration(5000);  
  37.         anim.start();
  38.     }
  39. }

基本上还是很简单的,总共也没几行代码。首先在自定义View的构造方法当中初始化了一个Paint对象作为画笔,并将画笔颜色设置为蓝色,接着在onDraw()方法当中进行绘制。这里我们绘制的逻辑是由currentPoint这个对象控制的,如果currentPoint对象不等于空,那么就调用drawCircle()方法在currentPoint的坐标位置画出一个半径为50的圆,如果currentPoint对象是空,那么就调用startAnimation()方法来启动动画。

 

那么我们来观察一下startAnimation()方法中的代码,其实大家应该很熟悉了,就是对Point对象进行了一个动画操作而已。这里我们定义了一个startPoint和一个endPoint,坐标分别是View的左上角和右下角,并将动画的时长设为5秒。然后有一点需要大家注意的,就是我们通过监听器对动画的过程进行了监听,每当Point值有改变的时候都会回调onAnimationUpdate()方法。在这个方法当中,我们对currentPoint对象进行了重新赋值,并调用了invalidate()方法,这样的话onDraw()方法就会重新调用,并且由于currentPoint对象的坐标已经改变了,那么绘制的位置也会改变,于是一个平移的动画效果也就实现了。

 

下面我们只需要在布局文件当中引入这个自定义控件:

  1. <RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”  
  2.     android:layout_width=“match_parent”  
  3.     android:layout_height=“match_parent”  
  4.     >  
  5.     <com.example.tony.myapplication.MyAnimView  
  6.         android:layout_width=“match_parent”  
  7.         android:layout_height=“match_parent” />  
  8. </RelativeLayout>  

*后运行一下程序,效果如下图所示:

 

%title插图%num

 

OK!这样我们就成功实现了通过对对象进行值操作来实现动画效果的功能,这就是ValueAnimator的高级用法。

 

ObjectAnimator的高级用法

ObjectAnimator的基本用法和工作原理在上一篇文章当中都已经讲解过了,相信大家都已经掌握。那么大家应该都还记得,我们在吐槽补间动画的时候有提到过,补间动画是只能实现移动、缩放、旋转和淡入淡出这四种动画操作的,功能限定死就是这些,基本上没有任何扩展性可言。比如我们想要实现对View的颜色进行动态改变,补间动画是没有办法做到的。

 

但是属性动画就不会受这些条条框框的限制,它的扩展性非常强,对于动态改变View的颜色这种功能是完全可是胜任的,那么下面我们就来学习一下如何实现这样的效果。

 

大家应该都还记得,ObjectAnimator内部的工作机制是通过寻找特定属性的get和set方法,然后通过方法不断地对值进行改变,从而实现动画效果的。因此我们就需要在MyAnimView中定义一个color属性,并提供它的get和set方法。这里我们可以将color属性设置为字符串类型,使用#RRGGBB这种格式来表示颜色值,代码如下所示:

  1. public class MyAnimView extends View {  
  2.     …
  3.     private String color;  
  4.     public String getColor() {  
  5.         return color;  
  6.     }
  7.     public void setColor(String color) {  
  8.         this.color = color;  
  9.         mPaint.setColor(Color.parseColor(color));
  10.         invalidate();
  11.     }
  12.     …
  13. }

注意在setColor()方法当中,我们编写了一个非常简单的逻辑,就是将画笔的颜色设置成方法参数传入的颜色,然后调用了invalidate()方法。这段代码虽然只有三行,但是却执行了一个非常核心的功能,就是在改变了画笔颜色之后立即刷新视图,然后onDraw()方法就会调用。在onDraw()方法当中会根据当前画笔的颜色来进行绘制,这样颜色也就会动态进行改变了。

 

那么接下来的问题就是怎样让setColor()方法得到调用了,毫无疑问,当然是要借助ObjectAnimator类,但是在使用ObjectAnimator之前我们还要完成一个非常重要的工作,就是编写一个用于告知系统如何进行颜色过度的TypeEvaluator。创建ColorEvaluator并实现TypeEvaluator接口,代码如下所示:

  1. public class ColorEvaluator implements TypeEvaluator {  
  2.     private int mCurrentRed = –1;  
  3.     private int mCurrentGreen = –1;  
  4.     private int mCurrentBlue = –1;  
  5.     @Override  
  6.     public Object evaluate(float fraction, Object startValue, Object endValue) {  
  7.         String startColor = (String) startValue;
  8.         String endColor = (String) endValue;
  9.         int startRed = Integer.parseInt(startColor.substring(1, 3), 16);  
  10.         int startGreen = Integer.parseInt(startColor.substring(3, 5), 16);  
  11.         int startBlue = Integer.parseInt(startColor.substring(5, 7), 16);  
  12.         int endRed = Integer.parseInt(endColor.substring(1, 3), 16);  
  13.         int endGreen = Integer.parseInt(endColor.substring(3, 5), 16);  
  14.         int endBlue = Integer.parseInt(endColor.substring(5, 7), 16);  
  15.         // 初始化颜色的值  
  16.         if (mCurrentRed == –1) {  
  17.             mCurrentRed = startRed;
  18.         }
  19.         if (mCurrentGreen == –1) {  
  20.             mCurrentGreen = startGreen;
  21.         }
  22.         if (mCurrentBlue == –1) {  
  23.             mCurrentBlue = startBlue;
  24.         }
  25.         // 计算初始颜色和结束颜色之间的差值  
  26.         int redDiff = Math.abs(startRed – endRed);  
  27.         int greenDiff = Math.abs(startGreen – endGreen);  
  28.         int blueDiff = Math.abs(startBlue – endBlue);  
  29.         int colorDiff = redDiff + greenDiff + blueDiff;  
  30.         if (mCurrentRed != endRed) {  
  31.             mCurrentRed = getCurrentColor(startRed, endRed, colorDiff, 0,  
  32.                     fraction);
  33.         } else if (mCurrentGreen != endGreen) {  
  34.             mCurrentGreen = getCurrentColor(startGreen, endGreen, colorDiff,
  35.                     redDiff, fraction);
  36.         } else if (mCurrentBlue != endBlue) {  
  37.             mCurrentBlue = getCurrentColor(startBlue, endBlue, colorDiff,
  38.                     redDiff + greenDiff, fraction);
  39.         }
  40.         // 将计算出的当前颜色的值组装返回  
  41.         String currentColor = “#” + getHexString(mCurrentRed)  
  42.                 + getHexString(mCurrentGreen) + getHexString(mCurrentBlue);
  43.         return currentColor;  
  44.     }
  45.     /** 
  46.      * 根据fraction值来计算当前的颜色。 
  47.      */  
  48.     private int getCurrentColor(int startColor, int endColor, int colorDiff,  
  49.             int offset, float fraction) {  
  50.         int currentColor;  
  51.         if (startColor > endColor) {  
  52.             currentColor = (int) (startColor – (fraction * colorDiff – offset));  
  53.             if (currentColor < endColor) {  
  54.                 currentColor = endColor;
  55.             }
  56.         } else {  
  57.             currentColor = (int) (startColor + (fraction * colorDiff – offset));  
  58.             if (currentColor > endColor) {  
  59.                 currentColor = endColor;
  60.             }
  61.         }
  62.         return currentColor;  
  63.     }
  64.     /** 
  65.      * 将10进制颜色值转换成16进制。 
  66.      */  
  67.     private String getHexString(int value) {  
  68.         String hexString = Integer.toHexString(value);
  69.         if (hexString.length() == 1) {  
  70.             hexString = “0” + hexString;  
  71.         }
  72.         return hexString;  
  73.     }
  74. }

这大概是我们整个动画操作当中*复杂的一个类了。没错,属性动画的高级用法中*有技术含量的也就是如何编写出一个合适的TypeEvaluator。好在刚才我们已经编写了一个PointEvaluator,对它的基本工作原理已经有了了解,那么这里我们主要学习一下ColorEvaluator的逻辑流程吧。

 

首先在evaluate()方法当中获取到颜色的初始值和结束值,并通过字符串截取的方式将颜色分为RGB三个部分,并将RGB的值转换成十进制数字,那么每个颜色的取值范围就是0-255。接下来计算一下初始颜色值到结束颜色值之间的差值,这个差值很重要,决定着颜色变化的快慢,如果初始颜色值和结束颜色值很相近,那么颜色变化就会比较缓慢,而如果颜色值相差很大,比如说从黑到白,那么就要经历255*3这个幅度的颜色过度,变化就会非常快。

 

那么控制颜色变化的速度是通过getCurrentColor()这个方法来实现的,这个方法会根据当前的fraction值来计算目前应该过度到什么颜色,并且这里会根据初始和结束的颜色差值来控制变化速度,*终将计算出的颜色进行返回。

 

*后,由于我们计算出的颜色是十进制数字,这里还需要调用一下getHexString()方法把它们转换成十六进制字符串,再将RGB颜色拼装起来之后作为*终的结果返回。

 

好了,ColorEvaluator写完之后我们就把*复杂的工作完成了,剩下的就是一些简单调用的问题了,比如说我们想要实现从蓝色到红色的动画过度,历时5秒,就可以这样写:

  1. ObjectAnimator anim = ObjectAnimator.ofObject(myAnimView, “color”, new ColorEvaluator(),   
  2.     “#0000FF”, “#FF0000”);  
  3. anim.setDuration(5000);  
  4. anim.start();

用法非常简单易懂,相信不需要我再进行解释了。

 

接下来我们需要将上面一段代码移到MyAnimView类当中,让它和刚才的Point移动动画可以结合到一起播放,这就要借助我们在上篇文章当中学到的组合动画的技术了。修改MyAnimView中的代码,如下所示:

  1. public class MyAnimView extends View {  
  2.     …
  3.     private void startAnimation() {  
  4.         Point startPoint = new Point(RADIUS, RADIUS);  
  5.         Point endPoint = new Point(getWidth() – RADIUS, getHeight() – RADIUS);  
  6.         ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);  
  7.         anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
  8.             @Override  
  9.             public void onAnimationUpdate(ValueAnimator animation) {  
  10.                 currentPoint = (Point) animation.getAnimatedValue();
  11.                 invalidate();
  12.             }
  13.         });
  14.         ObjectAnimator anim2 = ObjectAnimator.ofObject(this, “color”, new ColorEvaluator(),   
  15.                 “#0000FF”, “#FF0000”);  
  16.         AnimatorSet animSet = new AnimatorSet();  
  17.         animSet.play(anim).with(anim2);
  18.         animSet.setDuration(5000);  
  19.         animSet.start();
  20.     }
  21. }

可以看到,我们并没有改动太多的代码,重点只是修改了startAnimation()方法中的部分内容。这里先是将颜色过度的代码逻辑移动到了startAnimation()方法当中,注意由于这段代码本身就是在MyAnimView当中执行的,因此ObjectAnimator.ofObject()的*个参数直接传this就可以了。接着我们又创建了一个AnimatorSet,并把两个动画设置成同时播放,动画时长为五秒,*后启动动画。现在重新运行一下代码,效果如下图所示:

 

%title插图%num

 

OK,位置动画和颜色动画非常融洽的结合到一起了,看上去效果还是相当不错的,这样我们就把ObjectAnimator的高级用法也掌握了。

 

好的,通过本篇文章的学习,我们对属性动画已经有了颇为深刻的认识,那么本篇文章的内容到此为止,下篇文章当中将会介绍更多关于属性动画的其它技巧,感兴趣的朋友请继续阅读 Android属性动画完全解析(下),Interpolator和ViewPropertyAnimator的用法 。

 

Android属性动画完全解析(上),初识属性动画的基本用法

Android属性动画完全解析(上),初识属性动画的基本用法

 

 

在手机上去实现一些动画效果算是件比较炫酷的事情,因此Android系统在一开始的时候就给我们提供了两种实现动画效果的方式,逐帧动画(frame-by-frame animation)和补间动画(tweened animation)。逐帧动画的工作原理很简单,其实就是将一个完整的动画拆分成一张张单独的图片,然后再将它们连贯起来进行播放,类似于动画片的工作原理。补间动画则是可以对View进行一系列的动画操作,包括淡入淡出、缩放、平移、旋转四种。

 

然而自Android 3.0版本开始,系统给我们提供了一种全新的动画模式,属性动画(property animation),它的功能非常强大,弥补了之前补间动画的一些缺陷,几乎是可以完全替代掉补间动画了。对于逐帧动画和补间动画的用法,我不想再多讲,它们的技术已经比较老了,而且网上资料也非常多,那么今天我们这篇文章的主题就是对Android属性动画进行一次完全解析。

 

为什么要引入属性动画?

Android之前的补间动画机制其实还算是比较健全的,在android.view.animation包下面有好多的类可以供我们操作,来完成一系列的动画效果,比如说对View进行移动、缩放、旋转和淡入淡出,并且我们还可以借助AnimationSet来将这些动画效果组合起来使用,除此之外还可以通过配置Interpolator来控制动画的播放速度等等等等。那么这里大家可能要产生疑问了,既然之前的动画机制已经这么健全了,为什么还要引入属性动画呢?

 

其实上面所谓的健全都是相对的,如果你的需求中只需要对View进行移动、缩放、旋转和淡入淡出操作,那么补间动画确实已经足够健全了。但是很显然,这些功能是不足以覆盖所有的场景的,一旦我们的需求超出了移动、缩放、旋转和淡入淡出这四种对View的操作,那么补间动画就不能再帮我们忙了,也就是说它在功能和可扩展方面都有相当大的局限性,那么下面我们就来看看补间动画所不能胜任的场景。

 

注意上面我在介绍补间动画的时候都有使用“对View进行操作”这样的描述,没错,补间动画是只能够作用在View上的。也就是说,我们可以对一个Button、TextView、甚至是LinearLayout、或者其它任何继承自View的组件进行动画操作,但是如果我们想要对一个非View的对象进行动画操作,抱歉,补间动画就帮不上忙了。可能有的朋友会感到不能理解,我怎么会需要对一个非View的对象进行动画操作呢?这里我举一个简单的例子,比如说我们有一个自定义的View,在这个View当中有一个Point对象用于管理坐标,然后在onDraw()方法当中就是根据这个Point对象的坐标值来进行绘制的。也就是说,如果我们可以对Point对象进行动画操作,那么整个自定义View的动画效果就有了。显然,补间动画是不具备这个功能的,这是它的*个缺陷。

 

然后补间动画还有一个缺陷,就是它只能够实现移动、缩放、旋转和淡入淡出这四种动画操作,那如果我们希望可以对View的背景色进行动态地改变呢?很遗憾,我们只能靠自己去实现了。说白了,之前的补间动画机制就是使用硬编码的方式来完成的,功能限定死就是这些,基本上没有任何扩展性可言。

 

*后,补间动画还有一个致命的缺陷,就是它只是改变了View的显示效果而已,而不会真正去改变View的属性。什么意思呢?比如说,现在屏幕的左上角有一个按钮,然后我们通过补间动画将它移动到了屏幕的右下角,现在你可以去尝试点击一下这个按钮,点击事件是*对不会触发的,因为实际上这个按钮还是停留在屏幕的左上角,只不过补间动画将这个按钮绘制到了屏幕的右下角而已。

 

也正是因为这些原因,Android开发团队决定在3.0版本当中引入属性动画这个功能,那么属性动画是不是就把上述的问题全部解决掉了?下面我们就来一起看一看。

 

新引入的属性动画机制已经不再是针对于View来设计的了,也不限定于只能实现移动、缩放、旋转和淡入淡出这几种动画操作,同时也不再只是一种视觉上的动画效果了。它实际上是一种不断地对值进行操作的机制,并将值赋值到指定对象的指定属性上,可以是任意对象的任意属性。所以我们仍然可以将一个View进行移动或者缩放,但同时也可以对自定义View中的Point对象进行动画操作了。我们只需要告诉系统动画的运行时长,需要执行哪种类型的动画,以及动画的初始值和结束值,剩下的工作就可以全部交给系统去完成了。

 

既然属性动画的实现机制是通过对目标对象进行赋值并修改其属性来实现的,那么之前所说的按钮显示的问题也就不复存在了,如果我们通过属性动画来移动一个按钮,那么这个按钮就是真正的移动了,而不再是仅仅在另外一个位置绘制了而已。

 

好了,介绍了这么多,相信大家已经对属性动画有了一个*基本的认识了,下面我们就来开始学习一下属性动画的用法。

 

ValueAnimator

ValueAnimator是整个属性动画机制当中*核心的一个类,前面我们已经提到了,属性动画的运行机制是通过不断地对值进行操作来实现的,而初始值和结束值之间的动画过渡就是由ValueAnimator这个类来负责计算的。它的内部使用一种时间循环的机制来计算值与值之间的动画过渡,我们只需要将初始值和结束值提供给ValueAnimator,并且告诉它动画所需运行的时长,那么ValueAnimator就会自动帮我们完成从初始值平滑地过渡到结束值这样的效果。除此之外,ValueAnimator还负责管理动画的播放次数、播放模式、以及对动画设置监听器等,确实是一个非常重要的类。

 

但是ValueAnimator的用法却一点都不复杂,我们先从*简单的功能看起吧,比如说想要将一个值从0平滑过渡到1,时长300毫秒,就可以这样写:

  1. ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
  2. anim.setDuration(300);  
  3. anim.start();

怎么样?很简单吧,调用ValueAnimator的ofFloat()方法就可以构建出一个ValueAnimator的实例,ofFloat()方法当中允许传入多个float类型的参数,这里传入0和1就表示将值从0平滑过渡到1,然后调用ValueAnimator的setDuration()方法来设置动画运行的时长,*后调用start()方法启动动画。

 

用法就是这么简单,现在如果你运行一下上面的代码,动画就会执行了。可是这只是一个将值从0过渡到1的动画,又看不到任何界面效果,我们怎样才能知道这个动画是不是已经真正运行了呢?这就需要借助监听器来实现了,如下所示:

  1. ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
  2. anim.setDuration(300);  
  3. anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
  4.     @Override  
  5.     public void onAnimationUpdate(ValueAnimator animation) {  
  6.         float currentValue = (float) animation.getAnimatedValue();  
  7.         Log.d(“TAG”, “cuurent value is ” + currentValue);  
  8.     }
  9. });
  10. anim.start();

可以看到,这里我们通过addUpdateListener()方法来添加一个动画的监听器,在动画执行的过程中会不断地进行回调,我们只需要在回调方法当中将当前的值取出并打印出来,就可以知道动画有没有真正运行了。运行上述代码,控制台打印如下所示:

 

%title插图%num

 

从打印日志的值我们就可以看出,ValueAnimator确实已经在正常工作了,值在300毫秒的时间内从0平滑过渡到了1,而这个计算工作就是由ValueAnimator帮助我们完成的。另外ofFloat()方法当中是可以传入任意多个参数的,因此我们还可以构建出更加复杂的动画逻辑,比如说将一个值在5秒内从0过渡到5,再过渡到3,再过渡到10,就可以这样写:

  1. ValueAnimator anim = ValueAnimator.ofFloat(0f, 5f, 3f, 10f);
  2. anim.setDuration(5000);  
  3. anim.start();

当然也许你并不需要小数位数的动画过渡,可能你只是希望将一个整数值从0平滑地过渡到100,那么也很简单,只需要调用ValueAnimator的ofInt()方法就可以了,如下所示:

  1. ValueAnimator anim = ValueAnimator.ofInt(0, 100);  

ValueAnimator当中*常用的应该就是ofFloat()和ofInt()这两个方法了,另外还有一个ofObject()方法,我会在下篇文章进行讲解。

 

那么除此之外,我们还可以调用setStartDelay()方法来设置动画延迟播放的时间,调用setRepeatCount()和setRepeatMode()方法来设置动画循环播放的次数以及循环播放的模式,循环模式包括RESTART和REVERSE两种,分别表示重新播放和倒序播放的意思。这些方法都很简单,我就不再进行详细讲解了。

 

ObjectAnimator

相比于ValueAnimator,ObjectAnimator可能才是我们*常接触到的类,因为ValueAnimator只不过是对值进行了一个平滑的动画过渡,但我们实际使用到这种功能的场景好像并不多。而ObjectAnimator则就不同了,它是可以直接对任意对象的任意属性进行动画操作的,比如说View的alpha属性。

 

不过虽说ObjectAnimator会更加常用一些,但是它其实是继承自ValueAnimator的,底层的动画实现机制也是基于ValueAnimator来完成的,因此ValueAnimator仍然是整个属性动画当中*核心的一个类。那么既然是继承关系,说明ValueAnimator中可以使用的方法在ObjectAnimator中也是可以正常使用的,它们的用法也非常类似,这里如果我们想要将一个TextView在5秒中内从常规变换成全透明,再从全透明变换成常规,就可以这样写:

  1. ObjectAnimator animator = ObjectAnimator.ofFloat(textview, “alpha”, 1f, 0f, 1f);  
  2. animator.setDuration(5000);  
  3. animator.start();

可以看到,我们还是调用了ofFloat()方法来去创建一个ObjectAnimator的实例,只不过ofFloat()方法当中接收的参数有点变化了。这里*个参数要求传入一个object对象,我们想要对哪个对象进行动画操作就传入什么,这里我传入了一个textview。第二个参数是想要对该对象的哪个属性进行动画操作,由于我们想要改变TextView的不透明度,因此这里传入”alpha”。后面的参数就是不固定长度了,想要完成什么样的动画就传入什么值,这里传入的值就表示将TextView从常规变换成全透明,再从全透明变换成常规。之后调用setDuration()方法来设置动画的时长,然后调用start()方法启动动画,效果如下图所示:

 

%title插图%num

 

学会了这一个用法之后,其它的用法我们就可以举一反三了,那比如说我们想要将TextView进行一次360度的旋转,就可以这样写:

  1. ObjectAnimator animator = ObjectAnimator.ofFloat(textview, “rotation”, 0f, 360f);  
  2. animator.setDuration(5000);  
  3. animator.start();

可以看到,这里我们将第二个参数改成了”rotation”,然后将动画的初始值和结束值分别设置成0和360,现在运行一下代码,效果如下图所示:

 

%title插图%num

 

那么如果想要将TextView先向左移出屏幕,然后再移动回来,就可以这样写:

  1. float curTranslationX = textview.getTranslationX();  
  2. ObjectAnimator animator = ObjectAnimator.ofFloat(textview, “translationX”, curTranslationX, -500f, curTranslationX);  
  3. animator.setDuration(5000);  
  4. animator.start();

这里我们先是调用了TextView的getTranslationX()方法来获取到当前TextView的translationX的位置,然后ofFloat()方法的第二个参数传入”translationX”,紧接着后面三个参数用于告诉系统TextView应该怎么移动,现在运行一下代码,效果如下图所示:

 

%title插图%num

 

然后我们还可以TextView进行缩放操作,比如说将TextView在垂直方向上放大3倍再还原,就可以这样写:

  1. ObjectAnimator animator = ObjectAnimator.ofFloat(textview, “scaleY”, 1f, 3f, 1f);  
  2. animator.setDuration(5000);  
  3. animator.start();

这里将ofFloat()方法的第二个参数改成了”scaleY”,表示在垂直方向上进行缩放,现在重新运行一下程序,效果如下图所示:

 

%title插图%num

 

到目前为止,ObjectAnimator的用法还算是相当简单吧,但是我相信肯定会有不少朋友现在心里都有同样一个疑问,就是ofFloat()方法的第二个参数到底可以传哪些值呢?目前我们使用过了alpha、rotation、translationX和scaleY这几个值,分别可以完成淡入淡出、旋转、水平移动、垂直缩放这几种动画,那么还有哪些值是可以使用的呢?其实这个问题的答案非常玄乎,就是我们可以传入任意的值到ofFloat()方法的第二个参数当中。任意的值?相信这很出乎大家的意料吧,但事实就是如此。因为ObjectAnimator在设计的时候就没有针对于View来进行设计,而是针对于任意对象的,它所负责的工作就是不断地向某个对象中的某个属性进行赋值,然后对象根据属性值的改变再来决定如何展现出来。

 

那么比如说我们调用下面这样一段代码:

  1. ObjectAnimator.ofFloat(textview, “alpha”, 1f, 0f);  

其实这段代码的意思就是ObjectAnimator会帮我们不断地改变textview对象中alpha属性的值,从1f变化到0f。然后textview对象需要根据alpha属性值的改变来不断刷新界面的显示,从而让用户可以看出淡入淡出的动画效果。

 

那么textview对象中是不是有alpha属性这个值呢?没有,不仅textview没有这个属性,连它所有的父类也是没有这个属性的!这就奇怪了,textview当中并没有alpha这个属性,ObjectAnimator是如何进行操作的呢?其实ObjectAnimator内部的工作机制并不是直接对我们传入的属性名进行操作的,而是会去寻找这个属性名对应的get和set方法,因此alpha属性所对应的get和set方法应该就是:

  1. public void setAlpha(float value);  
  2. public float getAlpha();  

那么textview对象中是否有这两个方法呢?确实有,并且这两个方法是由View对象提供的,也就是说不仅TextView可以使用这个属性来进行淡入淡出动画操作,任何继承自View的对象都可以的。

 

既然alpha是这个样子,相信大家一定已经明白了,前面我们所用的所有属性都是这个工作原理,那么View当中一定也存在着setRotation()、getRotation()、setTranslationX()、getTranslationX()、setScaleY()、getScaleY()这些方法,不信的话你可以到View当中去找一下。

 

组合动画

独立的动画能够实现的视觉效果毕竟是相当有限的,因此将多个动画组合到一起播放就显得尤为重要。幸运的是,Android团队在设计属性动画的时候也充分考虑到了组合动画的功能,因此提供了一套非常丰富的API来让我们将多个动画组合到一起。

 

实现组合动画功能主要需要借助AnimatorSet这个类,这个类提供了一个play()方法,如果我们向这个方法中传入一个Animator对象(ValueAnimator或ObjectAnimator)将会返回一个AnimatorSet.Builder的实例,AnimatorSet.Builder中包括以下四个方法:

  • after(Animator anim)   将现有动画插入到传入的动画之后执行
  • after(long delay)   将现有动画延迟指定毫秒后执行
  • before(Animator anim)   将现有动画插入到传入的动画之前执行
  • with(Animator anim)   将现有动画和传入的动画同时执行

好的,有了这四个方法,我们就可以完成组合动画的逻辑了,那么比如说我们想要让TextView先从屏幕外移动进屏幕,然后开始旋转360度,旋转的同时进行淡入淡出操作,就可以这样写:

  1. ObjectAnimator moveIn = ObjectAnimator.ofFloat(textview, “translationX”, -500f, 0f);  
  2. ObjectAnimator rotate = ObjectAnimator.ofFloat(textview, “rotation”, 0f, 360f);  
  3. ObjectAnimator fadeInOut = ObjectAnimator.ofFloat(textview, “alpha”, 1f, 0f, 1f);  
  4. AnimatorSet animSet = new AnimatorSet();  
  5. animSet.play(rotate).with(fadeInOut).after(moveIn);
  6. animSet.setDuration(5000);  
  7. animSet.start();

可以看到,这里我们先是把三个动画的对象全部创建出来,然后new出一个AnimatorSet对象之后将这三个动画对象进行播放排序,让旋转和淡入淡出动画同时进行,并把它们插入到了平移动画的后面,*后是设置动画时长以及启动动画。运行一下上述代码,效果如下图所示:

 

%title插图%num

 

Animator监听器

在很多时候,我们希望可以监听到动画的各种事件,比如动画何时开始,何时结束,然后在开始或者结束的时候去执行一些逻辑处理。这个功能是完全可以实现的,Animator类当中提供了一个addListener()方法,这个方法接收一个AnimatorListener,我们只需要去实现这个AnimatorListener就可以监听动画的各种事件了。

 

大家已经知道,ObjectAnimator是继承自ValueAnimator的,而ValueAnimator又是继承自Animator的,因此不管是ValueAnimator还是ObjectAnimator都是可以使用addListener()这个方法的。另外AnimatorSet也是继承自Animator的,因此addListener()这个方法算是个通用的方法。

 

添加一个监听器的代码如下所示:

  1. anim.addListener(new AnimatorListener() {  
  2.     @Override  
  3.     public void onAnimationStart(Animator animation) {  
  4.     }
  5.     @Override  
  6.     public void onAnimationRepeat(Animator animation) {  
  7.     }
  8.     @Override  
  9.     public void onAnimationEnd(Animator animation) {  
  10.     }
  11.     @Override  
  12.     public void onAnimationCancel(Animator animation) {  
  13.     }
  14. });

可以看到,我们需要实现接口中的四个方法,onAnimationStart()方法会在动画开始的时候调用,onAnimationRepeat()方法会在动画重复执行的时候调用,onAnimationEnd()方法会在动画结束的时候调用,onAnimationCancel()方法会在动画被取消的时候调用。

 

但是也许很多时候我们并不想要监听那么多个事件,可能我只想要监听动画结束这一个事件,那么每次都要将四个接口全部实现一遍就显得非常繁琐。没关系,为此Android提供了一个适配器类,叫作AnimatorListenerAdapter,使用这个类就可以解决掉实现接口繁琐的问题了,如下所示:

  1. anim.addListener(new AnimatorListenerAdapter() {  
  2. });

这里我们向addListener()方法中传入这个适配器对象,由于AnimatorListenerAdapter中已经将每个接口都实现好了,所以这里不用实现任何一个方法也不会报错。那么如果我想监听动画结束这个事件,就只需要单独重写这一个方法就可以了,如下所示:

  1. anim.addListener(new AnimatorListenerAdapter() {  
  2.     @Override  
  3.     public void onAnimationEnd(Animator animation) {  
  4.     }
  5. });

使用XML编写动画

我们可以使用代码来编写所有的动画功能,这也是*常用的一种做法。不过,过去的补间动画除了使用代码编写之外也是可以使用XML编写的,因此属性动画也提供了这一功能,即通过XML来完成和代码一样的属性动画功能。

 

通过XML来编写动画可能会比通过代码来编写动画要慢一些,但是在重用方面将会变得非常轻松,比如某个将通用的动画编写到XML里面,我们就可以在各个界面当中轻松去重用它。

 

如果想要使用XML来编写动画,首先要在res目录下面新建一个animator文件夹,所有属性动画的XML文件都应该存放在这个文件夹当中。然后在XML文件中我们一共可以使用如下三种标签:

  • <animator>  对应代码中的ValueAnimator
  • <objectAnimator>  对应代码中的ObjectAnimator
  • <set>  对应代码中的AnimatorSet

那么比如说我们想要实现一个从0到100平滑过渡的动画,在XML当中就可以这样写:

  1. <animator xmlns:android=“http://schemas.android.com/apk/res/android”  
  2.     android:valueFrom=“0”  
  3.     android:valueTo=“100”  
  4.     android:valueType=“intType”/>  

而如果我们想将一个视图的alpha属性从1变成0,就可以这样写:

  1. <objectAnimator xmlns:android=“http://schemas.android.com/apk/res/android”  
  2.     android:valueFrom=“1”  
  3.     android:valueTo=“0”  
  4.     android:valueType=“floatType”  
  5.     android:propertyName=“alpha”/>  

其实XML编写动画在可读性方面还是挺高的,上面的内容相信不用我做解释大家也都看得懂吧。

 

另外,我们也可以使用XML来完成复杂的组合动画操作,比如将一个视图先从屏幕外移动进屏幕,然后开始旋转360度,旋转的同时进行淡入淡出操作,就可以这样写:

  1. <set xmlns:android=“http://schemas.android.com/apk/res/android”  
  2.     android:ordering=“sequentially” >  
  3.     <objectAnimator  
  4.         android:duration=“2000”  
  5.         android:propertyName=“translationX”  
  6.         android:valueFrom=“-500”  
  7.         android:valueTo=“0”  
  8.         android:valueType=“floatType” >  
  9.     </objectAnimator>  
  10.     <set android:ordering=“together” >  
  11.         <objectAnimator  
  12.             android:duration=“3000”  
  13.             android:propertyName=“rotation”  
  14.             android:valueFrom=“0”  
  15.             android:valueTo=“360”  
  16.             android:valueType=“floatType” >  
  17.         </objectAnimator>  
  18.         <set android:ordering=“sequentially” >  
  19.             <objectAnimator  
  20.                 android:duration=“1500”  
  21.                 android:propertyName=“alpha”  
  22.                 android:valueFrom=“1”  
  23.                 android:valueTo=“0”  
  24.                 android:valueType=“floatType” >  
  25.             </objectAnimator>  
  26.             <objectAnimator  
  27.                 android:duration=“1500”  
  28.                 android:propertyName=“alpha”  
  29.                 android:valueFrom=“0”  
  30.                 android:valueTo=“1”  
  31.                 android:valueType=“floatType” >  
  32.             </objectAnimator>  
  33.         </set>  
  34.     </set>  
  35. </set>  

这段XML实现的效果和我们刚才通过代码来实现的组合动画的效果是一模一样的,每个参数的含义都非常清楚,相信大家都是一看就懂,我就不再一一解释了。

 

*后XML文件是编写好了,那么我们如何在代码中把文件加载进来并将动画启动呢?只需调用如下代码即可:

  1. Animator animator = AnimatorInflater.loadAnimator(context, R.animator.anim_file);
  2. animator.setTarget(view);
  3. animator.start();

调用AnimatorInflater的loadAnimator来将XML动画文件加载进来,然后再调用setTarget()方法将这个动画设置到某一个对象上面,*后再调用start()方法启动动画就可以了,就是这么简单。

 

好的,通过本篇文章的学习,我相信大家已经对属性动画的基本用法已经有了一个相当不错的认识,并把*常用的一些功能都掌握好了,那么本篇文章的内容就到这里,下篇文章当中会继续介绍属性动画,讲解ValueAnimator和ObjectAnimator的高级用法,感兴趣的朋友请继续阅读 Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法 。

Android Parcel对象详解

Android Parcel对象详解

关于Parcel的使用

在分析Parcel之前,首先按照分析流程,介绍下关于Parcel的相关常规使用。

首先是关于Parcel的获取:

Parcel parcle = Parcel.Obtain();

 

额,这感觉似曾相识啊,我们这里大胆猜测Parcel的初始化也是由其对象池进行初始化的。在得到了Parcel对象之后,下一步的工作。嗯,我想起来,应该介绍下Parcel的作用吧:

其实看到这篇文章的各位,应该也不需要这种科普吧,哈哈。我从源码注释中截取如下:

*Container for a message (data and object references) that can
* be sent through an IBinder. A Parcel can contain both flattened data
* that will be unflattened on the other side of the IPC (using the various
* methods here for writing specific types, or the general

从这段注释中可以看出,Parcel是一个容器,他可以包含数据或者是对象引用,并且能够用于Binder的传输。同时支持序列化以及跨进程之后进行反序列化,同时其提供了很多方法帮助开发者完成这些功能。ok,对这里差不多明朗了,Parcel主要就是用来进行IPC通信的。当然不仅仅是Binder这一种跨进程通信。

接下来回到这题,既然Parcel是一个容器,那么肯定需要向其中传入数据才行啊,没错,所以在初始化Parcel之后,需要进行如下操作:

parcel.writeInt(int val);

 

向Parcel中传入一个Int型的数据,接下来还有:

parcel.writeString(String val);

 

向Parcel中传入一个String型的数据。

这里只以这两种*为常见的数据类型的写入作为例子,实际上Parcel所支持的数据类型可多了去了,具体可以如下图所示:

这里写图片描述

在完成了数据的写入之后,就需要进行数据的序列化:

parcel.marshall();

 

在经过上一步的处理之后,返回了一个byte数组,主要的IPC相关的操作主要就是围绕此byte数组进行的。同时,由于parcel的读写都是一个指针操作的,这一步涉及到native的操作,所以,在将数据写入之后,需要将指针手动指向到*初的位置,即如下的操作:

parcel.setDataPosition(0);

 

到此处,Parcel的这一步操作还没有收尾,想想前面parcel的Obtain()方法,我们有理由相信,parcel的销毁应该是使用了对应的recycle()方法。

所以此处有:

parcel.recycle();

 

将此Parcel对象进行释放,完成了IPC操作的一半。至于是如何将数据传输过去的,暂不进行展开。此处在IPC的另一端的Parcel的获取处理。

再进行了IPC的操作之后,一般读取出来的就是之前序列化的byte数组,所以,首先要进行一个反序列化操作,即如下的操作:

parcel.unmarshall(byte[] data, int offest, int length);

 

其中的参数分别是这个byte数组,以及读取偏移量,以及数组的长度。

此时得到的parcel就是一个正常的parcel对象,这时就可以将之前我们所存入的数据按照顺序进行获取,即:

parcel.readInt();

 

以及

parcel.readString();

 

读取完毕之后,同样是一个parcel的回收操作:

parcel.recycle();

 

以上就是parcel的常规使用,获取有些朋友不太知道parcel的使用场景,其实*常见的,在我们编写完AIDL的接口之后,IDE会自动生成一个对应的.java文件,这个java文件就是实际用来进行aidl的通信的,在这个实现里面,数据的传递就是使用的parcel,当然还有其他的应用场景,这里只说了一个大家都比较常见的实践。

关于Parcel的实现

之前有提到过,parcel的使用对于java开发者来说,还是比较陌生的,像*了指针的操作,所以基本可以确定java层对于parcel的处理仅仅是一个封装代理,实际的实现在c/c++ native。既然这样的话,我们就应该想到,parcel的使用同样涉及到jni的使用。所以我们目前的思路就是在源码中找到parcel的三层代码(Java-Jni-C)。

我的具体做法是直接使用 everything 在源码目录下搜索 parcel,然后根据之前的思路进行包的导出,我的分析基础就是以下的几个包里的实现:

Java层:

\frameworks\base\core\java\android\os

 

JNI:

\frameworks\base\core\jni

 

native:

\frameworks\native\libs\binder

 

然后导入这几个包中的文件方便检索:

这里写图片描述

根据我们以上的使用顺序来进行分析,首先需要进行一个Parcel的获取,看看Java层的实现:

 /**
     * Retrieve a new Parcel object from the pool.
     */
    public static Parcel obtain() {
        final Parcel[] pool = sOwnedPool;
        synchronized (pool) {
            Parcel p;
            for (int i=0; i<POOL_SIZE; i++) {
                p = pool[i];
                if (p != null) {
                    pool[i] = null;
                    if (DEBUG_RECYCLE) {
                        p.mStack = new RuntimeException();
                    }
                    return p;
                }
            }
        }
        return new Parcel(0);
    }

 

从注释也可以看出,Parcle的初始化,主要是使用一个对象池进行的,这样可以提高性能以及内存消耗。首先要明确的是,源码中定义的池子有两个:

    private static final int POOL_SIZE = 6;
    private static final Parcel[] sOwnedPool = new Parcel[POOL_SIZE];
    private static final Parcel[] sHolderPool = new Parcel[POOL_SIZE];

 

从名字也可以看出,不同池子的作用,sOwnedPool 这个池子主要就是用来存储parcel的,Obtain()方法首先会去检索池子中的parcel对象,若是能取出parcel,那么先将这个这个parcel返回,同时将这个位置置空。若是现在连池子都不存在的话,那么就直接新建一个parcel对象。这里的实现与Handler中的message采用同样的处理。

我们了解了获取之后,比较关心的就是如何去新建一个parcel对象,也就是new这个过程,那么看看此处中的parcel构造方法:

private Parcel(int nativePtr) {
        if (DEBUG_RECYCLE) {
            mStack = new RuntimeException();
        }
        //Log.i(TAG, "Initializing obj=0x" + Integer.toHexString(obj), mStack);
        init(nativePtr);
    }

 

可以看到,在此处参数名称被称为:nativePtr,这个大家都比较熟悉了,ptr嘛,指的就是一个指针,这里又是一个封装,需要继续深入看实现:

private void init(int nativePtr) {
        if (nativePtr != 0) {
            mNativePtr = nativePtr;
            mOwnsNativeParcelObject = false;
        } else {
            mNativePtr = nativeCreate();
            mOwnsNativeParcelObject = true;
        }
    }

 

这里首先对参数进行检查,这里因为初始化传入的参数是0,那么直接执行nativeCreate(),并且将标志位mOwnsNativeParcelObject 置为true,表示这个 parcel已经在native进行了创建。

此处的ativeCreate()是一个native方法,其具体实现已经切换到native环境了,那么我们此时的分析就要从jni进行了,经过检索,在jni的代码中,其实现为以下函数:

static jint android_os_Parcel_create(JNIEnv* env, jclass clazz)
{
    Parcel* parcel = new Parcel();
    return reinterpret_cast<jint>(parcel);
}

 

这是一个jni的实现,首先是调用了native的初始化,并且,返回操作这个对象的指针:

Parcel::Parcel()
{
    initState();
}

 

是一个c++的构造方法,关于析构方法,暂时不管,其中的init实现为:

void Parcel::initState()
{
    mError = NO_ERROR;
    mData = 0;
    mDataSize = 0;
    mDataCapacity = 0;
    mDataPos = 0;
    ALOGV("initState Setting data size of %p to %d\n", this, mDataSize);
    ALOGV("initState Setting data pos of %p to %d\n", this, mDataPos);
    mObjects = NULL;
    mObjectsSize = 0;
    mObjectsCapacity = 0;
    mNextObjectHint = 0;
    mHasFds = false;
    mFdsKnown = true;
    mAllowFds = true;
    mOwner = NULL;
}

 

可以看出,对parcel的初始化,只是在native层初始化了一些数据值。

在完成初始化之后,就将这个操作指针给返回。这样就完成了parcel的初始化。

初始化完毕之后,就可以进行数据的写入了,首先写入一个int型数据,其java层实现如下:

 /**
     * Write an integer value into the parcel at the current dataPosition(),
     * growing dataCapacity() if needed.
     */
    public final void writeInt(int val) {
        nativeWriteInt(mNativePtr, val);
    }

 

可以看出,在这里java层就纯粹是一个对于native实现的封装了,这时候的分析来到jni:

static void android_os_Parcel_writeInt(JNIEnv* env, jclass clazz, jint nativePtr, jint val) {
    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
    const status_t err = parcel->writeInt32(val);
    if (err != NO_ERROR) {
        signalExceptionForError(env, clazz, err);
    }
}

 

在这里我们要特别注意两个参数,一个是之前传上去的指针以及需要保存的int数据,这两个值分别是:

jint nativePtr, jint val

 

首先是根据这个指针,这里说一下,指针实际上就是一个整型地址值,所以这里使用强转将int值转化为parcel类型的指针是可行的,然后使用这个指针来操作native的parcel对象,即:

const status_t err = parcel->writeInt32(val);

 

这里注意到我们是写入了一个32位的int值,这个点一定要注意,32位,4个字节。

深入进去看看实现:

status_t Parcel::writeInt32(int32_t val)
{
    return writeAligned(val);
}

 

可以看出,这里实际上调用了:

writeAligned(val);

来进行数据的写入,这里理解下align的意思,实际上是一个对齐写入,怎么个对齐法,看看:

template<class T>
status_t Parcel::writeAligned(T val) {
    COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE(sizeof(T)) == sizeof(T));

    if ((mDataPos+sizeof(val)) <= mDataCapacity) {
restart_write:
        *reinterpret_cast<T*>(mData+mDataPos) = val;
        return finishWrite(sizeof(val));
    }

    status_t err = growData(sizeof(val));
    if (err == NO_ERROR) goto restart_write;
    return err;
}

 

在这个方法中首先是一个断言检查,然后对输入的参数取size值,再加上之前已经移动的位置,判断是否超过了该Pacel所定义的能力值mDataCapacity。

若是超过了能力值的话,那么直接将能力值进行扩大,扩大的值是val值的大小,比如,int值是32bit,那么就增加4个字节,返回的结果是状态值,若是没有出错的话,就利用goto语句执行,这里的goto的语句只要是一个指针的操作,将指针移动到端点,然后写入val的size值。这里可以看出这个函数的意义,因为无论是否超过能力值它都会写入T类型值的size值。

到这里,Parcel就写入了一个Int型的值。

同样的思路,大家可以参考以上的分析,继续进行Parcel一个常规使用的分析,我之前是想将全部的实现都分析出来的,但是后来发现,大体的思路都差不多,这么写的话,会多出来很多废话,所以接下来的分析,大家如果有兴趣的话,就继续分析下去,欢迎一起进行讨论!

另外,在分析过程中,我对Android的JNI调用进行一番探索,总之一句话就是说Jvm环境切换到Native环境之中后,Java如何通过Java层声明的native方法来查找到对应的JNI方法的?因为我对JVM的实现这一部分没有太多了解,所以只能从Android源码中代码层面上来分析,至少在Android中:

在切换到native环境之后,实际上,这两种函数的映射是由一个多重数组来进行管理的,具体如下:

static const JNINativeMethod gParcelMethods[] = {
    {"nativeDataSize",            "(I)I", (void*)android_os_Parcel_dataSize},
    {"nativeDataAvail",           "(I)I", (void*)android_os_Parcel_dataAvail},
    {"nativeDataPosition",        "(I)I", (void*)android_os_Parcel_dataPosition},
    {"nativeDataCapacity",        "(I)I", (void*)android_os_Parcel_dataCapacity},
    {"nativeSetDataSize",         "(II)V", (void*)android_os_Parcel_setDataSize},
    {"nativeSetDataPosition",     "(II)V", (void*)android_os_Parcel_setDataPosition},
    {"nativeSetDataCapacity",     "(II)V", (void*)android_os_Parcel_setDataCapacity},

    {"nativePushAllowFds",        "(IZ)Z", (void*)android_os_Parcel_pushAllowFds},
    {"nativeRestoreAllowFds",     "(IZ)V", (void*)android_os_Parcel_restoreAllowFds},

    {"nativeWriteByteArray",      "(I[BII)V", (void*)android_os_Parcel_writeNative},
    {"nativeWriteInt",            "(II)V", (void*)android_os_Parcel_writeInt},
    {"nativeWriteLong",           "(IJ)V", (void*)android_os_Parcel_writeLong},
    {"nativeWriteFloat",          "(IF)V", (void*)android_os_Parcel_writeFloat},
    {"nativeWriteDouble",         "(ID)V", (void*)android_os_Parcel_writeDouble},
    {"nativeWriteString",         "(ILjava/lang/String;)V", (void*)android_os_Parcel_writeString},
    {"nativeWriteStrongBinder",   "(ILandroid/os/IBinder;)V", (void*)android_os_Parcel_writeStrongBinder},
    {"nativeWriteFileDescriptor", "(ILjava/io/FileDescriptor;)V", (void*)android_os_Parcel_writeFileDescriptor},

    {"nativeCreateByteArray",     "(I)[B", (void*)android_os_Parcel_createByteArray},
    {"nativeReadInt",             "(I)I", (void*)android_os_Parcel_readInt},
    {"nativeReadLong",            "(I)J", (void*)android_os_Parcel_readLong},
    {"nativeReadFloat",           "(I)F", (void*)android_os_Parcel_readFloat},
    {"nativeReadDouble",          "(I)D", (void*)android_os_Parcel_readDouble},
    {"nativeReadString",          "(I)Ljava/lang/String;", (void*)android_os_Parcel_readString},
    {"nativeReadStrongBinder",    "(I)Landroid/os/IBinder;", (void*)android_os_Parcel_readStrongBinder},
    {"nativeReadFileDescriptor",  "(I)Ljava/io/FileDescriptor;", (void*)android_os_Parcel_readFileDescriptor},

........

 

以下还有很多映射关系,这样通过映射就可以将函数给进行对应了,但是还有一点,这个东西是何时,以及何处进行调用的,这个展开说又是一个漫长的故事了,所以这一段我也不进行分析了,大家知道有这么一个东西就ok了,当然欢迎一起进行讨论哦!

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