QQ上發送么么噠時候,彈出彈跳錶情,是如何實現的?


如果題主問的是「如何實現有么么噠就出效果」這種邏輯,那麼做字元判斷調用方法就可以了,但是我想這種問題不值得上知乎問一回是吧。

那麼,我猜題主問的是:表情彈來彈去的動畫如何實現的吧;

ok,我們把場景還原一遍

效果gif:

http://img1.ph.126.net/r_YH8TofKCrSxAjp_5i5Kg==/6631845120421043830.gif

可以看出,表情做了一個典型的彈跳自由落體運動,以下內容不超過中學物理知識,請放心食用

我們把它彈跳的運動軌跡拆解一下:

看起來很簡單是吧。。。沒有專業的可視化軟體,我就拿嘴說吧,說一下每一次彈跳的運動實現;

老規矩,做題之前,要列出已知變數和未知變數:

其中,startX,startY,startVX,currentX,currentY,currentVX ,currentVY均為未知變數。只有startVY已知;

OK,我們先假定一個坐標點的集合pointList ,裡面儲存一些坐標點,模擬要落在對話框的坐標;

那麼,從第一個點到第二個點,要怎麼做呢?

我們把表示球在第幾個坐標的參數初始化,startX,startY為第一個坐標點坐標,並觸發一個線程,開始繪製界面;

我們再回到第一張圖,我們把這個稱為第一階段運動:

X軸方向很好理解,在落地之前,做勻速直線運動:

t是怎麼來?t是Y軸方向,自由落體 S1和S2運動時間的總和;

把這一段軌跡轉化為代碼,則是:

X軸方向,做勻速直線運動:

S1階段運動,y軸受重力逐漸Vy變為0:

S2自由落體運動:

OK,第一階段的運動完成,現在到了第二階段,小球觸到目標坐標後,有一個速度衰減,然後做彈起運動:

在做一個循環,把各個坐標點都遍歷一遍就可以了;

完成效果圖:

http://img2.ph.126.net/BFOVd6RZbvIDhcZtErUgUw==/6631884702839640449.gif

我要吐槽一下,知乎為什麼沒有gif支持...

OK,運動軌跡的實現就是這樣了,如果不是做安卓開發的人,看到這就可以了。

代碼放在git上了,git地址

wuyongxiang/QQ_circle

包括上一次qq拉伸效果的代碼

qq上拖動未讀消息那種動態效果是如何實現的? - 祥子的回答 - 知乎

--------------------------------------------分割線-------------------------------------

然後就是QQ是怎麼把這個運動放進ListView中的?這個就很頭疼了,我先mark一下,再好好想想;

1.18 5:30

看了下評論,說是把這層布局放在listview上就好了,實際上並不行的

[IMG]http://image18-c.poco.cn/mypoco/myphoto/20170118/17/1744793392017011817275606.gif?360x634_110[/IMG]

這個是把listview和這個view嵌套在ScrollView中,設置list滿屏高的效果。

如果重寫onMeasure拉長listiew,則不能獲取到item中的坐標點,目前沒有想到完美的解決辦法;

有個思路可以在每個item中都嵌入這個view,根據運動效果分開顯示,效果肯定是好的,就是實現起來很費時間,又不賺錢,對於面向人民幣編程的我沒啥吸引力

(/= _ =)/~┴┴

回答了兩個關於騰訊的問題了,騰訊產品部是不得來誇誇我?

@騰訊 @騰訊科技


利益相關

當年Android版QQ彈跳動畫的碼農

說下大概思路吧,年代久遠細節不太記得了。

這裡借用一下 @祥子的圖片

以A-B-C為例

整個過程分兩大部:

第一部分. 計算當前坐標

0. 獲得listView中下一個item的top坐標,轉換為屏幕坐標

1. 在A點給一個初始的y軸速度, 方向向上

2. 計算到達B點的時間和y軸速度, 公式s = v*t + 1/2*g*t^2, v" = v + g*t.簡單推導一下即可, 時間記為t(AB)

3. 根據上面得到的速度,乘以一個衰減係數,方向相反,即為反彈的y軸初速度.

4. 同理得到B點到達C點的時間,記為t(BC)

5. 計算 v = x(AF)/t(AB), 此為水平向右允許的最小速度

6. 根據上面得出的速度,計算若跳到F點後彈跳兩次後的落點E, 公式 s = v*t(BC), 此為可以使用的最左側終點, 最右側終點很好理解, 即為D.

7. 在E到D之間選取一個隨機點,選為最終落點C

8. 根據x(AC)/(t(AB)+t(AC)), 算得實際x軸速度。

9. 根據當前時間計算坐標,公式x = vt; y = vt + 1/2*g*t^2;

10. 重複9到達終點C,回到第0步

PS: 還有一些如果聊天文本過短的處理,這裡不詳細闡述了

PPS: 第一個和最後一個邏輯略有變化,這裡也不闡述了

第二部分. 動畫

0. 在listview上覆蓋一層custom view,大小和listview一致。負責渲染動畫

1. 放置表情view,使的表情的底部中點位於上面算出來的X,Y位置

2. 根據當前速度拉伸or拍扁view(設置scaleX,scaleY, 3.0以前的機器不支持)

PS: 不要用canvas.draw來繪製圖標,數量很多的情況下會很慢,用view.offsetTopAnBottom/view.offsetLeftAndRight來移動View

以上

PPS:

有人在問scroll滾動時要如何跟隨, 其實很簡單的...

第0步時候會獲取下一層item的top坐標..記錄下來

第9步計算每一幀坐標時,重新獲取上面那個item的top坐標,和初始記錄下那個坐標兩者相減即為偏移量..


我猜是編程。


寫了一下,雖然思路差不多了,但是寫起來有很多問題。。。而且我還沒解決。。。

1.表情View的滑動跟隨

我用的是RecyclerView,所以表情View的滑動跟隨很簡單,監聽recyclerView的滑動,然後直接把參數里的偏移量給表情View即可

2.動畫的繪製

我本來打算直接用屬性動畫做的,但是發現做是能做但是相當麻煩,代碼相當的多,於是放棄了,然後用了開線程直接更新x,y然後postInvalidate( )。然後出現問題了,動畫速度時快時慢,而且有時很卡。。。難不成是我手機配置太低?總之不知道怎麼解決。。有人知道請告訴我一下

3.坐標計算

坐標計算其實是個耐心活。。

目前我寫的代碼只是大概的邏輯有了。。但是很渣很渣,主要是動畫有問題,還有坐標有問題。。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

更新:

看了原作者的答案,我決定將動畫用offsetTopAndBottom實現試試看。。。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

再次更新:

因為直接開線程用公式算坐標更新動畫一直有問題(比如動畫時快時慢,很卡,而且連續按幾下按鈕程序就會沒反應),所以我喪心病狂的用動畫寫了下。。。

我將跳躍分為了落差跳(一個對話框跳到另一個)和平跳(同一個對話框上的反彈)

還有就是希望有人能告訴我為啥線程更新坐標然後重繪會卡。。。

實現效果如下:

http://wx3.sinaimg.cn/mw690/e21cb47egy1fc1vl8x968g20bm0j11hh.gif

代碼如下:

public void startJump(final List& PointList) {
this.PointList = PointList;
Animator anim = FirstDown(PointList.get(0).getX(),-100,PointList.get(0).getY());

AnimatorSet set1 = LuoChaJump(PointList.get(0).getX(),PointList.get(0).getY(),
PointList.get(1).getX(),PointList.get(1).getY());

AnimatorSet set2 = PinTiao(PointList.get(1).getX(),PointList.get(1).getY(),
PointList.get(1).getX()+50,PointList.get(1).getY(),100);

AnimatorSet set3 = PinTiao(PointList.get(1).getX()+50,PointList.get(1).getY(),
PointList.get(1).getX()+100,PointList.get(1).getY(),50);

AnimatorSet set4 = LuoChaJump(PointList.get(1).getX()+100,PointList.get(1).getY(),
PointList.get(2).getX(),PointList.get(2).getY());

AnimatorSet set5 = PinTiao(PointList.get(2).getX(),PointList.get(2).getY(),
PointList.get(2).getX()-50,PointList.get(2).getY(),100);

AnimatorSet set6 = PinTiao(PointList.get(2).getX()-50,PointList.get(2).getY(),
PointList.get(2).getX()-100,PointList.get(2).getY(),50);

AnimatorSet set7 = FinishDown(PointList.get(2).getX()-100,PointList.get(2).getY());

AnimatorSet set = new AnimatorSet();
set.playSequentially(anim,set1,set2,set3,set4,set5,set6,set7);
set.start();
}

public Animator FirstDown(float x1,float y1,float y2){
Point startPoint = new Point(x1, y1);
Point endPoint = new Point(x1, y2);
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(2500);
return anim;
}

public AnimatorSet LuoChaJump(float x1,float y1,float x2,float y2){
float dx = (x2-x1)/3+x1;
Point startPoint = new Point(x1, y1);
Point endPoint = new Point(dx, y1-500);
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 DecelerateInterpolator());
anim.setDuration(1000);

Point startPoint2 = new Point(dx, y1-500);
Point endPoint2 = new Point(x2, y2);
ValueAnimator anim2 = ValueAnimator.ofObject(new PointEvaluator(), startPoint2, endPoint2);
anim2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (Point) animation.getAnimatedValue();
invalidate();
}
});
anim2.setInterpolator(new AccelerateInterpolator());
anim2.setDuration(1500);

AnimatorSet set = new AnimatorSet();
set.play(anim2).after(anim);
return set;

}

public AnimatorSet PinTiao(float x1,float y1,float x2,float y2,int dy){
float dx = (x2-x1)/2+x1;
Point startPoint = new Point(x1, y1);
Point endPoint = new Point(dx, y1-dy);
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 DecelerateInterpolator());
anim.setDuration(300);

Point startPoint2 = new Point(dx, y1-dy);
Point endPoint2 = new Point(x2, y2);
ValueAnimator anim2 = ValueAnimator.ofObject(new PointEvaluator(), startPoint2, endPoint2);
anim2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (Point) animation.getAnimatedValue();
invalidate();
}
});
anim2.setInterpolator(new AccelerateInterpolator());
anim2.setDuration(250);

AnimatorSet set = new AnimatorSet();
set.play(anim2).after(anim);
return set;
}

public AnimatorSet FinishDown(float x1,float y1){
Point startPoint = new Point(x1, y1);
Point endPoint = new Point(x1, y1-100);
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 DecelerateInterpolator());
anim.setDuration(250);

Point startPoint2 = new Point(x1, y1-100);
Point endPoint2 = new Point(x1, MyUtils.getScreenHeight(mContext)+100);
ValueAnimator anim2 = ValueAnimator.ofObject(new PointEvaluator(), startPoint2, endPoint2);
anim2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (Point) animation.getAnimatedValue();
invalidate();
}
});
anim2.setInterpolator(new AccelerateInterpolator());
anim2.setDuration(1500);

AnimatorSet set = new AnimatorSet();
set.play(anim2).after(anim);
return set;
}

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

之前的動畫我發現是斜的跳的所以不自然。。於是又擼了一個

效果如下:

http://ww2.sinaimg.cn/mw690/e21cb47ejw1fc2qs77nebg20bm0j1mxz.gif

代碼如下:

public void startJump(float x, float y){
ObjectAnimator animation1 = ObjectAnimator.ofFloat(this,"translationX",x,x+200);
animation1.setDuration(500);
animation1.setInterpolator(new LinearInterpolator());

ObjectAnimator animation2 = ObjectAnimator.ofFloat(this,"translationY",y,y-300);
animation2.setDuration(500);
animation2.setInterpolator(new DecelerateInterpolator());

ObjectAnimator animation3 = ObjectAnimator.ofFloat(this,"translationX",x+200,x+400);
animation3.setDuration(500);
animation3.setInterpolator(new LinearInterpolator());

ObjectAnimator animation4 = ObjectAnimator.ofFloat(this,"translationY",y-300,y);
animation4.setDuration(500);
animation4.setInterpolator(new AccelerateInterpolator());

ObjectAnimator animation5 = ObjectAnimator.ofFloat(this,"translationX",x+400,x+500);
animation5.setDuration(300);
animation5.setInterpolator(new LinearInterpolator());

ObjectAnimator animation6 = ObjectAnimator.ofFloat(this,"translationY",y,y-150);
animation6.setDuration(300);
animation6.setInterpolator(new DecelerateInterpolator());

ObjectAnimator animation7 = ObjectAnimator.ofFloat(this,"translationX",x+500,x+600);
animation7.setDuration(300);
animation7.setInterpolator(new LinearInterpolator());

ObjectAnimator animation8 = ObjectAnimator.ofFloat(this,"translationY",y-150,y);
animation8.setDuration(300);
animation8.setInterpolator(new AccelerateInterpolator());

ObjectAnimator animation9 = ObjectAnimator.ofFloat(this,"translationX",x+600,x+640);
animation9.setDuration(200);
animation9.setInterpolator(new LinearInterpolator());

ObjectAnimator animation10 = ObjectAnimator.ofFloat(this,"translationY",y,y-100);
animation10.setDuration(200);
animation10.setInterpolator(new DecelerateInterpolator());

ObjectAnimator animation11 = ObjectAnimator.ofFloat(this,"translationX",x+640,x+680);
animation11.setDuration(200);
animation11.setInterpolator(new LinearInterpolator());

ObjectAnimator animation12 = ObjectAnimator.ofFloat(this,"translationY",y-100,y);
animation12.setDuration(200);
animation12.setInterpolator(new AccelerateInterpolator());

AnimatorSet set1 = new AnimatorSet();
set1.play(animation1).with(animation2);

AnimatorSet set2 = new AnimatorSet();
set2.play(animation3).with(animation4);

AnimatorSet set3 = new AnimatorSet();
set3.play(animation5).with(animation6);

AnimatorSet set4 = new AnimatorSet();
set4.play(animation7).with(animation8);

AnimatorSet set5 = new AnimatorSet();
set5.play(animation9).with(animation10);

AnimatorSet set6 = new AnimatorSet();
set6.play(animation11).with(animation12);

AnimatorSet set = new AnimatorSet();
set.playSequentially(set1,set2,set3,set4,set5,set6);
set.start();
}

有空擼一個完整的。。。


不請自來,先佔個坑,先說說自己的想法和思路:

首先這個功能應該是有兩個技術點,一個就是蹦躂的滑稽表情,另一個就是如何將動畫嵌套到相應的View裡面。

關於第一點,說白了就是自己繪製動畫而已, @祥子 已經有詳細的代碼了,就不重複說了。

主要談的也是第二點,其實一般第一印象是重寫listview,直接在listview裡面繪製動畫,不過這個也可以實現,獲取相應的item左邊、高度都可以,但是有個問題:動畫是從哪裡繪製,哪裡結束的?然後我打開qq測試了下,看了下qq是如何實現的。然後發現其實也是挺簡單的。

簡單來說其實qq它類似是在listview展示空間布置了一張透明的布局(或者view),動畫全部是在這裡完成的(形象點說就是,聊天記錄展示的空間,而不是所有歷史記錄展示的空間 )。如果我們給listview添加OnScrollListener監聽的話,我們就可以獲取到firstVisibleItem、 visibleItemCount、 totalItemCount,寫到這裡如果是做Android開發的話大家已經知道如何做了吧。

以上就是大體的一個思路,如果不對歡迎指正。

至於代碼,今天的風有些喧囂啊


說明你的聊天內容騰訊都過濾了。沒有秘密


上面的回答已經很完美了,這裡動手實踐了一下,稍作梳理。

  • 先分析

1.每次自由落體運動-過程a

2.從一個消息框到下一個消息框-過程b (由多個過程a組成)

3.獲取屏幕上消息的的Y軸坐標,並不斷更新數據結構-過程c

4.c+b就是整個動畫

  • 代碼們

過程a: 一次自由落體

這裡借用下上面的圖

通過y軸的初速度和時間差 就可計算得到x軸的速度 路程、y軸的速度和路程

while(true){
...
//形成動畫
}

算坐標這種事還是自己試一試吧,感覺每個人的計算過程都會不一樣的。

過程b: 用數據轉換為動畫

多個過程a組成

while(currentIndex&

輸入坐標點的數據

public void setPoints(List&points){
this.points=points;
}

過程c:數據獲取(然後把結果輸入給過程b)

根據對recyclerview的監聽來獲取數據,隨著不斷更新屏幕中的內容,在數據結構中不斷加入新的坐標點。(在這裡,記錄最上方item的坐標y,不斷存其他item到map中,索引為key,坐標差為value)

這裡需要做到一些簡化,避免多餘的消耗。

1.map中有最底部item的數據時候,說明已經更新完了,停止更新map。

2.第一次生成map的時候,便不需要更上方的item的數據。因為么么噠是往下跳的。

最後隨著recyclerview的移動,對這個動畫層偏移dy即可。

ballsurfaceView.offsetTopAndBottom(dy);

  • 遇到的麻煩
  1. view不斷重畫會很卡,用surfaceview替代
  2. 動畫的顯示可能會很長,這裡用到了scrollview當做父容器
  3. 禁止scrollview滑動
  4. surfaceview直接添加的話高度為零,需要放到一個LinearLayout當中
  5. surfaceview的高度不應該是固定值
  6. 因為可利用數據只是屏幕內能看到的內容,所以需要不斷更新數據,加入新的坐標點。這裡偷下懶,對象引用傳遞就好了。

  • 大功告成!

http://upload-images.jianshu.io/upload_images/2482523-2918b8bf00d4a185.gif

以上です。

知乎首答就這樣沒了,:-(


iOS 上直接用 UIDynamicAnimator 就好了,為什麼 要做高中物理題


→_→ 看了下樓上好像都在說描述運動軌跡。。這不是高中物理知識么。。其實自己寫一個插值器就可以了嘛 然後怎麼把它加進去 那這個就更簡單了 在listview上面加一個遮罩層view(獲取listview的父目錄 然後直接addchild 因為最後加進去 所以就在最上層 並且攔截這個view的分發touch事件返回false) 那這個view就不會對觸控產生影響,然後想畫什麼在這個view上畫就好了(前段時間做的一個兼容到2.3的ripple動畫也是用這種方法投機倒把) ←_←

順便一提 手機qq其實整個聊天界面和messagebox(最近聯繫人)都是在同一個activity里 只不過是framelayout控制顯示與否,這樣就更方便了具體的實現過程 畢竟framelayout覆蓋也大丈夫。當然這只是一種保證可以實現的方法,具體是不是這樣實現 有空去分析下主界面的view樹自有分曉 以上。


推薦閱讀:

求一個數學公式:要求生成一個可控制分布的隨機數?
Python的for使用問題?
怎麼樣算是學會一門編程語言?
你們開始是如何學習編程的?
看代碼千行,不如手寫一行,是否在理?為何?

TAG:騰訊QQ | 界面 | 編程 | Android開發 |