TUIKaraoke 是一个开源的音视频 UI 组件,集成了 腾讯云实时音视频、即时通信、正版曲库直通车等产品,通过在项目中集成 TUIKaraoke 组件,只需要编写几行代码就可以为您的应用添加在线 K 歌场景,体验 K 歌、麦位管理、收发礼物、文字聊天等 TRTC 在 KTV 场景下的相关能力。
基本功能如下图所示:

| 角色 | 描述 |
|---|---|
| 房主 | 歌房创建者 |
| 连麦主播 | 进入歌房后,通过上麦成为连麦主播 |
| 主唱 | 连麦主播点歌后进行排麦演唱,正在演唱者成为主唱 |
| 听众 | 进入歌房的倾听者 |
今天我们来体验一下,如何使用TUIKaraoke组件来快速搭建在线K歌。
当然在实验之前需要做一些准备,诸如环境配置,获取AppID和密钥等。
如果您未开通腾讯云 TRTC 服务,可进入 腾讯云实时音视频控制台,创建一个新的 TRTC 应用后,在应用管理列表里面找到当前的应用:

点击配置管理,进入到应用信息页面,在应用信息里面就可以找到SDKAppID:

点击快速上手,第二步 获取签发UserSig的密钥Secretkey:

我们用最新版的Android Studio创建新项目,或者在你的项目里面Android Studio 需要3.5及以上版本,API Level 至少是17,官方建议 21以上。另外如果是体验官方的demo,gradle版本还是使用demo的gradle-wrapper.properties里面的设置,jdk使用1.8。在我们的新项目里面或者现有项目里面就可以不用按照demo的来了。这里我创建的新项目语言是kotlin,gradle版本是7.4.2,当然jdk也相应的使用了jdk16。放一张我的配置

可以通过Use Gradle from选择使用自己本地下载好的gradle,或者还是按照gradle-wrapper.properties里面配置的去下载就行了。
1.单击进入 GitHub - tencentyun/TUIKaraoke,选择克隆/下载代码,然后拷贝 Android目录下的 tuikaraoke 和 debug 目录到您的工程中(debug :调试相关, tuikaraoke : KTV业务逻辑)。

setting.gradle 中导入如下配置:include ':tuikaraoke'
include ':debug'
3.在 app 的 build.gradle 文件中添加对 TUIKaraoke 的依赖:
api project(':tuikaraoke')
4.在根目录的 build.gradle 文件中添加 TRTC SDK 和 IM SDK 的依赖:
ext {
liteavSdk = "com.tencent.liteav:LiteAVSDK_TRTC:latest.release"
imSdk = "com.tencent.imsdk:imsdk-plus:latest.release"
}
在 AndroidManifest.xml 中配置 App 的权限,SDK 需要以下权限(6.0以上的 Android 系统需要动态申请麦克风、读取存储权限等):
// 使用场景:悬浮窗功能需要此权限;
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
// 使用场景:使用蓝牙耳机时需要此权限;
<uses-permission android:name="android.permission.BLUETOOTH" />
在 proguard-rules.pro 文件,将 SDK 相关类加入不混淆名单:
-keep class com.tencent.** { *; }
编译下载完成后项目里面已经有了tuikaraoke 和debug两个模块了

debug/src/main/java/com/tencent/liteav/debug/GenerateTestUserSig.java 文件。GenerateTestUserSig.java 文件中的SDKAppID 和 Secretkey 为上面从腾讯云实时音视频控制台里面获取到的SDKAppID和秘钥。
至此,项目的前期准备已经完成,试着编译下,不出意外的话应该可以编译成功。
接下来我们就一起一步一步来实现在线KTV场景啦。
1.设置登录用户信息
用户进入应用时,需要收集登录用户信息,包括userId,userName还有头像等信息。
我们可以构造一个简单的登录界面,输入框只允许输入字符串类型,长度不超过32字节,不支持使用特殊字符,使用英文或数字来设置userId。大致如下:

当然,我们可以根据自己具体的项目结合业务实际账号体系自行设置。
TUIKaraoke提供了两个类,UserModel模型类和UserModelManager管理类来管理用户信息。
构造好用户模型信息后就可以使用UserModelManager实例来存储为后续使用:
private fun login() {
val userId = mEditUserId.text.toString().trim()
val userModel = UserModel()
userModel.apply {
this.userId = userId
userName = userId
userSig = GenerateTestUserSig.genTestUserSig(userId)
val index = Random().nextInt(AvatarConstant.USER_AVATAR_ARRAY.count())
val coverUrl = AvatarConstant.USER_AVATAR_ARRAY[index]
userAvatar = coverUrl
}
val manager = UserModelManager.getInstance()
manager.userModel = userModel
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
其中 userSig:根据 SDKAppId、userId,Secretkey 等信息计算得到的安全保护签名,可以使用TUIKaraoke 提供的GenerateTestUserSig.genTestUserSig 计算,或者参考 如何计算及使用 UserSig实现。
2.配置TRTCKaraokeRoom 组件,TRTCKaraokeRoom 是基于腾讯云实时音视频(TRTC)和即时通信 IM 服务组合而成的组件,支持提供一系列房间列表、房间热度、主播列表等功能,比如创建房间,管理点歌上麦,发送礼物和各种文本、自定义消息,自定义消息可用于实现弹幕、点赞等。
首先通过sharedInstance来获取TRTCKaraokeRoom 单例对象。
public static synchronized TRTCKaraokeRoom sharedInstance(Context context);
接着通过TRTCKaraokeRoom的login方法登录到服务:
public abstract void login(int sdkAppId,
String userId, String userSig,
TRTCKaraokeRoomCallback.ActionCallback callback);
userId和userSig可以从我们前面登录的信息里面拿到,接下来就简单了,代码如下:
/**
* 初始化实例并登录
*/
private fun initData() {
// 1.初始化 获取 TRTCKaraokeRoom 单例对象
mTRTCKaraokeRoom = TRTCKaraokeRoom.sharedInstance(this)
// 2.登录
mTRTCKaraokeRoom.login(
GenerateTestUserSig.SDKAPPID,
userModel.userId, //当前用户的 ID,字符串类型,只允许包含英文字母(a-z 和 A-Z)、数字(0-9)、连词符(-)和下划线(_)
userModel.userSig //签名
) { code, _ ->
if (code == 0) {//登录成功回调,成功时 code 为0。
//修改个人信息
mTRTCKaraokeRoom.setSelfProfile(
userModel.userName,
userModel.userAvatar
) { code, _ ->
if (code == 0) {
Log.d(TAG, "修改个人信息成功")
}
}
}
}
}
TRTCKaraokeRoom服务配置好了后,我们就可以管理房间了,创建房间或者进入房间,也可以获取房间列表。
其中创建房间和进入房间需要按两步走,只有房主才能创建房间,销毁房间,听众只能进入房间,退出房间。
创建房间我们使用TUIKaraoke提供的KaraokeRoomCreateDialog弹窗就可以了,我们只需要提供登录的房主信息,具体的创建房间就由TUIKaraoke来完成就好了。

调用弹窗的代码如下:
/**
* 创建房间(房主调用),若房间不存在,系统将自动创建一个新房间。
*/
private fun createRoom() {
val dialog = KaraokeRoomCreateDialog(this)
dialog.showRoomCreateDialog(
userModel.userId,
userModel.userId,
userModel.userAvatar,
TRTCCloudDef.TRTC_AUDIO_QUALITY_DEFAULT,
true
)
}
不幸的是,在这里,大家打开弹窗的时候可能会报错。
com.tencent.liteav.tuikaraoke.ui.room.KaraokeRoomBaseActivity 的 requestFeature使用报错。报错信息是java.lang.RuntimeException:
Unable to start activity ComponentInfo{com.mariko.karaoke/com.tencent.liteav.tuikaraoke.ui.room.KaraokeRoomAnchorActivity}:
android.util.AndroidRuntimeException: requestFeature()must be called before adding content
需要修改KaraokeRoomBaseActivity 由继承 AppCompatActivity 改为 Activity。
1.拷贝歌曲管理类实现。
在打开弹窗的时候还有个错误:
java.lang.RuntimeException: Unable to start activity ComponentInfo {com.mariko.karaoke/com.tencent.liteav.tuikaraoke.ui.room.KaraokeRoomAnchorActivity}: java.lang.NullPointerException:
Attempt to invoke virtual method 'void com.tencent.liteav.tuikaraoke.ui.music.KaraokeMusicService.setRoomInfo(com.tencent.liteav.tuikaraoke.model.TRTCKaraokeRoomDef$RoomInfo)'
on a null object reference
原因是在KaraokeRoomAnchorActivity里面KaraokeMusicService.setRoomInfo调用崩溃了,为什么会崩溃呢?原来通过KaraokeMusicService是通过 mPakcageName 反射创建管理实现的:
// 通过反射创建歌曲管理实现类的实例
public void createKTVMusicImpl() {
try {
Class clz = Class.forName(mPakcageName);
Constructor constructor = clz.getConstructor(Context.class);
mKaraokeMusicService = (KaraokeMusicService) constructor.newInstance(this);
} catch (Exception e) {
e.printStackTrace();
}
}
找到mPakcageName:
private String mPakcageName = "com.tencent.liteav.demo.karaokeimpl.KaraokeMusicServiceImpl";
在我们的项目的里面是没有的,提示我们要实现这个歌曲管理类,然后替换。
为了快速实现,我们从 GitHub - tencentyun/TUIKaraoke, 选择已经下载的demo代码里面,找到TUIKaraoke/Android/app/src/main/java/com/tencent/liteav/demo/karaokeimpl的karaokeimpl文件夹,拷贝到我们的项目里面,记得更改下包名,最后如下图所示:

2.拷贝本地音频文件。
从demo项目里面拷贝assets目录里面的音频文件和字幕。

3.加载音频文件。
在进入房间之前,也就是打开弹窗之前需要将本地的音频文件预先加载到内存里面。
使用一个公共方法加载音频或者字幕:
public static void copyAssetsToFile(Context context, String name) {
String savePath = ContextCompat.getExternalFilesDirs(context, null)[0].getAbsolutePath();
String filename = savePath + "/" + name;
File dir = new File(savePath);
// 如果目录不存在,创建这个目录
if (!dir.exists()) {
dir.mkdir();
}
try {
if (!(new File(filename)).exists()) {
InputStream is = context.getResources().getAssets().open(name);
FileOutputStream fos = new FileOutputStream(filename);
byte[] buffer = new byte[7168];
int count = 0;
while ((count = is.read(buffer)) > 0) {
fos.write(buffer, 0, count);
}
fos.close();
is.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
然后再分别加载所有文件:
public static void initLocalData(Context context) {
copyAssetsToFile(context, "houlai_bz.mp3");
copyAssetsToFile(context, "houlai_yc.mp3");
copyAssetsToFile(context, "qfdy_yc.mp3");
copyAssetsToFile(context, "qfdy_bz.mp3");
copyAssetsToFile(context, "xq_bz.mp3");
copyAssetsToFile(context, "xq_yc.mp3");
copyAssetsToFile(context, "nuannuan_bz.mp3");
copyAssetsToFile(context, "nuannuan_yc.mp3");
copyAssetsToFile(context, "jda.mp3");
copyAssetsToFile(context, "jda_bz.mp3");
copyAssetsToFile(context, "houlai_lrc.vtt");
copyAssetsToFile(context, "qfdy_lrc.vtt");
copyAssetsToFile(context, "xq_lrc.vtt");
copyAssetsToFile(context, "nuannuan_lrc.vtt");
copyAssetsToFile(context, "jda_lrc.vtt");
}
我们使用的歌曲来自本地的,当然可以增加自己的实现,调用api从腾讯云正版曲库获取歌曲。


至此创建房间的弹窗应该可以打开了,K歌房间也可以进去啦。
打开弹窗后TUIKaraoke是通过KaraokeRoomAnchorActivity来创建房间和K歌主界面的,KaraokeRoomAnchorActivity继承自KaraokeRoomBaseActivity,KaraokeRoomBaseActivity主要是加载界面和处理一些事件等,位于tuikaraoke.ui.room 目录下。页面布局是R.layout.trtckaraoke_activity_main。

我们要做一些定制可以从布局上面顺藤摸瓜了。
在K歌界面我们看到了一段 欢迎消息,同时还有有跳转的链接:

很想把它换掉或者去掉对不对?
其实他就是一个欢迎消息的回显。
在房间里面我们有很多消息类型,有的是文本消息,还有的是带同意按钮的邀请等待的消息,消息的实体tuikaraoke.ui.widget.msg.MsgEntity 的MsgEntity实体类:
package com.tencent.liteav.tuikaraoke.ui.widget.msg;
public class MsgEntity {
public static final int TYPE_NORMAL = 0;
public static final int TYPE_WAIT_AGREE = 1;
public static final int TYPE_AGREED = 2;
public static final int TYPE_WELCOME = 3;
public static final int TYPE_ORDERED_SONG = 4;
public static final int TYPE_ERROR = -1;
public String userId;
public String userName;
public String content;
public String invitedId;
public String linkUrl;
public int type;
public int color;
public boolean isChat;
public String songName;
}
有6种类型:
* 普通消息: TYPE_NORMAL 消息的内容会在界面显示出来
* 邀请等待的消息: TYPE_WAIT_AGREE 消息中会有同意的按钮,可以进行事件处理
* 邀请已同意消息: TYPE_AGREED 邀请消息已被处理,事件按钮被隐藏
* 欢迎消息: TYPE_WELCOME 会出现在界面中,同时有跳转的链接url
* 点歌消息: TYPE_ORDERED_SONG 消息中会有管理点歌的按钮,房主可以进行事件处理
TUIKaraoke提供了一个消息互动显示的适配器MsgListAdapter,根据消息的类型显示不同的样式,消息的发送者的username可以对颜色进行设置,而且实现了监听按钮等的点击事件,当有消息添加到mList,就通知适配器刷新。
同时使用一个mRvImMsg的RecyclerView来展示消息的。涉及到消息的几个属性如下:
KaraokeRoomBaseActivity文件:
protected RecyclerView mRvImMsg;
protected MsgListAdapter mMsgListAdapter;
protected List<MsgEntity> mMsgEntityList;
统一添加消息的入口是在KaraokeRoomBaseActivity的showImMsg方法里面:
protected void showImMsg(final MsgEntity entity)
到这里我们就可以找到在KaraokeRoomBaseActivity 的onCreate里面,开始加载页面的时候就新增了一条欢迎消息了:

消息的类型是TYPE_WELCOME,找到欢迎的文本和文本点击的链接是R.string.trtckaraoke_welcome_visit ,R.string.trtckaraoke_welcome_visit_link,在string.xml资源文件里面我们就可以重新设置为我们自己的,或者也可以不显示欢迎信息,把上面那段欢迎消息注释掉即可。
<string name="trtckaraoke_welcome_visit">"欢迎体验TRTC Karaoke!进一步了解如何快速搭建Karaoke,请点击: "</string>
<string name="trtckaraoke_welcome_visit_link">"https://cloud.tencent.com/document/product/647/59403"</string>
首先在KTV界面的xml 布局 trtckaraoke_activity_main.xml中找到礼物按钮
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/btn_more_gift"
style="@style/TRTCKtvRoomButtonStyle"
android:layout_marginEnd="20dp"
android:background="@drawable/trtckaraoke_ic_gift" />
接着查看礼物按钮的点击事件:
mBtnGift.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showGiftPanel();
}
});
事件中打开的就是展示礼物面板
//展示礼物面板
private void showGiftPanel() {
IGiftPanelView giftPanelView = new GiftPanelViewImp(this);
giftPanelView.init(mGiftInfoDataHandler);
giftPanelView.setGiftPanelDelegate(new GiftPanelDelegate() {
@Override
public void onGiftItemClick(GiftInfo giftInfo) {
sendGift(giftInfo);
}
@Override
public void onChargeClick() {
}
});
giftPanelView.show();
}
可以看到我们的礼物面板是一个弹窗GiftPanelViewImp,继承自BottomSheetDialog。它完整实现了礼物的接口IGiftPanelView,我们完全要定制礼物面板的话也需要实现这套接口:
public interface IGiftPanelView {
/**
* 面板通用接口
*/
void init(GiftInfoDataHandler giftInfoDataHandler);
/**
* 打开礼物面板
*/
void show();
/**
* 关闭礼物面板
*/
void hide();
//订阅礼物面板事件
void setGiftPanelDelegate(GiftPanelDelegate delegate);
}
创建一个实例就是通过void init(GiftInfoDataHandler giftInfoDataHandler)来初始化的,入参是一个GiftInfoDataHandler,它包含了如何获取礼物信息和获取成功失败后的回调。
看下GiftInfoDataHandler类:
public class GiftInfoDataHandler {
private static final String TAG = "GiftInfoManager";
private GiftAdapter mGiftAdapter;
private Map<String, GiftInfo> mGiftInfoMap = new HashMap<>();
public void setGiftAdapter(GiftAdapter adapter) {
mGiftAdapter = adapter;
queryGiftInfoList(null);
}
构造一个GiftInfoDataHandler需要一个实现了GiftAdapter的获取礼物信息的适配器mGiftAdapter。
public abstract class GiftAdapter {
/**
* 查询礼物信息
*
* @param callback
*/
public abstract void queryGiftInfoList(OnGiftListQueryCallback callback);
}
在KaraokeRoomBaseActivity类里可以找到initData房里初始化了mGiftInfoDataHandler,通过传入一个默认实现的DefaultGiftAdapterImp来构造mGiftInfoDataHandler:
// 礼物
GiftAdapter giftAdapter = new DefaultGiftAdapterImp();
mGiftInfoDataHandler = new GiftInfoDataHandler();
mGiftInfoDataHandler.setGiftAdapter(giftAdapter);
到这里已经是拨开云雾见青天啦,其实我们要找的就是这个DefaultGiftAdapterImp,是它实现如何获取礼物的,那么先看下DefaultGiftAdapterImp 类:
public class DefaultGiftAdapterImp extends GiftAdapter implements HttpGetRequest.HttpListener {
private static final String TAG = "DefaultGiftAdapterImp";
private static final int CORE_POOL_SIZE = 5;
private static final String GIFT_DATA_URL = "https://liteav.sdk.qcloud.com/app/res/picture/live/gift/gift_data.json";
private GiftBeanThreadPool mGiftBeanThreadPool;
private OnGiftListQueryCallback mOnGiftListQueryCallback;
@Override
public void queryGiftInfoList(final OnGiftListQueryCallback callback) {
mOnGiftListQueryCallback = callback;
ThreadPoolExecutor threadPoolExecutor = getThreadExecutor();
HttpGetRequest request = new HttpGetRequest(GIFT_DATA_URL, this);
threadPoolExecutor.execute(request);
}
我们看到它实现GiftAdapter接口的查询礼物信息的方法queryGiftInfoList,在queryGiftInfoList方法里面我们看到它是通过http请求从网络服务器上获取到了礼物数据的,接口地址就是
private static final String GIFT_DATA_URL = "https://liteav.sdk.qcloud.com/app/res/picture/live/gift/gift_data.json";
试着从浏览器里面打开接口看下返回数据

接口返回的数据类型是GiftBean,DefaultGiftAdapterImp又做了一层转换,转换成后面要方便处理的GiftData类型:
public class GiftData {
// 礼物id
public String giftId;
//礼物图片对应的url
public String giftPicUrl;
//礼物全屏动画url
public String lottieUrl;
//礼物的名称
public String title;
//礼物价格
public int price;
//礼物类型 0为普通礼物, 1为播放全屏动画
public int type;
}
到这里我们其实就可以模仿DefaultGiftAdapterImp做一套自己的实现,或者根据自己的业务需要实现自己的网络接口,按照GiftData模型返回数据类型就行。
GiftPanelViewImp的代码,它是通过一个ViewPager来切换不同页的,每页显示的礼物是一个List<View>。GiftPanelViewImp类里面找到mDefalutPanelType属性,它是个String类型,默认值是一个常量GIFT_PANEL_TYPE_SINGLEROW,指向的字符串是single_row,也就是单行,我们可以修改为多行显示。GIFT_PANEL_TYPE_SINGLEROW常量的文件里,我们找到多行显示的常量GIFT_PANEL_TYPE_MULTIROW,替换下mDefalutPanelType的值:private String mDefalutPanelType = GIFT_PANEL_TYPE_MULTIROW;
运行项目显示的效果如下:

显示了两行礼物,随着礼物数量增多就可以实时适配多行显示了。
public interface GiftPanelDelegate {
/**
* 礼物点击事件
*/
void onGiftItemClick(GiftInfo giftInfo);
/**
* 充值点击事件
*/
void onChargeClick();
}
在实例化礼物面板的时候,KaraokeRoomBaseActivity已经帮我们实现了礼物的点击事件了:
giftPanelView.setGiftPanelDelegate(new GiftPanelDelegate() {
@Override
public void onGiftItemClick(GiftInfo giftInfo) {
sendGift(giftInfo);
}
@Override
public void onChargeClick() {
}
});
sendGift方法就是发送礼物消息出去同时展示礼物动画和弹幕,还有处理弹幕消息的handleGiftMsg方法等。
充值点击事件就需要我们根据业务的需要自己实现啦。
查看TUIKaraoke并没有提供播放礼物动画,我们可以尝试着自己在TUIKaraoke的基础上简单实现一个使用Lottie播放动画的需求。
Lottie是支持Android, iOS, 和React Native,并且只需简单的代码就可以实现复杂动画效果的库,具体使用我们可以参考lottie-android。
在TUIKaraoke的build.gradle文件里面引入Lottie库:
implementation 'com.airbnb.android:lottie:5.2.0'
在KTV主页面布局trtckaraoke_activity_main里面最外层新增LottieAnimationView控件:
<androidx.constraintlayout.widget.ConstraintLayout
...
....
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lt_gift"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:lottie_autoPlay="false"
app:lottie_loop="false"
app:lottie_repeatMode="restart" />
</androidx.constraintlayout.widget.ConstraintLayout>
定义LottieAnimationView属性,并新增监听动画完成事件,播放完动画后隐藏控件。
private LottieAnimationView mLottieAnimationView;
mLottieAnimationView = findViewById(R.id.lt_gift);
mLottieAnimationView.addAnimatorListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mLottieAnimationView.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationEnd(Animator animation) {
mLottieAnimationView.setVisibility(View.GONE);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
新增一个方法来开始播放动画:
//展示礼物动画
private void showGiftLottieAnimation(String lottieUrl) {
mLottieAnimationView.setVisibility(View.VISIBLE);
mLottieAnimationView.setAnimationFromUrl(lottieUrl);
mLottieAnimationView.playAnimation();
}
接下来需要在两个地方添加播放动画,一个就是点击礼物的时候播放,另一个是在收到别人发送礼物的时候播放动画了。
在打开礼物面板初始化的时候,礼物面板的点击礼物的delega里面:
//展示礼物面板
private void showGiftPanel() {
IGiftPanelView giftPanelView = new GiftPanelViewImp(this);
giftPanelView.init(mGiftInfoDataHandler);
giftPanelView.setGiftPanelDelegate(new GiftPanelDelegate() {
@Override
public void onGiftItemClick(GiftInfo giftInfo) {
sendGift(giftInfo);
//播放动画
if (!giftInfo.lottieUrl.isEmpty()) {
showGiftLottieAnimation(giftInfo.lottieUrl);
}
}
@Override
public void onChargeClick() {
}
});
giftPanelView.show();
}
在收到消息里面,TUIKaraoke通过handleGiftMsg方法处理弹幕消息,在这个方法里面可以播放我们的礼物动画:
/**
* 处理礼物弹幕消息
*/
private void handleGiftMsg(TRTCKaraokeRoomDef.UserInfo userInfo, String data) {
if (mGiftInfoDataHandler != null) {
Gson gson = new Gson();
GiftSendJson jsonData = gson.fromJson(data, GiftSendJson.class);
String giftId = jsonData.getGiftId();
GiftInfo giftInfo = mGiftInfoDataHandler.getGiftInfo(giftId);
if (giftInfo != null) {
if (userInfo != null) {
giftInfo.sendUserHeadIcon = userInfo.userAvatar;
if (!TextUtils.isEmpty(userInfo.userName)) {
giftInfo.sendUser = userInfo.userName;
} else {
giftInfo.sendUser = userInfo.userId;
}
}
mGiftAnimatorLayout.show(giftInfo);
//播放动画
if (!giftInfo.lottieUrl.isEmpty()) {
showGiftLottieAnimation(giftInfo.lottieUrl);
}
}
}
}
至此,当我们点击了含有lottieUrl的礼物的时候就可以看到礼物动画了。

总结一下,很多UI组件和功能TUIKaraoke都帮我们实现了,我们只需要在项目里面接入就行了,然后在TUIKaraoke里面修改下配置,通过接口增加我们的实现,也可以在TUIKaraoke的基础上稍加修改就可以基本上完成我们的需求了。
目录1.AdmobSDK下载地址2.将下载好的unityPackagesdk导入到unity里编辑 3.解析依赖到项目中
本文主要介绍在使用Selenium进行自动化测试或者任务时,对于使用了iframe的页面,如何定位iframe中的元素文章目录场景描述解决方案具体代码场景描述当我们在使用Selenium进行自动化测试的时候,可能会遇到一些界面或者窗体是使用HTML的iframe标签进行承载的。对于iframe中的标签,如果直接查找是无法找到的,会抛出没有找到元素的异常。比如近在咫尺的例子就是,CSDN的登录窗体就是使用的iframe,大家可以尝试通过F12开发者模式查看到的tag_name,class_name,id或者xpath来定位中的页面元素,会抛出NoSuchElementException异常。解决
有没有办法快速将表格格式的ruby哈希打印到文件中?如:keyAkeyBkeyC...1232343451253474456...其中散列的值是不同大小的数组。还是使用双循环是唯一的方法?谢谢 最佳答案 试试我写的这个gem(在表中打印散列、ruby对象、ActiveRecord对象):http://github.com/arches/table_print 关于ruby-如何以表格格式快速打印Ruby哈希值?,我们在StackOverflow上找到一个类似的问题:
电脑启动出现显示器黑屏是一个相当常见的问题。如果您遇到了这个问题,不要惊慌,因为它有很多可能的原因,可以采取一些简单的措施来解决它。在本文中,小编将介绍下面4种常见的电脑启动后显示器黑屏的原因,排查这些原因,快速解决! 演示机型:联想Ideapad700-15ISK-ISE系统版本:Windows10一、显示器问题如果出现电脑启动后显示器黑屏的情况。那么首先您需要检查一下显示器是否正常工作。您可以通过更换另一个显示器或将当前显示器连接到另一台计算机来检查显示器是否存在问题。如果问题仍然存在,那么您可以排除显示器故障的可能性。 二、显卡问题如果您的电脑配备了独立显卡,那么显卡故障也可能是导致电脑
mutationtesting遇到一个问题是它很慢,因为默认情况下您会为每个生成的突变执行完整的测试运行(测试文件或一组测试文件)。加快突变测试的一种方法是,一旦遇到单一故障(但仅在突变测试期间),就停止对给定突变体的测试运行。更好的做法是让变异测试者记住杀死最后一个变异体的第一个测试是什么,并将其首先交给下一个变异体。ruby中是否有任何东西可以做这些事情,或者我最好的选择是开始猴子修补?(是的,我知道单元测试应该很快。显示所有失败的测试在突变测试之外很有用,因为它不仅可以帮助您识别出问题,还可以查明哪里出了问题)编辑:我目前正在对测试/单元使用heckle。如果测试/单元不可能记住
我有两个类:1.Sale是ActiveRecord的子类;它的工作是将销售数据持久保存到数据库中。classSale2.SalesReport是一个标准的Ruby类;它的工作是生成和绘制有关销售的信息。classSalesReportdefinitialize(start_date,end_date)@start_date=start_date@end_date=end_dateenddefsales_in_durationSale.total_for_duration(@start_date,@end_date)end#...end因为我想使用TDD并且我希望我的测试运行得非常快,所
一、离线方式1.1.下载ip2region.xdbGitHub项目地址:https://github.com/lionsoul2014/ip2region我们首先需要下载一个ip2region.xdb的文件下载地址:https://github.com/lionsoul2014/ip2region/blob/master/data/ip2region.xdb打开后点击如图的Download图标即可下载。下载完成后,需要将该文件放到我们的项目中。ps:我是直接放到服务器的,因为放在项目的资源文件夹下,当我们调试的时候使用JavaSpring自带的工具去获取该文件的绝对路径时,没有任何问题,能够正
三大公有云厂商,香港地区主机测评一、ping时延比对(厦门电信本地测试):Ping时延测试腾讯云阿里云华为云延迟率最低时延44ms,最高72ms,平均46ms47.242段:最低时延59ms,最高204ms,平均107ms最低时延45ms,最高93ms,平均47ms丢包率丢包率小有的ip段丢包率较大每个段都会有概率丢包阿里云:47.242段:最低时延59ms,最高204ms,平均107ms,有的ip段丢包率较大8.210段:最低时延64ms,最高232ms,平均119ms,丢包率较好腾讯云:最低时延44ms,最高72ms,平均46ms,丢包率小华为云:最低时延45ms,最高93ms,平均47m
“架设一个亿级高并发系统,是多数程序员、架构师的工作目标。许多的技术从业人员甚至有时会降薪去寻找这样的机会。但并不是所有人都有机会主导,甚至参与这样一个系统。今天我们用12306火车票购票这样一个业务场景来做DDD领域建模。”开篇要实现软件设计、软件开发在一个统一的思想、统一的节奏下进行,就应该有一个轻量级的框架对开发过程与代码编写做一定的约束。虽然DDD是一个软件开发的方法,而不是具体的技术或框架,但拥有一个轻量级的框架仍然是必要的,为了开发一个支持DDD的框架,首先需要理解DDD的基本概念和核心的组件。一.什么是领域驱动设计(DDD)首先要知道DDD是一种开发理念,核心是维护一个反应领域概
我正在寻找一个快速、无需配置的FTP服务器。完全像Serve的东西或Rack_dav,但对于FTP,它可以通过运行命令来发布文件夹。是否有gem或其他东西可以做这样的事情?解决方案基于Wayne的ftpdgem,我创建了一个快速且易于使用的gem,名为Purvey. 最佳答案 ftpdgem支持TLS,并带有文件系统驱动程序。与em-ftpd一样,您提供一个驱动程序,但该驱动程序不需要做太多事情。这是一个最低限度的FTP服务器,它接受任何用户名/密码,并提供临时目录中的文件:require'ftpd'require'tmpdir'c