android动画过程如何不三季稻被打断腿

最近在Android上做了一些动画效果,网上查了一些资料,有各种各样的使用方式,于是乘热打铁,想具体分析一下动画是如何实现的,Animation, Animator都有哪些区别等等。
首先说Animation(android.view.animation.Animation)对象。
无论是用纯java代码构建Animation对象,还是通过xml文件定义Animation,其实最终的结果都是
Animation a = new AlphaAnimation();
Animation b = new ScaleAnimation();
Animation c = new RotateAnimation();
Animation d = new TranslateAnimation();
分别是透明度,缩放,旋转,位移四种动画效果。
而我们使用的时候,一般是用这样的形式:
View.startAnimation(a);
那么就来看看View中的startAnimation()方法。
1.View.startAnimation(Animation)
先是调用View.setAnimation(Animation)方法给自己设置一个Animation对象,这个对象是View类中的一个名为mCurrentAnimation的成员变量。
然后它调用invalidate()来重绘自己。
我想,既然setAnimation()了,那么它要用的时候,肯定要getAnimation(),找到这个方法在哪里调用就好了。于是通过搜索,在View.draw(Canvas, ViewGroup, long)方法中发现了它的调用,代码片段如下:
2.View.draw(Canvas, ViewGroup, long)
其中调用了View.drawAnimation()方法。
3.View.drawAnimation(ViewGroup, long, Animation, boolean)
代码片段如下:
其中调用了Animation.getTransformation()方法。
4.Animation.getTransformation(long, Transformation, float)
该方法直接调用了两个参数Animation.getTransformation()方法。
5.Animation.getTransformation(long, Transformation)
该方法先将参数currentTime处理成一个float表示当前动画进度,比如说,一个2000ms的动画,已经执行了1000ms了,那么进度就是0.5或者说50%。
然后将进度值传入插值器(Interpolator)得到新的进度值,前者是均匀的,随着时间是一个直线的线性关系,而通过插值器计算后得到的是一个曲线的关系。
然后将新的进度值和Transformation对象传入applyTranformation()方法中。
6.Animation.applyTransformation(float, Transformation)
Animation的applyTransformation()方法是空实现,具体实现它的是Animation的四个子类,而该方法正是真正的处理动画变化的过程。分别看下四个子类的applyTransformation()的实现。
ScaleAnimation
AlphaAnimation
RotateAnimation
TranslateAnimation
可见applyTransformation()方法就是动画具体的实现,系统会以一个比较高的频率来调用这个方法,一般情况下60FPS,是一个非常流畅的画面了,也就是16ms,为了验证这一点,我在applyTransformation方法中加入计算时间间隔并打印的代码进行验证,代码如下:
最终得到的log如下图所示:
右侧是&手动&计算出来的时间差,有一定的波动,但大致上是16-17ms的样子,左侧是日志打印的时间,时间非常规则的相差20ms。
于是,根据以上的结果,可以得出以下内容:
1.首先证明了一点,Animation.applyTransformation()方法,是动画具体的调用方法,我们可以覆写这个方法,快速的制作自己的动画。
2.另一点,为什么是16ms左右调用这个方法呢?是谁来控制这个频率的?
对于以上的疑问,我有两个猜测:
1.系统自己以postDelayed(this, 16)的形式调用的这个方法。具体的写法,请参考
2.系统一个死循环疯狂的调用,运行一系列方法走到这个位置的间隔刚好是16ms左右,如果主线程卡了,这个间隔就变长了。
为了找到答案,我在Stack Overflow上发帖问了下,然后得到一个情报,那就是让我去看看Choreographer(android.view.Choreographer)类。
1.Choreographer的构造方法
看了下Choreographer类的构造方法,是private的,不允许new外部类new,于是又发现了它有一个静态的getInstance()方法,那么,我需要找到getInstance()方法被谁调用了,就可以知道Choreographer对象在什么地方被使用。一查,发现Choreographer.getInstance()在ViewRootImpl的构造方法中被调用。以下代码是ViewRootImpl.ViewRootImpl(Context, Display)的片段。
OK,找到了ViewRootImpl中拥有一个mChoreographer对象,接下来,我需要去找,它如何被使用了,调用了它的哪些方法。于是发现如下代码:
在scheduleTraversals()方法中,发现了这个对象的使用。
2.Choreographer.postCallback(int, Runnable, Object)
该方法辗转调用了两个内部方法,最终是调用了Choreographer.postCallbackDelayedInternal()方法。
3.Choreographer.postCallbackDelayedInternal(int, Object, Object, long)
这个方法中,
1.首先拿到当前的时间。
这里参数中有一个delay,它的值可以具体查看一下,你会发现它就是一个静态常量,定义在Choreographer类中,它的值是10ms。也就是说,理想情况下,所有的时间都是以100FPS来运行的。
2.将要执行的内容加入到一个mCallbackQueues中。
3.然后执行scheduleFrameLocked()或者发送一个Message。
接着我们看Choreographer.scheduleFrameLocked(long)方法
4.Choreographer.scheduleFrameLocked(long)
if判断进去的部分是是否使用垂直同步,暂时不考虑。
else进去的部分,还是将消息发送到mHandler对象中。那我们就直接来看mHandler对象就好了
5.Choreographer.FrameHandler
mHandler实例的类型是FrameHandler,它的定义就在Choreographer类中,代码如下:
它的处理方法中有三个分支,但最终都会调用这个doFrame()方法。
6.Choreographer.doFrame()
doFrame()方法巴拉巴拉一大段,但在下面有非常工整的一段代码,一下就吸引了我的眼球。
它调用了三次doCallbacks()方法,暂且不说这个方法是干什么的,但从它的第一个参数可以看到分别是输入(INPUT),动画(ANIMATION),遍历(TRAVERSAL)。
于是,我先是看了下这三个常量的意义。下图所示:
显然,注释是说:输入事件最先处理,然后处理动画,最后才处理view的布局和绘制。接下来我们看看Choreographer.doCallbacks()里面做了什么。
7.Choreographer.doCallbacks(int, long)
这个方法的操作非常统一,有三种不同类型的操作(输入,动画,遍历),但在这里却看不见这些具体事件的痕迹,这里我们不得不分析一下mCallbackQueues这个成员变量了。
mCallbackQueues是一个CallbackQueue对象数组。而它的下标,其意义并不是指元素1,元素2,元素3&&而是指类型,请看上面doCallbacks()的代码,参数callbackType传给了mCallbackQueues[callbackType]中,而callbackType是什么呢?
其实就是前面说到的三个常量,CALLBACK_INPUT, CALLBACK_ANIMATION, CALLBACK_TRAVERVAL。
那么只需要根据不同的callbackType,就可以从这个数组里面取出不同类型的CallbackQueue对象来。
那么CallbackQueue又是什么呢?
CallbackQueue是Choreographer的一个内部类,其中我认为有两个很重要的方法,分别是:extractDueCallbacksLocked(long)和addCallbackLocked(long, Object, Object)。
先说addCallbackLocked(long, Object, Object)。
1.CallbackQueue.addCallbackLocked(long, Object, Object)
首先它通过一个内部方法构建了一个CallbackRecord对象,然后后面的if判断和while循环,大致上是将参数中的对象链接在CallbackRecord的尾部。其实CallbackRecord就是一个链表结构的对象。
2.CallbackQueue.extractDueCallbacksLocked(long)
这个方法是根据当前的时间,选出执行链表中与该时间最近的一个操作来处理,实际上,我们可以通俗的理解为&跳帧&。
想象一下,如果主线程运行的非常快速,非常流畅,每一步都能在10ms内准时运行到,那么我们的执行链表中的元素始终只有一个。
如果主线程中做了耗时操作,那么各种事件一直在往各自的链表中添加,但是当主线程有空来执行的时候,发现链表已经那么多积累的过期的事件了,那么就直接选择最后一个来执行,那么界面上看起来,就是卡顿了一下。
到这里为止,我们得出以下结论:
1.控制外部输入事件处理,动画执行,UI变化都是在同一个类中做的处理,即是Choreographer,其中它规定的了理想的运行间隔为10ms,因为各种操作需要花费一定的时间,所以外部执行的间隔统计出来是大约16ms。
2.在Choreographer对象中有三条链表,分别保存着待处理的输入事件,待处理的动画事件,待处理的遍历事件。
3.每次执行的时候,Choreographer会根据当前的时间,只处理事件链表中最后一个事件,当有耗时操作在主线程时,事件不能及时执行,就会出现所谓的&跳帧&,&卡顿&现象。
4.Choreographer的共有方法postCallback(callbackType, Object)是往事件链表中放事件的方法。而doFrame()是消耗这些事件的方法。
事到如今,已经探究出不少有用的细节。这里,又给自己提出一个问题,根据以上的事实,那么,只需要找到哪些东西再往这三条链表中放事件呢?
于是进一步探究一下。我们只需要找到postCallback()被哪些方法调用了即可。
于是请点击,通过grepcode列举了调用postCallback()的方法。
搞明白了Choreographer的工作原理,再去看ObjectAnimator,ValueAnimator的实现,就非常的轻松了。
ObjectAnimator.start()方法实际上是辗转几次调用了ValueAnimator的start()方法,ValueAnimator.start()又调用了一个临时变量animationHandler.start()。
animationHandler实际上是一个Runnable,其中start()方法调用了scheduleAnimation()。
而这个方法:
调用了postCallback()方法。
将this(Runnable)post之后,实际上肯定就是要执行Runnable.run()方法
run()方法中又调用了doAnimationFrame()方法。这个方法具体的实现了动画的某一帧的过程,然后再次调用了scheduleAnimation()方法。
就相当于postDelayed(this, 16)这种方式了。
到这里为止,对Animation原理的分析就到此结束了,本来只想分析下Animation的实现过程,没想到顺带研究了一下Choreographer的工作原理,今天收获还是不少。
其实还有好多疑问,技术学习也一天急不得,靠的是每日慢慢的积累,相信总有一天,各种疑惑都会迎刃而解。
阅读(...) 评论()Android Animation运行原理详解 - 简书
Android Animation运行原理详解
作为Android程序员,或者是想要去模仿一些酷炫的效果,或者是为了实现视觉的变态需求,或者是压抑不住内心的创造欲想要炫技,我们不可避免地需要做各种动画。Android中,动画主要分为帧动画、插间动画以及属性动画。帧动画最为简单,是用一系列的素材作为关键帧逐帧播放,常用于制作加载动画,其工作量主要在设计部分;插间动画与属性动画则更多地是需要开发通过控制各种动画参数来实现,只有系统地理解Android中动画运行的原理,才能创作出更出色的动画,属性动画在下一篇文章中分析,本文主要分享我在探索插间动画运行原理过程中的一些收获,包括:Matrix如何控制动画参数;动画中各参数具体起什么作用;透明度动画、缩放动画、平移动画以及旋转动画的运行逻辑;动画在View的绘制过程中如何被应用。
2. Matrix介绍
在Android中,Matrix是一个3 x 3的矩阵:
Matrix 3 x 3 矩阵
Matrix可将一个点映射到另一个点,矩阵中包含了处理缩放、透视以及平移的区域,从而可用于控制实现平移、缩放、旋转等动画效果。强烈建议阅读以更深入地了解Matrix实现动画控制原理,这里仅摘录其中的关键信息:
结论一:设对给定的图像依次进行了基本变化F1、F2、F3…..、Fn,它们的变化矩阵分别为T1、T2、T3…..、Tn,图像复合变化的矩阵T可以表示为:T = TnTn-1…T1。
结论二:Preconcats matrix相当于右乘矩阵,Postconcats
matrix相当于左乘矩阵。
Matrix还给我们提供了各种友好的接口来组合生成复杂的动画,举个例子:假如我们想要实现一个平移(a,b)之后旋转(c,d)的动画,那用Matrix的实现代码就是这样的:
Matrix matrix = new Matrix();
matrix.setTranslate(a, b);
matrix.postScale(c, d);
3. Animation运行原理分析
(1)基本属性介绍
使用过Animation的同学对下述基本属性应该非常熟悉,这里为了文章完整性,特地赘述一下:
mStartTime:动画实际开始时间
mStartOffset:动画延迟时间
mFillEnabled:mFillBefore及mFillAfter是否使能
mFillBefore:动画结束之后是否需要进行应用动画
mFillAfter:动画开始之前是否需要进行应用动画
mDuration:单次动画运行时长
mRepeatMode:动画重复模式(RESTART、REVERSE)
mRepeatCount:动画重复次数(INFINITE,直接值)
mInterceptor:动画插间器
mListener:动画开始、结束、重复回调监听器
虽然大部分都知道上面这些属性怎么用,但是可能还是有一些人对这些字段为什么有这样的作用不甚明白,于是我们就来分析一下。
(2)计算动画数据
Animation在其getTransformation函数被调用时会计算一帧动画数据,而上面这些属性基本都是在计算动画数据时发光发热,我们先看看getTransformation函数的运行逻辑:
若startTime为START_ON_FIRST_FRAME(值为-1)时,将startTime设定为curTime
计算当前动画进度:
normalizedTime = (curTime - (startTime + startOffset))/duration
若mFillEnabled==false:将normalisedTime夹逼至[0.0f, 1.0f]
判断是否需要计算动画数据:
若normalisedTime在[0.0f, 1.0f],需计算动画数据
若normalisedTime不在[0.0f, 1.0f]:
normalisedTime&0.0f, 仅当mFillBefore==true时才计算动画数据
normalisedTime&1.0f, 仅当mFillAfter==true时才计算动画数据
若需需要计算动画数据:
若当前为第一帧动画,触发mListener.onAnimationStart
若mFillEnabled==false:将normalisedTime夹逼至[0.0f, 1.0f]
根据插间器mInterpolator调整动画进度:
interpolatedTime = mInterpolator.getInterpolation(normalizedTime)
若动画反转标志位mCycleFlip为true,则
interpolatedTime = 1.0 - normalizedTime
调用动画更新函数applyTransformation(interpolatedTime, transformation)计算出动画数据
若夹逼之前normalisedTime大于1.0f, 则判断是否需继续执行动画:
已执行次数mRepeatCount等于需执行次数mRepeated
若未触发mListener.onAnimationEnd,则触发之
已执行次数mRepeatCount不等于需执行次数mRepeated
自增mRepeatCount
重置mStartTime为-1
若mRepeatMode为REVERSE,则取反mCycleFlip
触发mListener.onAnimationRepeat
这一段是根据getTransformation源码分析出来的,建议有兴趣的同学可以直接查看源码。上面这段分析留了一个不小的悬念,那就是动画更新函数是什么鬼,这个函数在Animation这个抽象类中仅仅是个钩子函数,由其子类提供具体实现,于是自然而然地引出了我们的下一个主题:主流动画介绍。
(3)主流动画分析
AlphaAnimation:透明度动画
mFromAlpha:起始透明度
mToAlpha:终止透明度
applyTransformation函数实现
transformation.setAlpha(mFromAlpha + ((mToAlpha - mFromAlpha) * interpolatedTime))
ScaleAnimation:缩放动画
mFromX:起始X值
mToX:终止X值
mFromY:起始Y值
mToY:终止Y值
mPivotX:缩放中心点X坐标
mPivotY:缩放中心点Y坐标
属性计算逻辑
mFromX、mToX、mFromY、mToY计算
Float类型scale直接值
Faction类型相对值
相对于自身(%):百分比转换为float直接值
相对于父亲(%p):根据父亲size计算出size直接值,然后计算与本身size的百分比,最后转换为float直接值
Dimension类型size直接值:计算与本身size的百分比,然后转换为float直接值
mPivotX、mPivotY计算
ABSOLUTE类型直接值
RELATIVE_TO_SELF类型相对值:相对值乘以自身size得到直接值
RELATIVE_TO_PARENT类型相对值:相对值乘以父亲size得到直接值
applyTransformation函数实现
sx = mFromX + ((mToX - mFromX) * interpolatedTime)
sy = mFromY + ((mToY - mFromY) * interpolatedTime)
是否设定缩放中心点:
若mPivotX==0 且 mPivotY==0:transformation.getMatrix().setScale(sx, sy)
否则:transformation.getMatrix().setScale(sx, sy, mPivotX, mPivotY)
TranslateAnimation:平移动画
mFromXDelta
mFromYDelta
属性计算逻辑
同ScaleAnimation中mPivotX、mPivotY的计算逻辑
applyTransformation函数实现
dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime)
dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime)
transformation.getMatrix().setTranslate(dx, dy)
RotateAnimation:旋转动画
mFromDegrees
mToDegrees
属性计算逻辑
mFromDegrees、mToDegrees均为角度(°)绝对值
mPivotX、mPivotY计算逻辑同ScaleAnimation
applyTransformation函数实现
是否设定缩放中心点:
若mPivotX==0 且 mPivotY==0:transformation.getMatrix().setScale(sx, sy)
否则:transformation.getMatrix().setScale(sx, sy, mPivotX, mPivotY)
透明度、缩放、平移以及旋转是最基本的动画,通过组合这些动画可以实现各种不一样的酷炫的效果,但是怎么才能实现这些动画的组合,这就不得不提到AnimationSet了。
(4) AnimationSet分析
AnimationSet是动画集合,用于组合运行多个动画,仅支持playTogether模式。
AnimationSet继承了Animation的字段,但是字段的应用有一些变化:
duration, repeatMode, fillBefore, fillAfter:这些属性会传递应用到所有的子Animation
repeatCount, fillEnabled:这些属性在AnimationSet中不被应用
startOffset, shareInterpolator:这些属性仅用于AnimationSet,不会传递至子Animation
4.0以前在xml中设置duration, repeatMode, fillBefore, fillAfter, startOffset不会被应用,但是4.0之后再xml中设定这些属性跟运行时设定效果一致
一些值的计算逻辑:
duration:
缺省时,取所有子Animation中最长的duration;
已设定时,返回mDuration
hasAlpha、willChangeTransformationMatrix、willChangeBounds:当有子Animation时,所有子Animation的值取“或”
startTime:取所有子Animation中最小的startTime
子Animation中startOffset处理:
保存子Animation的原始startOffset
设置子Animation的startOffset为原始startOffset与AnimationSet的startOffset之和
保存的原始startOffset在AnimationSet.clear是用于恢复各子Animation的startOffset
applyTransformation函数实现
顺序调用子Animation的applyTransformation,然后利用Transformation.compose组合所有子Animation返回的Transformation作为该AnimationSet当前帧的变换状态
started及more值取所有子Animation对应值的“或”
ended值取所有子Animation对应值的“与”
当started第一次为true时,调用AnimationSet的mListener.onAnimationStart
当ended第一次为true(此时所有子Animation均结束)时,调用AnimationSet的mListener.onAnimationEnd
介绍完了主流动画以及组合动画,是不是Animation就介绍完了?其实不然,里面还漏掉了一个重要角色,那就是计算得到的动画数据是用什么存储的。实际上,Animation的动画函数getTransformation目的在于生成当前帧的一个Transformation,这个Transformation采用alpha以及Matrix存储了一帧动画的数据,Transformation包含两种模式:
alpha模式:用于支持透明度动画
matrix模式:用于支持缩放、平移以及旋转动画
同时,Transformation还提供了许多两个接口用于组合多个Transformation:
compose:前结合(alpha相乘、矩阵右乘、边界叠加)
postCompose:后结合(alpha相乘、矩阵左乘、边界叠加)
至此,Animation本身算介绍完整了,还差一个可用于从XML中构建动画以及插间器的AnimationUtils,这里就不做具体分析了,有兴趣的同学可以自行研究。但是,到现在为止,我们还没讲明白是:getTransformation这个函数究竟是在哪里调用的?计算得到的动画数据又是怎么被应用的?慌不要慌,待我娓娓道来,当这些问题揭秘之后,我们就知道为什么Animation这个包要放在android.view下面以及Animation完成之后为什么View本身的属性不会被改变,于是也就知道插间动画(Animation)跟属性动画(Animator)本质上的区别在哪了。
4. Animation的调用
要了解Animation的调用源头,要从Animation的基本使用View.startAnimation开始寻根溯源:
public void startAnimation(Animation animation) {
animation.setStartTime(Animation.START_ON_FIRST_FRAME);
setAnimation(animation);
invalidateParentCaches();
invalidate(true);
通过invalidate(true)函数会触发View的重新绘制,由于View的绘制流程并不是本文的重点,因此这里仅说明从View.draw是怎么走到对Animation的处理函数的:
View.draw(Canvas)
—& ViewGroup.dispatchDraw(Canvas)
—& ViewGroup.drawChild(Canvas, View, long)
—& View.draw(Canvas, ViewGroup, long)
—& View.applyLegacyAnimation(ViewGroup, long, Animation, boolean)
而View.applyLegacyAnimation就是Animation大显神通的舞台,其核心代码主要分三个部分:
初始化Animation(仅初始化一次)
调用Animation.initialize(width, height, parentWidth, parentHeight),通过View及ParentView的Size来解析Animation中的相关数据;
调用Animation.initializeInvalidateRegion(left, top, right, bottom)来设定动画的初始区域,并在fillBefore为true时计算Animation动画进度为0.0f的数据
调用getTransformation根据当前绘制事件生成Animation中对应帧的动画数据
根据动画数据设定重绘制区域
若仅为Alpha动画,此时动画区域为View的当前区域,且不会产生变化
若包含非Alpha动画,此时动画区域需要调用Animation.getInvalidateRegion进行计算,该函数会根据上述生成动画数据Thransformation中的Matrix进行计算,并与之前的动画区域执行unio操作,从而获取动画的完整区域
调用ViewGroup.invalidate(int l, int t, int r, int b)设定绘制区域
当View.applyLegacyAnimation调用完成之后,View此次绘制的动画数据就构建完成,之后便回到View.draw(Canvas, ViewGroup, long)应用动画数据对视图进行绘制刷新,其核心代码如下:
if (transformToApply != null) {
if (concatMatrix) {
if (drawingWithRenderNode) {
// 应用动画数据
renderNode.setAnimationMatrix(transformToApply.getMatrix());
canvas.translate(-transX, -transY);
// 应用动画数据
canvas.concat(transformToApply.getMatrix());
canvas.translate(transX, transY);
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
float transformAlpha = transformToApply.getAlpha();
if (transformAlpha & 1) {
// 应用动画数据
alpha *= transformA
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
重点来了,大家看到Animation产生的动画数据实际并不是应用在View本身的,而是应用在RenderNode或者Canvas上的,这就是为什么Animation不会改变View的属性的根本所在。另一方面,我们知道Animation仅在View被绘制的时候才能发挥自己的价值,这也是为什么插间动画被放在Android.view包内,因为它跟View是真心相爱的。
文章到这,其实差不多可以结束了,但是创作动画过程中总是会被用到的一个神器还没出现,这让我有些不舍,尽管有太多人讲解这一神器,但是我还是毅然决然地决定抄一遍书,一来表示我对这一神器的爱,另一方面也是希望让文章更完整。
5. 插间器(Interpolator)
如果没有插间器,Animation应该按照时间来线性计算每一个时间点的动画帧数据;当时当加入插件器之后,我们计算动画帧数据时就可以更加的富有创造力,我可以随心所欲地计算任一时间点的动画帧数据,可以新加速在减速,也可以先减速在加速,总之一句话,我的地盘我做主。按照剧情的发展,接下来我应该介绍常用插间器了,但是作为一个有态度的程序员,我是不会按常理出牌的,想要了解常用插间器的实现原理,建议阅读。
其实很早之前就看过Animation的源码,但是当时因为懒并没有写文章做笔记,这次因为项目需要优化动画,于是又重新撸了一遍,在此撰文为记,以备后用。当然,也希望这篇分享能给大家一些收获,非常感谢你的阅读,如果有浪费到你的时间,也就浪费了,权当看了一章凑字数的小说,233333~~~
只是一个程序员;
在前面一篇文章中,我们分析了Android应用程序窗口的绘图表面的创建过程。Android应用程序窗口的绘图表面在创建完成之后,我们就可以从上到下地绘制它里面的各个视图了,即各个UI元素了。不过在绘制这些UI元素之前,我们还需要从上到下地测量它们实际所需要的大小,以及对它们...
1 背景 不能只分析源码呀,分析的同时也要整理归纳基础知识,刚好有人微博私信让全面说说Android的动画,所以今天来一发Android应用的各种Animation大集合。英文厉害的请直接移步参考Android Developer。 Android系统提供了很多丰富的API...
用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金Cover 有什么料? 从这篇文章中你能获得这些料: 知道setContentView()之后发生了什么? ... Android 获取 View 宽高的常用正确方式,避免为零 - 掘金相信有很多朋友...
心灵鸡汤:真正成功的人生,不在于成就的大小,而在于你是否努力地去实现自我,喊出自己的声音,走出属于自己的道路。 如果大家想看更多关于Android基础夯实系列博文,请移步到我的博客:Ryane's Blog 摘要 不积跬步,无以至千里;不积小流,无以成江海。学习任何东西我们...
3.0以前,android支持两种动画模式,tween animation,frame animation,在android3.0中又引入了一个新的动画系统:property animation,这三种动画模式在SDK中被称为property animation,view ...
大邺和小学妹吵架了,小学妹还在因为他给她买了一双鞋子生气,这个鞋子她在商店里看过,在橱窗最显眼的位置。小学妹心疼大邺,责备他买那么贵的东西。“因为你喜欢啊。”大邺斩钉截铁地说。“我什么时候说过了”小学妹疑惑地望着他。“那天和你去见磊磊的女朋友,她穿了一双新鞋,我看你一直盯着...
美联储缩表节奏屡屡上演“狼来了”的故事,一举一动让外汇市场剧烈波动。但中国商业银行的缩表瘦身进程却已经写在了上市银行的半年报中 统计数据显示:今年上半年,四大行员工总数较去年底减少了超过2.5万人,近半在A股上市银行的利息净收入出现减少。 今年上半年,在A股上市的25家银行...
最刻骨铭心的一段恋爱发生在我25岁的时候,我一直觉得这次才是我真正的初恋,我体会到了什么是真正的爱情,体会到了真正的爱情是无私无畏的。 第一次见到他是在戏剧学院的黑匣子,我坐在舞台的一角,离着舞台很近。我不知道他有没有看见我,我觉得他看见了。 他上身穿着一件藏青色带着泡泡袖...
《日课110|怎样(不)说谎》 不想回答或者不能回答的问题如何回答,这也是困扰我很久的问题,读完文章算是得到一点思路。 之前我抱定只说真话不说谎话的策略--实在有点说谎话需要构造并且还需要用一个个谎话去维护当初说过的谎话,为了让自己活得轻松些,我选择说真话。 但是,真话并不...
原文链接 http://avenwu.net//conflicts-between-appbarlayout-and-listview-footer/?utm_source=tuicool&utm_medium=referral Google新出的mat...

我要回帖

更多关于 魔兽三季稻被打断腿 的文章

 

随机推荐