Unlimited number of poll options (#1340)

* implement unlimited number of poll options

* fixes

* extract percent calculation into function so it can be used anywhere

* add license header
This commit is contained in:
Konrad Pozniak 2019-06-22 21:55:03 +02:00 committed by GitHub
commit b95ff10a3c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 325 additions and 689 deletions

View file

@ -0,0 +1,123 @@
/* Copyright 2019 Conny Duck
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.RadioButton
import android.widget.TextView
import androidx.emoji.text.EmojiCompat
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.util.CustomEmojiHelper
import com.keylesspalace.tusky.util.HtmlUtils
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.viewdata.PollOptionViewData
import com.keylesspalace.tusky.viewdata.calculatePercent
class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
private var pollOptions: List<PollOptionViewData> = emptyList()
private var voteCount: Int = 0
private var mode = RESULT
private var emojis: List<Emoji> = emptyList()
fun setup(options: List<PollOptionViewData>, voteCount: Int, emojis: List<Emoji>, mode: Int) {
this.pollOptions = options
this.voteCount = voteCount
this.emojis = emojis
this.mode = mode
notifyDataSetChanged()
}
fun getSelected() : List<Int> {
return pollOptions.filter { it.selected }
.map { pollOptions.indexOf(it) }
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PollViewHolder {
return PollViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_poll, parent, false))
}
override fun getItemCount(): Int {
return pollOptions.size
}
override fun onBindViewHolder(holder: PollViewHolder, position: Int) {
val option = pollOptions[position]
holder.resultTextView.visible(mode == RESULT)
holder.radioButton.visible(mode == SINGLE)
holder.checkBox.visible(mode == MULTIPLE)
when(mode) {
RESULT -> {
val percent = calculatePercent(option.votesCount, voteCount)
val pollOptionText = holder.resultTextView.context.getString(R.string.poll_option_format, percent, option.title)
val emojifiedPollOptionText = CustomEmojiHelper.emojifyText(HtmlUtils.fromHtml(pollOptionText), emojis, holder.resultTextView)
holder.resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText)
val level = percent * 100
holder.resultTextView.background.level = level
}
SINGLE -> {
val emojifiedPollOptionText = CustomEmojiHelper.emojifyString(option.title, emojis, holder.radioButton)
holder.radioButton.text = EmojiCompat.get().process(emojifiedPollOptionText)
holder.radioButton.isChecked = option.selected
holder.radioButton.setOnClickListener {
pollOptions.forEachIndexed { index, pollOption ->
pollOption.selected = index == holder.adapterPosition
notifyItemChanged(index)
}
}
}
MULTIPLE -> {
val emojifiedPollOptionText = CustomEmojiHelper.emojifyString(option.title, emojis, holder.checkBox)
holder.checkBox.text = EmojiCompat.get().process(emojifiedPollOptionText)
holder.checkBox.isChecked = option.selected
holder.checkBox.setOnCheckedChangeListener { _, isChecked ->
pollOptions[holder.adapterPosition].selected = isChecked
}
}
}
}
companion object {
const val RESULT = 0
const val SINGLE = 1
const val MULTIPLE = 2
}
}
class PollViewHolder(view: View): RecyclerView.ViewHolder(view) {
val resultTextView: TextView = view.findViewById(R.id.status_poll_option_result)
val radioButton: RadioButton = view.findViewById(R.id.status_poll_radio_button)
val checkBox: CheckBox = view.findViewById(R.id.status_poll_checkbox)
}

View file

@ -7,11 +7,8 @@ import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;
@ -19,7 +16,8 @@ import android.widget.ToggleButton;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.emoji.text.EmojiCompat;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
@ -28,8 +26,6 @@ import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Attachment.Focus;
import com.keylesspalace.tusky.entity.Attachment.MetaData;
import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Poll;
import com.keylesspalace.tusky.entity.PollOption;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
@ -39,13 +35,14 @@ import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.view.MediaPreviewImageView;
import com.keylesspalace.tusky.viewdata.PollOptionViewData;
import com.keylesspalace.tusky.viewdata.PollViewData;
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import com.mikepenz.iconics.utils.Utils;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
@ -74,20 +71,19 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private View sensitiveMediaShow;
protected TextView[] mediaLabels;
private ToggleButton contentWarningButton;
protected ImageView avatarInset;
private ImageView avatarInset;
public ImageView avatar;
public TextView timestampInfo;
public TextView content;
public TextView contentWarningDescription;
private TextView[] pollResults;
private RecyclerView pollOptions;
private TextView pollDescription;
private RadioGroup pollRadioGroup;
private RadioButton[] pollRadioOptions;
private CheckBox[] pollCheckboxOptions;
private Button pollButton;
private PollAdapter pollAdapter;
private boolean useAbsoluteTime;
private SimpleDateFormat shortSdf;
private SimpleDateFormat longSdf;
@ -135,31 +131,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
contentWarningButton = itemView.findViewById(R.id.status_content_warning_button);
avatarInset = itemView.findViewById(R.id.status_avatar_inset);
pollResults = new TextView[]{
itemView.findViewById(R.id.status_poll_option_result_0),
itemView.findViewById(R.id.status_poll_option_result_1),
itemView.findViewById(R.id.status_poll_option_result_2),
itemView.findViewById(R.id.status_poll_option_result_3)
};
pollOptions = itemView.findViewById(R.id.status_poll_options);
pollDescription = itemView.findViewById(R.id.status_poll_description);
pollRadioGroup = itemView.findViewById(R.id.status_poll_radio_group);
pollRadioOptions = new RadioButton[] {
pollRadioGroup.findViewById(R.id.status_poll_radio_button_0),
pollRadioGroup.findViewById(R.id.status_poll_radio_button_1),
pollRadioGroup.findViewById(R.id.status_poll_radio_button_2),
pollRadioGroup.findViewById(R.id.status_poll_radio_button_3)
};
pollCheckboxOptions = new CheckBox[] {
itemView.findViewById(R.id.status_poll_checkbox_0),
itemView.findViewById(R.id.status_poll_checkbox_1),
itemView.findViewById(R.id.status_poll_checkbox_2),
itemView.findViewById(R.id.status_poll_checkbox_3)
};
pollButton = itemView.findViewById(R.id.status_poll_button);
pollAdapter = new PollAdapter();
pollOptions.setAdapter(pollAdapter);
pollOptions.setLayoutManager(new LinearLayoutManager(pollOptions.getContext()));
((DefaultItemAnimator) pollOptions.getItemAnimator()).setSupportsChangeAnimations(false);
this.useAbsoluteTime = useAbsoluteTime;
shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
@ -795,15 +775,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private CharSequence getPollDescription(Context context,
@NonNull StatusViewData.Concrete status) {
Poll poll = status.getPoll();
PollViewData poll = status.getPoll();
if (poll == null) {
return "";
} else {
CharSequence[] args = new CharSequence[5];
List<PollOption> options = poll.getOptions();
List<PollOptionViewData> options = poll.getOptions();
for (int i = 0; i < args.length; i++) {
if (i < options.size()) {
int percent = options.get(i).getPercent(poll.getVotesCount());
int percent = PollViewDataKt.calculatePercent(options.get(i).getVotesCount(), poll.getVotesCount());
args[i] = HtmlUtils.fromHtml(context.getString(
R.string.poll_option_format,
percent,
@ -835,19 +815,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
}
protected void setupPoll(Poll poll, List<Emoji> emojis, StatusActionListener listener) {
protected void setupPoll(PollViewData poll, List<Emoji> emojis, StatusActionListener listener) {
if (poll == null) {
for (TextView pollResult : pollResults) {
pollResult.setVisibility(View.GONE);
}
pollDescription.setVisibility(View.GONE);
pollRadioGroup.setVisibility(View.GONE);
for (CheckBox checkBox : pollCheckboxOptions) {
checkBox.setVisibility(View.GONE);
}
pollOptions.setVisibility(View.GONE);
pollDescription.setVisibility(View.GONE);
pollButton.setVisibility(View.GONE);
} else {
long timestamp = System.currentTimeMillis();
@ -855,25 +831,49 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
Context context = pollDescription.getContext();
pollOptions.setVisibility(View.VISIBLE);
if (expired || poll.getVoted()) {
// no voting possible
setupPollResult(poll, emojis);
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), emojis, PollAdapter.RESULT);
pollButton.setVisibility(View.GONE);
} else {
// voting possible
setupPollVoting(poll, emojis, listener);
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE);
pollButton.setVisibility(View.VISIBLE);
pollButton.setOnClickListener(v -> {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
List<Integer> pollResult = pollAdapter.getSelected();
if(!pollResult.isEmpty()) {
listener.onVoteInPoll(position, pollResult);
}
}
});
}
pollDescription.setVisibility(View.VISIBLE);
pollDescription.setText(getPollInfoText(timestamp, poll, context));
}
}
private CharSequence getPollInfoText(long timestamp, Poll poll, Context context) {
private CharSequence getPollInfoText(long timestamp, PollViewData poll, Context context) {
String votes = numberFormat.format(poll.getVotesCount());
String votesText = context.getResources().getQuantityString(R.plurals.poll_info_votes, poll.getVotesCount(), votes);
CharSequence pollDurationInfo;
if (poll.getExpired()) {
pollDurationInfo = context.getString(R.string.poll_info_closed);
} else if (poll.getExpiresAt() == null) {
return votesText;
} else {
if (useAbsoluteTime) {
pollDurationInfo = context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.getExpiresAt()));
@ -886,129 +886,4 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
return pollDescription.getContext().getString(R.string.poll_info_format, votesText, pollDurationInfo);
}
private void setupPollResult(Poll poll, List<Emoji> emojis) {
List<PollOption> options = poll.getOptions();
for(int i = 0; i < Status.MAX_POLL_OPTIONS; i++) {
if(i < options.size()) {
int percent = options.get(i).getPercent(poll.getVotesCount());
String pollOptionText = pollResults[i].getContext().getString(R.string.poll_option_format, percent, options.get(i).getTitle());
pollResults[i].setText(CustomEmojiHelper.emojifyText(HtmlUtils.fromHtml(pollOptionText), emojis, pollResults[i]));
pollResults[i].setVisibility(View.VISIBLE);
int level = percent * 100;
pollResults[i].getBackground().setLevel(level);
} else {
pollResults[i].setVisibility(View.GONE);
}
}
pollRadioGroup.setVisibility(View.GONE);
for(CheckBox checkBox: pollCheckboxOptions) {
checkBox.setVisibility(View.GONE);
}
pollButton.setVisibility(View.GONE);
}
private void setupPollVoting(Poll poll, List<Emoji> emojis, StatusActionListener listener) {
List<PollOption> options = poll.getOptions();
pollButton.setVisibility(View.VISIBLE);
for(TextView pollResult: pollResults) {
pollResult.setVisibility(View.GONE);
}
if(poll.getMultiple()) {
pollRadioGroup.setVisibility(View.GONE);
for(int i = 0; i < Status.MAX_POLL_OPTIONS; i++) {
if(i < options.size()) {
CharSequence emojifiedPollOptionText = CustomEmojiHelper.emojifyString(options.get(i).getTitle(), emojis, pollCheckboxOptions[i]);
emojifiedPollOptionText = EmojiCompat.get().process(emojifiedPollOptionText);
pollCheckboxOptions[i].setText(emojifiedPollOptionText);
pollCheckboxOptions[i].setVisibility(View.VISIBLE);
pollCheckboxOptions[i].setChecked(false);
} else {
pollCheckboxOptions[i].setVisibility(View.GONE);
}
}
pollButton.setOnClickListener(v -> {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
List<Integer> pollResult = new ArrayList<>(options.size());
for (int i = 0; i < options.size(); i++) {
if (pollCheckboxOptions[i].isChecked()) {
pollResult.add(i);
}
}
if (pollResult.size() == 0) {
return;
}
listener.onVoteInPoll(position, pollResult);
}
});
} else {
for(CheckBox pollCheckbox: pollCheckboxOptions) {
pollCheckbox.setVisibility(View.GONE);
}
pollRadioGroup.setVisibility(View.VISIBLE);
pollRadioGroup.clearCheck();
for(int i = 0; i < Status.MAX_POLL_OPTIONS; i++) {
if(i < options.size()) {
CharSequence emojifiedPollOptionText = CustomEmojiHelper.emojifyString(options.get(i).getTitle(), emojis, pollRadioOptions[i]);
emojifiedPollOptionText = EmojiCompat.get().process(emojifiedPollOptionText);
pollRadioOptions[i].setText(emojifiedPollOptionText);
pollRadioOptions[i].setVisibility(View.VISIBLE);
} else {
pollRadioOptions[i].setVisibility(View.GONE);
}
}
pollButton.setOnClickListener(v -> {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
int selectedRadioButtonIndex;
switch (pollRadioGroup.getCheckedRadioButtonId()) {
case R.id.status_poll_radio_button_0:
selectedRadioButtonIndex = 0;
break;
case R.id.status_poll_radio_button_1:
selectedRadioButtonIndex = 1;
break;
case R.id.status_poll_radio_button_2:
selectedRadioButtonIndex = 2;
break;
case R.id.status_poll_radio_button_3:
selectedRadioButtonIndex = 3;
break;
default:
return;
}
listener.onVoteInPoll(position, Collections.singletonList(selectedRadioButtonIndex));
}
});
}
}
}