QQ上發送么么噠時候,彈出彈跳錶情,是如何實現的?
如果題主問的是「如何實現有么么噠就出效果」這種邏輯,那麼做字元判斷調用方法就可以了,但是我想這種問題不值得上知乎問一回是吧。
那麼,我猜題主問的是:表情彈來彈去的動畫如何實現的吧;
ok,我們把場景還原一遍
效果gif:
http://img1.ph.126.net/r_YH8TofKCrSxAjp_5i5Kg==/6631845120421043830.gif
可以看出,表情做了一個典型的彈跳自由落體運動,以下內容不超過中學物理知識,請放心食用;
我們把它彈跳的運動軌跡拆解一下:
看起來很簡單是吧。。。沒有專業的可視化軟體,我就拿嘴說吧,說一下每一次彈跳的運動實現;老規矩,做題之前,要列出已知變數和未知變數:
OK,我們先假定一個坐標點的集合pointList ,裡面儲存一些坐標點,模擬要落在對話框的坐標;
那麼,從第一個點到第二個點,要怎麼做呢?我們把表示球在第幾個坐標的參數初始化,startX,startY為第一個坐標點坐標,並觸發一個線程,開始繪製界面;
我們再回到第一張圖,我們把這個稱為第一階段運動:
X軸方向很好理解,在落地之前,做勻速直線運動:
t是怎麼來?t是Y軸方向,自由落體 S1和S2運動時間的總和;把這一段軌跡轉化為代碼,則是: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之間選取一個隨機點,選為最終落點C8. 根據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&
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組成
輸入坐標點的數據 過程c:數據獲取(然後把結果輸入給過程b) 根據對recyclerview的監聽來獲取數據,隨著不斷更新屏幕中的內容,在數據結構中不斷加入新的坐標點。(在這裡,記錄最上方item的坐標y,不斷存其他item到map中,索引為key,坐標差為value) 這裡需要做到一些簡化,避免多餘的消耗。 1.map中有最底部item的數據時候,說明已經更新完了,停止更新map。 2.第一次生成map的時候,便不需要更上方的item的數據。因為么么噠是往下跳的。 最後隨著recyclerview的移動,對這個動畫層偏移dy即可。while(currentIndex&
public void setPoints(List&
this.points=points;
}
ballsurfaceView.offsetTopAndBottom(dy);
- 遇到的麻煩
- view不斷重畫會很卡,用surfaceview替代
- 動畫的顯示可能會很長,這裡用到了scrollview當做父容器
- 禁止scrollview滑動
- surfaceview直接添加的話高度為零,需要放到一個LinearLayout當中
- surfaceview的高度不應該是固定值
- 因為可利用數據只是屏幕內能看到的內容,所以需要不斷更新數據,加入新的坐標點。這裡偷下懶,對象引用傳遞就好了。
- 大功告成!
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使用問題?
※怎麼樣算是學會一門編程語言?
※你們開始是如何學習編程的?
※看代碼千行,不如手寫一行,是否在理?為何?