show detailed list of voters

+ refactoring adapter and viewholders for result screen

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2022-06-29 15:17:50 +02:00 committed by Andy Scherzinger (Rebase PR Action)
parent 909ee07ce6
commit 73d48a395c
13 changed files with 324 additions and 179 deletions

View File

@ -0,0 +1,19 @@
package com.nextcloud.talk.polls.adapters
import com.nextcloud.talk.R
data class PollResultHeaderItem(
val name: String,
val percent: Int,
val selfVoted: Boolean
) : PollResultItem {
override fun getViewType(): Int {
return VIEW_TYPE
}
companion object {
// layout is used as view type for uniqueness
public val VIEW_TYPE: Int = R.layout.poll_result_header_item
}
}

View File

@ -0,0 +1,29 @@
package com.nextcloud.talk.polls.adapters
import android.annotation.SuppressLint
import android.graphics.Typeface
import com.nextcloud.talk.databinding.PollResultHeaderItemBinding
import com.nextcloud.talk.models.database.UserEntity
class PollResultHeaderViewHolder(
private val user: UserEntity,
override val binding: PollResultHeaderItemBinding
) : PollResultViewHolder(binding) {
@SuppressLint("SetTextI18n")
override fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener) {
val item = pollResultItem as PollResultHeaderItem
binding.root.setOnClickListener { clickListener.onClick(pollResultItem) }
binding.pollOptionText.text = item.name
binding.pollOptionPercentText.text = "${item.percent}%"
if (item.selfVoted) {
binding.pollOptionText.setTypeface(null, Typeface.BOLD)
binding.pollOptionPercentText.setTypeface(null, Typeface.BOLD)
}
binding.pollOptionBar.progress = item.percent
}
}

View File

@ -1,10 +1,7 @@
package com.nextcloud.talk.polls.adapters package com.nextcloud.talk.polls.adapters
import com.nextcloud.talk.polls.model.PollDetails interface PollResultItem {
class PollResultItem( fun getViewType(): Int
val name: String, // fun getView(inflater: LayoutInflater?, convertView: View?): View?
val percent: Int, }
val selfVoted: Boolean,
val voters: List<PollDetails>?
)

View File

@ -1,5 +1,5 @@
package com.nextcloud.talk.polls.adapters package com.nextcloud.talk.polls.adapters
interface PollResultItemClickListener { interface PollResultItemClickListener {
fun onClick(pollResultItem: PollResultItem) fun onClick(pollResultHeaderItem: PollResultHeaderItem)
} }

View File

@ -1,107 +1,10 @@
package com.nextcloud.talk.polls.adapters package com.nextcloud.talk.polls.adapters
import android.annotation.SuppressLint
import android.graphics.Typeface
import android.text.TextUtils
import android.view.View
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.facebook.drawee.backends.pipeline.Fresco import androidx.viewbinding.ViewBinding
import com.facebook.drawee.generic.RoundingParams
import com.facebook.drawee.interfaces.DraweeController
import com.facebook.drawee.view.SimpleDraweeView
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.databinding.PollResultItemBinding
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.polls.model.PollDetails
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils
class PollResultViewHolder( abstract class PollResultViewHolder(
private val user: UserEntity, open val binding: ViewBinding
private val binding: PollResultItemBinding
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
abstract fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener)
@SuppressLint("SetTextI18n") }
fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener) {
binding.root.setOnClickListener { clickListener.onClick(pollResultItem) }
binding.root.setOnClickListener { clickListener.onClick(pollResultItem) }
binding.pollOptionText.text = pollResultItem.name
binding.pollOptionPercentText.text = "${pollResultItem.percent}%"
if (pollResultItem.selfVoted) {
binding.pollOptionText.setTypeface(null, Typeface.BOLD)
binding.pollOptionPercentText.setTypeface(null, Typeface.BOLD)
}
binding.pollOptionBar.progress = pollResultItem.percent
if (!pollResultItem.voters.isNullOrEmpty()) {
binding.pollOptionDetail.visibility = View.VISIBLE
val lp = LinearLayout.LayoutParams(
60,
50
)
pollResultItem.voters.forEach {
val avatar = SimpleDraweeView(binding.root.context)
avatar.layoutParams = lp
val roundingParams = RoundingParams.fromCornersRadius(5f)
roundingParams.roundAsCircle = true
avatar.hierarchy.roundingParams = roundingParams
avatar.controller = getAvatarDraweeController(it)
binding.pollOptionDetail.addView(avatar)
}
} else {
binding.pollOptionDetail.visibility = View.GONE
}
}
private fun getAvatarDraweeController(pollDetail: PollDetails): DraweeController? {
if (pollDetail.actorType == "guests") {
var displayName = NextcloudTalkApplication.sharedApplication?.resources?.getString(R.string.nc_guest)
if (!TextUtils.isEmpty(pollDetail.actorDisplayName)) {
displayName = pollDetail.actorDisplayName!!
}
val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
// .setOldController(binding.avatar.controller)
.setAutoPlayAnimations(true)
.setImageRequest(
DisplayUtils.getImageRequestForUrl(
ApiUtils.getUrlForGuestAvatar(
user.baseUrl,
displayName,
false
),
null
)
)
.build()
return draweeController
} else if (pollDetail.actorType == "users") {
val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
// .setOldController(binding.avatar.controller)
.setAutoPlayAnimations(true)
.setImageRequest(
DisplayUtils.getImageRequestForUrl(
ApiUtils.getUrlForAvatar(
user.baseUrl,
pollDetail.actorId,
false
),
null
)
)
.build()
return draweeController
}
return null
}
}

View File

@ -0,0 +1,18 @@
package com.nextcloud.talk.polls.adapters
import com.nextcloud.talk.R
import com.nextcloud.talk.polls.model.PollDetails
data class PollResultVoterItem(
val details: PollDetails
) : PollResultItem {
override fun getViewType(): Int {
return VIEW_TYPE
}
companion object {
// layout is used as view type for uniqueness
public val VIEW_TYPE: Int = R.layout.poll_result_voter_item
}
}

View File

@ -0,0 +1,84 @@
package com.nextcloud.talk.polls.adapters
import android.annotation.SuppressLint
import android.text.TextUtils
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.drawee.interfaces.DraweeController
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.databinding.PollResultVoterItemBinding
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.polls.model.PollDetails
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils
class PollResultVoterViewHolder(
private val user: UserEntity,
override val binding: PollResultVoterItemBinding
) : PollResultViewHolder(binding) {
@SuppressLint("SetTextI18n")
override fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener) {
val item = pollResultItem as PollResultVoterItem
// binding.root.setOnClickListener { clickListener.onClick(pollResultVoterItem) }
// binding.pollVoterAvatar = pollResultHeaderItem.name
binding.pollVoterName.text = item.details.actorDisplayName
// val lp = LinearLayout.LayoutParams(
// 60,
// 50
// )
//
// val avatar = SimpleDraweeView(binding.root.context)
// avatar.layoutParams = lp
// val roundingParams = RoundingParams.fromCornersRadius(5f)
// roundingParams.roundAsCircle = true
//
// binding.pollVoterAvatar.hierarchy.roundingParams = roundingParams
binding.pollVoterAvatar.controller = getAvatarDraweeController(item.details)
}
private fun getAvatarDraweeController(pollDetail: PollDetails): DraweeController? {
if (pollDetail.actorType == "guests") {
var displayName = NextcloudTalkApplication.sharedApplication?.resources?.getString(R.string.nc_guest)
if (!TextUtils.isEmpty(pollDetail.actorDisplayName)) {
displayName = pollDetail.actorDisplayName!!
}
val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
// .setOldController(binding.avatar.controller)
.setAutoPlayAnimations(true)
.setImageRequest(
DisplayUtils.getImageRequestForUrl(
ApiUtils.getUrlForGuestAvatar(
user.baseUrl,
displayName,
false
),
null
)
)
.build()
return draweeController
} else if (pollDetail.actorType == "users") {
val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
// .setOldController(binding.avatar.controller)
.setAutoPlayAnimations(true)
.setImageRequest(
DisplayUtils.getImageRequestForUrl(
ApiUtils.getUrlForAvatar(
user.baseUrl,
pollDetail.actorId,
false
),
null
)
)
.build()
return draweeController
}
return null
}
}

View File

@ -3,26 +3,56 @@ package com.nextcloud.talk.polls.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.nextcloud.talk.databinding.PollResultItemBinding import com.nextcloud.talk.databinding.PollResultHeaderItemBinding
import com.nextcloud.talk.databinding.PollResultVoterItemBinding
import com.nextcloud.talk.models.database.UserEntity import com.nextcloud.talk.models.database.UserEntity
class PollResultsAdapter( class PollResultsAdapter(
private val user: UserEntity, private val user: UserEntity,
private val clickListener: PollResultItemClickListener, private val clickListener: PollResultItemClickListener,
) : RecyclerView.Adapter<PollResultViewHolder>() { ) : RecyclerView.Adapter<PollResultViewHolder>() {
internal var list: MutableList<PollResultItem> = ArrayList<PollResultItem>() internal var list: MutableList<PollResultItem> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PollResultViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PollResultViewHolder {
val itemBinding = PollResultItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) when (viewType) {
return PollResultViewHolder(user, itemBinding) PollResultHeaderItem.VIEW_TYPE -> {
val itemBinding = PollResultHeaderItemBinding.inflate(
LayoutInflater.from(parent.context), parent,
false
)
return PollResultHeaderViewHolder(user, itemBinding)
}
PollResultVoterItem.VIEW_TYPE -> {
val itemBinding = PollResultVoterItemBinding.inflate(
LayoutInflater.from(parent.context), parent,
false
)
return PollResultVoterViewHolder(user, itemBinding)
}
}
val itemBinding = PollResultHeaderItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return PollResultHeaderViewHolder(user, itemBinding)
} }
override fun onBindViewHolder(holder: PollResultViewHolder, position: Int) { override fun onBindViewHolder(holder: PollResultViewHolder, position: Int) {
val pollResultItem = list[position] when (holder.itemViewType) {
holder.bind(pollResultItem, clickListener) PollResultHeaderItem.VIEW_TYPE -> {
val pollResultItem = list[position]
holder.bind(pollResultItem as PollResultHeaderItem, clickListener)
}
PollResultVoterItem.VIEW_TYPE -> {
val pollResultItem = list[position]
holder.bind(pollResultItem as PollResultVoterItem, clickListener)
}
}
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
return list.size return list.size
} }
override fun getItemViewType(position: Int): Int {
return list[position].getViewType()
}
} }

View File

@ -22,7 +22,6 @@
package com.nextcloud.talk.polls.ui package com.nextcloud.talk.polls.ui
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -33,10 +32,9 @@ import autodagger.AutoInjector
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.databinding.DialogPollResultsBinding import com.nextcloud.talk.databinding.DialogPollResultsBinding
import com.nextcloud.talk.models.database.UserEntity import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.polls.adapters.PollResultItem import com.nextcloud.talk.polls.adapters.PollResultHeaderItem
import com.nextcloud.talk.polls.adapters.PollResultItemClickListener import com.nextcloud.talk.polls.adapters.PollResultItemClickListener
import com.nextcloud.talk.polls.adapters.PollResultsAdapter import com.nextcloud.talk.polls.adapters.PollResultsAdapter
import com.nextcloud.talk.polls.model.Poll
import com.nextcloud.talk.polls.viewmodels.PollMainViewModel import com.nextcloud.talk.polls.viewmodels.PollMainViewModel
import com.nextcloud.talk.polls.viewmodels.PollResultsViewModel import com.nextcloud.talk.polls.viewmodels.PollResultsViewModel
import javax.inject.Inject import javax.inject.Inject
@ -81,11 +79,18 @@ class PollResultsFragment(
parentViewModel.viewState.observe(viewLifecycleOwner) { state -> parentViewModel.viewState.observe(viewLifecycleOwner) { state ->
if (state is PollMainViewModel.PollResultState) { if (state is PollMainViewModel.PollResultState) {
initAdapter() initAdapter()
initPollResults(state.poll) viewModel.setPoll(state.poll)
initEditButton(state.showEditButton) initEditButton(state.showEditButton)
initCloseButton(state.showCloseButton) initCloseButton(state.showCloseButton)
} }
} }
viewModel.items.observe(viewLifecycleOwner) {
val adapter = PollResultsAdapter(user, this).apply {
list = it
}
binding.pollResultsList.adapter = adapter
}
} }
private fun initAdapter() { private fun initAdapter() {
@ -94,51 +99,6 @@ class PollResultsFragment(
_binding?.pollResultsList?.layoutManager = LinearLayoutManager(context) _binding?.pollResultsList?.layoutManager = LinearLayoutManager(context)
} }
private fun initPollResults(poll: Poll) {
if (poll.details != null) {
val votersAmount = poll.details.size
val oneVoteInPercent = 100 / votersAmount // TODO: poll.numVoters when fixed on api
poll.options?.forEachIndexed { index, option ->
val votersForThisOption = poll.details.filter { it.optionId == index }
val optionsPercent = oneVoteInPercent * votersForThisOption.size
val pollResultItem = PollResultItem(
option,
optionsPercent,
isOptionSelfVoted(poll, index),
votersForThisOption
)
adapter?.list?.add(pollResultItem)
}
} else if (poll.votes != null) {
val votersAmount = poll.numVoters
val oneVoteInPercent = 100 / votersAmount
poll.options?.forEachIndexed { index, option ->
var votersAmountForThisOption = poll.votes.filter { it.key.toInt() == index }[index.toString()]
if (votersAmountForThisOption == null) {
votersAmountForThisOption = 0
}
val optionsPercent = oneVoteInPercent * votersAmountForThisOption
val pollResultItem = PollResultItem(
option,
optionsPercent,
isOptionSelfVoted(poll, index),
null
)
adapter?.list?.add(pollResultItem)
}
} else {
Log.e(TAG, "failed to get data to show poll results")
}
}
private fun isOptionSelfVoted(poll: Poll, index: Int): Boolean {
return poll.votedSelf?.contains(index) == true
}
private fun initEditButton(showEditButton: Boolean) { private fun initEditButton(showEditButton: Boolean) {
if (showEditButton) { if (showEditButton) {
_binding?.editVoteButton?.visibility = View.VISIBLE _binding?.editVoteButton?.visibility = View.VISIBLE
@ -161,8 +121,8 @@ class PollResultsFragment(
} }
} }
override fun onClick(pollResultItem: PollResultItem) { override fun onClick(pollResultHeaderItem: PollResultHeaderItem) {
Log.d(TAG, "click..") viewModel.filterItems()
} }
override fun onDestroyView() { override fun onDestroyView() {

View File

@ -24,6 +24,10 @@ package com.nextcloud.talk.polls.viewmodels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.nextcloud.talk.polls.adapters.PollResultHeaderItem
import com.nextcloud.talk.polls.adapters.PollResultItem
import com.nextcloud.talk.polls.adapters.PollResultVoterItem
import com.nextcloud.talk.polls.model.Poll
import com.nextcloud.talk.polls.repositories.PollRepository import com.nextcloud.talk.polls.repositories.PollRepository
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import javax.inject.Inject import javax.inject.Inject
@ -33,10 +37,20 @@ class PollResultsViewModel @Inject constructor(private val repository: PollRepos
sealed interface ViewState sealed interface ViewState
object InitialState : ViewState object InitialState : ViewState
private var _poll: Poll? = null
val poll: Poll?
get() = _poll
private val _viewState: MutableLiveData<ViewState> = MutableLiveData(InitialState) private val _viewState: MutableLiveData<ViewState> = MutableLiveData(InitialState)
val viewState: LiveData<ViewState> val viewState: LiveData<ViewState>
get() = _viewState get() = _viewState
private var _unfilteredItems: ArrayList<PollResultItem> = ArrayList()
private var _items: MutableLiveData<ArrayList<PollResultItem>> = MutableLiveData<ArrayList<PollResultItem>>()
val items: LiveData<ArrayList<PollResultItem>>
get() = _items
private var disposable: Disposable? = null private var disposable: Disposable? = null
override fun onCleared() { override fun onCleared() {
@ -44,6 +58,82 @@ class PollResultsViewModel @Inject constructor(private val repository: PollRepos
disposable?.dispose() disposable?.dispose()
} }
fun setPoll(poll: Poll) {
_poll = poll
initPollResults(_poll!!)
}
private fun initPollResults(poll: Poll) {
_items.value = ArrayList()
val votersAmount = getVotersAmount(poll)
val oneVoteInPercent = 100 / votersAmount
poll.options?.forEachIndexed { index, option ->
val votersAmountForThisOption = getVotersAmountForOption(poll, index)
val optionsPercent = oneVoteInPercent * votersAmountForThisOption
val pollResultHeaderItem = PollResultHeaderItem(
option,
optionsPercent,
isOptionSelfVoted(poll, index)
)
addToItems(pollResultHeaderItem)
val voters = poll.details?.filter { it.optionId == index }
if (!voters.isNullOrEmpty()) {
voters.forEach {
addToItems(PollResultVoterItem(it))
}
}
}
_unfilteredItems = _items.value?.let { ArrayList(it) }!!
}
private fun addToItems(pollResultItem: PollResultItem) {
val tempList = _items.value
tempList!!.add(pollResultItem)
_items.value = tempList
}
private fun getVotersAmount(poll: Poll): Int {
if (poll.details != null) {
return poll.details.size
} else if (poll.votes != null) {
return poll.numVoters
}
return 0
}
private fun getVotersAmountForOption(poll: Poll, index: Int): Int {
var votersAmountForThisOption: Int? = 0
if (poll.details != null) {
votersAmountForThisOption = poll.details.filter { it.optionId == index }.size
} else if (poll.votes != null) {
votersAmountForThisOption = poll.votes.filter { it.key.toInt() == index }[index.toString()]
if (votersAmountForThisOption == null) {
votersAmountForThisOption = 0
}
}
return votersAmountForThisOption!!
}
private fun isOptionSelfVoted(poll: Poll, index: Int): Boolean {
return poll.votedSelf?.contains(index) == true
}
fun filterItems() {
if (_items.value?.containsAll(_unfilteredItems) == true) {
val filteredList = _items.value?.filter { it.getViewType() == PollResultHeaderItem.VIEW_TYPE } as
MutableList<PollResultItem>
_items.value = ArrayList(filteredList)
} else {
_items.value = _unfilteredItems
}
}
companion object { companion object {
private val TAG = PollResultsViewModel::class.java.simpleName private val TAG = PollResultsViewModel::class.java.simpleName
} }

View File

@ -34,7 +34,7 @@
android:id="@+id/poll_results_list" android:id="@+id/poll_results_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:listitem="@layout/poll_result_item" /> tools:listitem="@layout/poll_result_header_item" />
</LinearLayout> </LinearLayout>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton

View File

@ -34,17 +34,7 @@
app:trackColor="@color/dialog_background" app:trackColor="@color/dialog_background"
app:trackCornerRadius="5dp" app:trackCornerRadius="5dp"
app:trackThickness="5dp" app:trackThickness="5dp"
android:paddingBottom="@dimen/standard_half_padding"
tools:progress="50" /> tools:progress="50" />
<LinearLayout
android:id="@+id/poll_option_detail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/poll_option_text"
app:layout_constraintTop_toBottomOf="@+id/poll_option_bar" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,25 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="4dp"
tools:background="@color/white">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/poll_voter_avatar"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="8dp"
android:layout_gravity="center"
app:roundAsCircle="true" />
<TextView
android:id="@+id/poll_voter_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
tools:text="Bill Murray" />
</LinearLayout>