基于BarrageRender自定義彈幕動(dòng)畫
BarrageRender目前已更新到2.0.1,自定義彈幕的機(jī)制有改變,本篇文章的代碼在2.0.1下不起作用,如果想按照本文中用的方法,請(qǐng)使用1.9.1版本的BarrageRender。后續(xù)會(huì)更新2.0.1版本下的自定義彈幕
BarrageRender 是iOS上一個(gè)非常出名的彈幕渲染開源框架,其可以讓我們?cè)贏pp中非常方便的集成彈幕功能,其作者在代碼中提供了兩種方式的彈幕動(dòng)畫,
BarrageFloatSprite
和BarrageWalkSprite
。可以說移動(dòng)和浮動(dòng)這兩種動(dòng)畫方式基本上已經(jīng)滿足了大部分App的需求,但是仍然有部分App需要在彈幕的展現(xiàn)形式上更加的自由,例如各大直播平臺(tái)的禮物彈幕。筆者將在這篇文章中分享自己在BarrageRender的基礎(chǔ)上編寫自定義禮物彈幕的過程。
先展示效果
再介紹BarrageWalkSprite原理
BarrageWalkSprite和本文將要實(shí)現(xiàn)的自定義Sprite有一定的關(guān)聯(lián)性,所以就通過分析BarrageWalkSprite的源碼來展示BarrageRender渲染彈幕的原理,另外一個(gè)BarrageFloatSprite的渲染方式稍有不同,但是如果你能搞清楚BarrageWalkSprite的原理,理解FloatSprite的渲染方式也是很輕松的。
彈幕的初始位置
BarrageRender在BarrageDispatcher的調(diào)度下觸發(fā)activeWithContext方法,而在此方法中,BarrageRender調(diào)用了Sprite的originInBounds:withSprite方法來確定每個(gè)精靈的初始位置
- (void)activeWithContext:(NSDictionary *)context
{
CGRect rect = [[context objectForKey:kBarrageRendererContextCanvasBounds]CGRectValue];
NSArray * sprites = [context objectForKey:kBarrageRendererContextRelatedSpirts];
NSTimeInterval timestamp = [[context objectForKey:kBarrageRendererContextTimestamp]doubleValue];
_timestamp = timestamp;
_view = [self bindingView];
[self configView];
[_view sizeToFit];
if (!CGSizeEqualToSize(_mandatorySize, CGSizeZero)) {
_view.frame = CGRectMake(0, 0, _mandatorySize.width, _mandatorySize.height);
}
_origin = [self originInBounds:rect withSprites:sprites];
_view.frame = CGRectMake(_origin.x, _origin.y, self.size.width, self.size.height);
}
BarrageWalkSpirte在originInBounds:withSprite方法中,根據(jù)當(dāng)前屏幕上已經(jīng)存在的Sprite來計(jì)算自己的初始位置。
- (CGPoint)originInBounds:(CGRect)rect withSprites:(NSArray *)sprites
{
// 獲取同方向精靈
NSMutableArray * synclasticSprites = [[NSMutableArray alloc]initWithCapacity:sprites.count];
for (BarrageWalkSprite * sprite in sprites) {
if (sprite.direction == _direction && sprite.side == self.side) { // 找尋同道中人
[synclasticSprites addObject:sprite];
}
}
static BOOL const AVAERAGE_STRATEGY = YES; // YES:條紋平均精靈策略(體驗(yàn)會(huì)好一些); NO:最快時(shí)間策略
NSTimeInterval stripMaxActiveTimes[STRIP_NUM]={0}; // 每一條網(wǎng)格 已有精靈中最后退出屏幕的時(shí)間
NSUInteger stripSpriteNumbers[STRIP_NUM]={0}; // 每一條網(wǎng)格 包含精靈的數(shù)目
NSUInteger stripNum = MIN(STRIP_NUM, MAX(self.trackNumber, 1)); // between (1,STRIP_NUM)
CGFloat stripHeight = rect.size.height/stripNum; // 水平條高度
CGFloat stripWidth = rect.size.width/stripNum; // 豎直條寬度
BOOL oritation = _direction == BarrageWalkDirectionL2R || _direction == BarrageWalkDirectionR2L; // 方向, YES代表水平彈幕
BOOL rotation = self.side == [self defaultSideWithDirection:_direction];
/// 計(jì)算數(shù)據(jù)結(jié)構(gòu),便于應(yīng)用算法
NSUInteger overlandStripNum = 1; // 橫跨網(wǎng)格條數(shù)目
if (oritation) { // 水平
overlandStripNum = (NSUInteger)ceil((double)self.size.height/stripHeight);
}
else // 豎直
{
overlandStripNum = (NSUInteger)ceil((double)self.size.width/stripWidth);
}
/// 當(dāng)前精靈需要的時(shí)間,左邊碰到邊界, 不是真實(shí)的活躍時(shí)間
NSTimeInterval maxActiveTime = oritation?rect.size.width/self.speed:rect.size.height/self.speed;
NSUInteger availableFrom = 0;
NSUInteger leastActiveTimeStrip = 0; // 最小時(shí)間的行
NSUInteger leastActiveSpriteStrip = 0; // 最小網(wǎng)格的行
for (NSUInteger i = 0; i < stripNum; i++) {
//尋找當(dāng)前行里包含的sprites
CGFloat stripFrom = i * (oritation?stripHeight:stripWidth);
CGFloat stripTo = stripFrom + (oritation?stripHeight:stripWidth);
if (!rotation) {
CGFloat preStripFrom = stripFrom;
stripFrom = (oritation?rect.size.height:rect.size.width) - stripTo;
stripTo = (oritation?rect.size.height:rect.size.width) - preStripFrom;
}
CGFloat lastDistanceAllOut = YES;
for (BarrageWalkSprite * sprite in synclasticSprites) {
CGFloat spriteFrom = oritation?sprite.origin.y:sprite.origin.x;
CGFloat spriteTo = spriteFrom + (oritation?sprite.size.height:sprite.size.width);
if ((spriteTo-spriteFrom)+(stripTo-stripFrom)>MAX(stripTo-spriteFrom, spriteTo-stripFrom)) { // 在條條里
stripSpriteNumbers[i]++;
NSTimeInterval activeTime = [sprite estimateActiveTime];
if (activeTime > stripMaxActiveTimes[i]){ // 獲取最慢的那個(gè)
stripMaxActiveTimes[i] = activeTime;
CGFloat distance = oritation?fabs(sprite.position.x-sprite.origin.x):fabs(sprite.position.y-sprite.origin.y);
lastDistanceAllOut = distance > (oritation?sprite.size.width:sprite.size.height);
}
}
}
if (stripMaxActiveTimes[i]>maxActiveTime || !lastDistanceAllOut) {
availableFrom = i+1;
}
else if (i - availableFrom >= overlandStripNum - 1){
break; // eureka!
}
if (i <= stripNum - overlandStripNum) {
if (stripMaxActiveTimes[i] < stripMaxActiveTimes[leastActiveTimeStrip]) {
leastActiveTimeStrip = i;
}
if (stripSpriteNumbers[i] < stripSpriteNumbers[leastActiveSpriteStrip]) {
leastActiveSpriteStrip = i;
}
}
}
if (availableFrom > stripNum - overlandStripNum) { // 那就是沒有找到嘍
availableFrom = AVAERAGE_STRATEGY?leastActiveSpriteStrip:leastActiveTimeStrip; // 使用最小個(gè)數(shù) or 使用最短時(shí)間
}
CGPoint origin = CGPointZero;
if (oritation) { // 水平
_destination.y = origin.y = (rotation?stripHeight*availableFrom:rect.size.height-stripHeight * availableFrom-self.size.height)+rect.origin.y;
origin.x = (self.direction == BarrageWalkDirectionL2R)?rect.origin.x - self.size.width:rect.origin.x + rect.size.width;
_destination.x = (self.direction == BarrageWalkDirectionL2R)?rect.origin.x + rect.size.width:rect.origin.x - self.size.width;
}
else
{
_destination.x = origin.x = (rotation?stripWidth*availableFrom:rect.size.width-stripWidth*availableFrom -self.size.width)+rect.origin.x;
origin.y = (self.direction == BarrageWalkDirectionT2B)?rect.origin.y - self.size.height:rect.origin.y + rect.size.height;
_destination.y = (self.direction == BarrageWalkDirectionT2B)?rect.origin.y + rect.size.height:rect.origin.y - self.size.height;
}
return origin;
}
代碼雖然很長,但是主要就是為了實(shí)現(xiàn)下面幾個(gè)邏輯:
1. BarrageWalkSprite先獲取了同方向的所有精靈
2. 根據(jù)屏幕軌道的frame范圍找到每一個(gè)軌道內(nèi)的所有精靈
3. 在同一軌道內(nèi)的所有精靈中找到存活時(shí)間最長的精靈(速度最慢)
4. 判斷速度最慢的那個(gè)精靈的尾部是否已經(jīng)完全進(jìn)入彈幕顯示區(qū)域
5. 如果速度最慢的精靈尾部已經(jīng)進(jìn)入彈幕顯示區(qū)域,則可以確定自己的可以緊跟在后面出現(xiàn),如果還沒有完全進(jìn)入彈幕顯示區(qū)域,則繼續(xù)在下一個(gè)軌道獲取合適的位置
6. 根據(jù)計(jì)算得到的自己可以出現(xiàn)的軌道,加上該軌道上最后一個(gè)精靈的位置,得到自己的起始位置
彈幕的運(yùn)動(dòng)軌跡
BarrageRender繪制每個(gè)精靈的運(yùn)動(dòng)軌跡的方式非常簡單,在BarrageRender中,內(nèi)置的時(shí)鐘引擎BarrageClock
負(fù)責(zé)在間隔時(shí)間內(nèi)調(diào)用所有已經(jīng)激活精靈基類BarrageSprite
中的updateWithTime方法。
- (void)initClock
{
__weak id weakSelf = self;
_clock = [BarrageClock clockWithHandler:^(NSTimeInterval time){
BarrageRenderer * strongSelf = weakSelf;
strongSelf->_time = time;
[strongSelf update];
}];
}
/// 每個(gè)刷新周期執(zhí)行一次
- (void)update
{
[_dispatcher dispatchSprites]; // 分發(fā)精靈
for (BarrageSprite * sprite in _dispatcher.activeSprites) {
[sprite updateWithTime:_time];
}
}
而在BarrageSprite
的updateWithTime方法中, 每個(gè)精靈重新更改了自身的frame屬性,以此來達(dá)到動(dòng)畫位移的效果。其中_valid
屬性是Sprite存活的唯一標(biāo)志,標(biāo)記為NO之后,Sprite就會(huì)從隊(duì)列中徹底移除
//BarrageSprite
- (void)updateWithTime:(NSTimeInterval)time
{
_valid = [self validWithTime:time];
_view.frame = [self rectWithTime:time];
}
BarrageWalkSprite通過屬性speed來實(shí)時(shí)改變自己的frame位置,同時(shí)計(jì)算剩下的destination和speed來算出自己的存活時(shí)間以用來標(biāo)記valid屬性
//BarrageWalkSprite
- (BOOL)validWithTime:(NSTimeInterval)time
{
return [self estimateActiveTime] > 0;
}
- (NSTimeInterval)estimateActiveTime
{
CGFloat activeDistance = 0;
switch (_direction) {
case BarrageWalkDirectionR2L:
activeDistance = self.position.x - _destination.x;
break;
case BarrageWalkDirectionL2R:
activeDistance = _destination.x - self.position.x;
break;
case BarrageWalkDirectionT2B:
activeDistance = _destination.y - self.position.y;
break;
case BarrageWalkDirectionB2T:
activeDistance = self.position.y - _destination.y;
default:
break;
}
return activeDistance/self.speed;
}
- (CGRect)rectWithTime:(NSTimeInterval)time
{
CGFloat X = self.destination.x - self.origin.x;
CGFloat Y = self.destination.y - self.origin.y;
CGFloat L = sqrt(X*X + Y*Y);
NSTimeInterval duration = time - self.timestamp;
CGPoint position = CGPointMake(self.origin.x + duration * self.speed * X/L, self.origin.y + duration * self.speed * Y/L);
return CGRectMake(position.x, position.y, self.size.width, self.size.height);
}
彈幕終點(diǎn)
BarrageWalkSprite的終點(diǎn)計(jì)算很簡單,彈幕的顯示的距離加上Sprite自身的寬度就是整個(gè)精靈需要位移的距離,這個(gè)destination的計(jì)算已經(jīng)體現(xiàn)在了起點(diǎn)位置的獲取當(dāng)中
CGPoint origin = CGPointZero;
if (oritation) { // 水平
_destination.y = origin.y = (rotation?stripHeight*availableFrom:rect.size.height-stripHeight * availableFrom-self.size.height)+rect.origin.y;
origin.x = (self.direction == BarrageWalkDirectionL2R)?rect.origin.x - self.size.width:rect.origin.x + rect.size.width;
_destination.x = (self.direction == BarrageWalkDirectionL2R)?rect.origin.x + rect.size.width:rect.origin.x - self.size.width;
}
else
{
_destination.x = origin.x = (rotation?stripWidth*availableFrom:rect.size.width-stripWidth*availableFrom -self.size.width)+rect.origin.x;
origin.y = (self.direction == BarrageWalkDirectionT2B)?rect.origin.y - self.size.height:rect.origin.y + rect.size.height;
_destination.y = (self.direction == BarrageWalkDirectionT2B)?rect.origin.y + rect.size.height:rect.origin.y - self.size.height;
}
return origin;
自定義Sprite
BarrageBubblingSprite的運(yùn)動(dòng)軌跡和BarrageWalkSprite有很多重合之處,所以自定義的BarrageBubblingSprite直接繼承BarrageWalkSprite以獲取其direction,side,speed,trackNumber等多個(gè)屬性,當(dāng)然還需要另外加上加速度speedUp和停留時(shí)間stay屬性
@interface BarrrageBubblingSprite : BarrageWalkSprite
@property (nonatomic,assign) CGFloat speedUp; //加速度
@property (nonatomic,assign) CGFloat stay; //到達(dá)終點(diǎn)后的停留時(shí)間
@end
起點(diǎn)位置
BubblingSprite的起點(diǎn)位置的獲取邏輯和WalkSprite的起點(diǎn)邏輯類似,不同的地方在于:
- 即使軌道內(nèi)最慢的那個(gè)精靈已經(jīng)完全進(jìn)入彈幕顯示區(qū)域,只要該精靈仍然存活,就不能緊跟其后,而是要另外找尋其他軌道
- 當(dāng)所有軌道都已經(jīng)有精靈占據(jù)的時(shí)候,找到存活時(shí)間最短的那個(gè)精靈,通過將其的stay屬性設(shè)置為0讓其直接消失,然后讓自己占據(jù)該精靈所在軌道
- (CGPoint)originInBounds:(CGRect)rect withSprites:(NSArray *)sprites
{
// 獲取同方向精靈
NSMutableArray * synclasticSprites = [[NSMutableArray alloc]initWithCapacity:sprites.count];
for (BarrageWalkSprite * sprite in sprites) {
if (sprite.direction == self.direction && sprite.side == self.side) { // 找尋同道中人
[synclasticSprites addObject:sprite];
}
}
NSUInteger stripNum = MIN(STRIP_NUM, MAX(self.trackNumber, 1)); // between (1,STRIP_NUM)
CGFloat stripHeight = rect.size.height/stripNum; // 水平條高度
CGFloat stripWidth = rect.size.width/stripNum; // 豎直條寬度
BOOL oritation = self.direction == BarrageWalkDirectionL2R || self.direction == BarrageWalkDirectionR2L; // 方向, YES代表水平彈幕
BOOL rotation = self.side == [self defaultSideWithDirection:self.direction];
/// 計(jì)算數(shù)據(jù)結(jié)構(gòu),便于應(yīng)用算法
NSUInteger overlandStripNum = 1; // 橫跨網(wǎng)格條數(shù)目
if (oritation) { // 水平
overlandStripNum = (NSUInteger)ceil((double)self.size.height/stripHeight);
}
else // 豎直
{
overlandStripNum = (NSUInteger)ceil((double)self.size.width/stripWidth);
}
NSUInteger availableFrom = 0;
BarrrageBubblingSprite* lastTimeSprite = self;
NSInteger lastSpriteIndex = 0;
for (NSUInteger i = 0; i < stripNum; i++) {
//尋找當(dāng)前行里包含的sprites
CGFloat stripFrom = i * (oritation?stripHeight:stripWidth);
CGFloat stripTo = stripFrom + (oritation?stripHeight:stripWidth);
if (!rotation) {
CGFloat preStripFrom = stripFrom;
stripFrom = (oritation?rect.size.height:rect.size.width) - stripTo;
stripTo = (oritation?rect.size.height:rect.size.width) - preStripFrom;
}
CGFloat exsitSprite = NO;
for (BarrrageBubblingSprite * sprite in synclasticSprites) {
CGFloat spriteFrom = oritation?sprite.origin.y:sprite.origin.x;
CGFloat spriteTo = spriteFrom + (oritation?sprite.size.height:sprite.size.width);
if ((spriteTo-spriteFrom)+(stripTo-stripFrom)>MAX(stripTo-spriteFrom, spriteTo-stripFrom)) { // 在條條里
exsitSprite = YES;
if (sprite.timestamp < lastTimeSprite.timestamp){
lastTimeSprite = sprite;
lastSpriteIndex = i;
}
break;
}
}
if (exsitSprite) {
availableFrom = i+1;
}else{ //第一行就是空的
break;
}
}
if (availableFrom == stripNum) { // 超出最大的軌道數(shù),擠掉最上層精靈
availableFrom = lastSpriteIndex;
lastTimeSprite.stay = 0;
}
CGPoint origin = CGPointZero;
if (oritation) { // 水平
_destination.y = origin.y = (rotation?stripHeight*availableFrom:rect.size.height-stripHeight * availableFrom-self.size.height)+rect.origin.y;
origin.x = (self.direction == BarrageWalkDirectionL2R)?rect.origin.x - self.size.width:rect.origin.x + rect.size.width;
_destination.x = (self.direction == BarrageWalkDirectionL2R)?rect.origin.x + rect.size.width - self.size.width :rect.origin.x + self.size.width;
}
else
{
_destination.x = origin.x = (rotation?stripWidth*availableFrom:rect.size.width-stripWidth*availableFrom -self.size.width)+rect.origin.x;
origin.y = (self.direction == BarrageWalkDirectionT2B)?rect.origin.y - self.size.height:rect.origin.y + rect.size.height;
_destination.y = (self.direction == BarrageWalkDirectionT2B)?rect.origin.y + rect.size.height - self.size.height:rect.origin.y + self.size.height;
}
return origin;
運(yùn)動(dòng)軌跡
BarrageBubblingSprite的運(yùn)動(dòng)軌跡和BarrageWalkSprite的運(yùn)動(dòng)軌跡不同的地方在于,BarrageWalkSprite是勻速前進(jìn),二BarrageBubblingSprite是加速前進(jìn),這樣,在計(jì)算某個(gè)時(shí)段Sprite的位置就需要考慮加速度的存在。
- (CGRect)rectWithTime:(NSTimeInterval)time{
CGFloat X = self.destination.x - self.origin.x;
CGFloat Y = self.destination.y - self.origin.y;
CGFloat L = sqrt(X*X + Y*Y);
NSTimeInterval duration = time - self.timestamp;
CGPoint position = CGPointMake(self.origin.x + duration * self.speed * X/L, self.origin.y + duration * self.speed * Y/L);
if (position.x >= self.destination.x) {
position.x = self.destination.x;
}else{
self.destinationStamp = time;
self.speed = duration*self.speedUp;
}
if(position.y >= self.destination.y) {
position.y = self.destination.y;
}else{
self.destinationStamp = time;
self.speed = duration*self.speedUp;
}
return CGRectMake(position.x, position.y, self.size.width, self.size.height);
}
在存活時(shí)間上,與BarrageWalkSprite不同的地方在于,BarrageWalkSprite在位移到終點(diǎn)的時(shí)候消失,而BarrageBubblingSprite在到達(dá)終點(diǎn)之后仍然需要停留stay的時(shí)間。這里引入了currentStamp和destinationStamp時(shí)間戳用于來計(jì)算stay時(shí)間是否已經(jīng)到達(dá)。
//計(jì)算精靈的剩余存活時(shí)間
- (double)countTimeByDistance:(CGFloat)distance{
CGFloat a = 0.5*self.speedUp;
CGFloat b = self.speed;
CGFloat c = -distance;
CGFloat delt = sqrt(b*b - 4*a*c);
double t = (-b+delt)/(2*a);
return t;
}
- (NSTimeInterval)estimateActiveTime
{
CGFloat activeDistance = 0;
switch (self.direction) {
case BarrageWalkDirectionR2L:
activeDistance = self.position.x - _destination.x;
break;
case BarrageWalkDirectionL2R:
activeDistance = _destination.x - self.position.x;
break;
case BarrageWalkDirectionT2B:
activeDistance = _destination.y - self.position.y;
break;
case BarrageWalkDirectionB2T:
activeDistance = self.position.y - _destination.y;
default:
break;
}
NSTimeInterval leftTime = 0.0;
CGFloat time = [self countTimeByDistance:activeDistance];
if (time > 0){
leftTime = time + self.stay;
}else{
leftTime = self.stay - (self.currentStamp - self.destinationStamp);
}
return leftTime;
}
- (BOOL)validWithTime:(NSTimeInterval)time{
self.currentStamp = time;
return [self estimateActiveTime] > 0;
}
自定義彈幕樣式
類似BarrageWalkImageSprite,我們也通過繼承BarrageSpirte的bindingView 來將自定義的彈幕view返回給BarrageRender