조건은 다음과 같다.
코드로 살펴보자.
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/bounce_interpolator">
<scale
android:duration="500"
android:fromXScale="0"
android:fromYScale="0"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="1.0"
android:toYScale="1.0" />
</set>
class MessageAdapter(val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var messageList = listOf<Message>()
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerView.ViewHolder {
return when (viewType) {
SEND_BY_USER -> {
val view =
ItemChatUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
UserViewHolder(view)
}
SEND_BY_BOT -> {
val view =
ItemChatBotBinding.inflate(LayoutInflater.from(parent.context), parent, false)
BotViewHolder(view)
}
else -> {
val view =
ItemChatLineBinding.inflate(LayoutInflater.from(parent.context), parent, false)
LineViewHolder(view)
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val curMsg = messageList[position]
when (curMsg.sendBy) {
SEND_BY_USER -> {
(holder as UserViewHolder).bind(curMsg)
}
SEND_BY_BOT -> {
(holder as BotViewHolder).bind(curMsg)
}
SEND_BY_LINE -> {
(holder as LineViewHolder).bind(curMsg)
}
}
}
override fun getItemCount(): Int {
return messageList.size
}
override fun getItemViewType(position: Int): Int {
return messageList[position].sendBy
}
fun setMessageList(newList: List<Message>) {
messageList = newList
notifyDataSetChanged()
}
inner class UserViewHolder(binding: ItemChatUserBinding) :
RecyclerView.ViewHolder(binding.root) {
var userTxt = binding.itemMsgUserTv
fun bind(item: Message) {
userTxt.text = item.message
}
}
inner class BotViewHolder(binding: ItemChatBotBinding) : RecyclerView.ViewHolder(binding.root) {
var chatTxt = binding.itemMsgBotTv
init {
val bounceAnim = AnimationUtils.loadAnimation(context, R.anim.bounce)
chatTxt.startAnimation(bounceAnim)
}
fun bind(item: Message) {
chatTxt.text = item.message
}
}
inner class LineViewHolder(binding: ItemChatLineBinding) :
RecyclerView.ViewHolder(binding.root) {
var versionTxt = binding.itemMsgVersionTv
fun bind(item: Message) {
versionTxt.text = item.message
}
}
}
전체 코드를 살펴보면 좋을 것 같아서 전부 넣었다. 이제 요구 조건들을 해결하고 있는 블록을 떠와서 자세히보자.
우선 특정 type에만 애니메이션이 넣어져야 한다. 챗봇을 구현하는 recyclerview에 그려지는 type은 총 세 개로, BOT
, USER
, LINE
인데 봇의 채팅이 출력되는 부분에만 애니메이션을 넣고 싶었다. 그래서 해당 부분의 ViewHolder에만 추가했다.
inner class BotViewHolder(binding: ItemChatBotBinding) : RecyclerView.ViewHolder(binding.root) {
var chatTxt = binding.itemMsgBotTv
init {
val bounceAnim = AnimationUtils.loadAnimation(context, R.anim.bounce)
chatTxt.startAnimation(bounceAnim)
}
fun bind(item: Message) {
chatTxt.text = item.message
}
}
애니메이션 관련 코드는 init
에 정의해야 한다. 그렇지 않고 bind
메서드에서 해결하게 되면 아이템이 추가될 때마다 해당 아이템에만 애니메이션이 적용되는 게 아니라, 전체 아이템의 모든 BOT
type 아이템에 애니메이션이 적용되기 때문이다.
ViewFlipper
를 활용하면 위와 같은 애니메이션을 넣을 수 있다.
ViewFlipper
는 여러 개의 자식 뷰를 가지고 있으면서 하나의 자식 뷰만 보여주는 컨테이너다. 자식 뷰들 중 하나를 선택하여 다른 자식 뷰로 전환할 때, 전환 애니메이션을 적용할 수 있다. 기본적으로 자동으로 전환되는 기능이 있다. 그래서 다음 자식 뷰로 전환할 때는 설정된 전환 애니메이션을 실행하고, 자동으로 일정 시간이 지나면 다음 자식 뷰로 자동으로 전환된다.
우선 기존 상대 채팅 xml에 다음 코드를 추가한다.
<ViewFlipper
android:id="@+id/typing_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inAnimation="@anim/fade_in"
android:outAnimation="@anim/fade_out"
android:padding="16dp">
<View
android:id="@+id/temp1"
android:layout_width="10dp"
android:layout_height="10dp"
android:background="@drawable/baseline_circle_24" />
<View
android:id="@+id/temp2"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginStart="13dp"
android:background="@drawable/baseline_circle_24"
android:backgroundTint="@color/white" />
<View
android:id="@+id/temp3"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginStart="26dp"
android:background="@drawable/baseline_circle_24" />
</ViewFlipper>
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:fromAlpha="0.0"
android:toAlpha="1.0"
android:duration="500"
android:interpolator="@android:anim/accelerate_decelerate_interpolator" />
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:duration="500"
android:interpolator="@android:anim/accelerate_decelerate_interpolator" />
그리고 typing_indicator를 제어하기 위해 recyclerview adpater를 다음과 같이 구성한다.
inner class BotViewHolder(binding: ItemChatBotBinding) : RecyclerView.ViewHolder(binding.root) {
var chatTxt = binding.itemMsgBotTv
private val typingIndicator: ViewFlipper = binding.typingIndicator
init {
val bounceAnim = AnimationUtils.loadAnimation(context, R.anim.bounce)
chatTxt.startAnimation(bounceAnim)
}
fun bind(item: Message) {
chatTxt.text = item.message
if (item.sendBy == SEND_BY_TYPING) {
typingIndicator.apply {
visibility = View.VISIBLE
startFlipping()
flipInterval = 200
}
} else {
typingIndicator.apply {
visibility = View.GONE
stopFlipping()
}
}
}
}
startFlipping()
과 stopFlipping()
을 통해 점의 움직임을 제어한다.flipInterval
을 활용하면 속도를 조절할 수 있다. 단위는 밀리초다.addView()
로 자식 뷰를 추가할 수도 있다.setDisplayedChild()
로 전환할 자식 뷰의 인덱스 지정도 가능하다.