Skip to content

Commit d314bd2

Browse files
[Jetsurvey] Simplify single/multiple choice questions (#1026)
There was a lot of overlap in these questions, even though the designs are very similar. I consolidated all versions and updated it according to the designs. this CL follows up on #1024 so please merge that before this.
2 parents 6757721 + f0d253c commit d314bd2

File tree

9 files changed

+275
-612
lines changed

9 files changed

+275
-612
lines changed

Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/Survey.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.example.compose.jetsurvey.survey
1818

1919
import android.net.Uri
20+
import androidx.annotation.DrawableRes
2021
import androidx.annotation.StringRes
2122

2223
data class SurveyResult(
@@ -50,11 +51,11 @@ sealed class SurveyActionResult {
5051
data class Contact(val contact: String) : SurveyActionResult()
5152
}
5253

54+
data class AnswerOption(@StringRes val textRes: Int, @DrawableRes val iconRes: Int? = null)
55+
5356
sealed class PossibleAnswer {
54-
data class SingleChoice(val optionsStringRes: List<Int>) : PossibleAnswer()
55-
data class SingleChoiceIcon(val optionsStringIconRes: List<Pair<Int, Int>>) : PossibleAnswer()
56-
data class MultipleChoice(val optionsStringRes: List<Int>) : PossibleAnswer()
57-
data class MultipleChoiceIcon(val optionsStringIconRes: List<Pair<Int, Int>>) : PossibleAnswer()
57+
data class SingleChoice(val options: List<AnswerOption>) : PossibleAnswer()
58+
data class MultipleChoice(val options: List<AnswerOption>) : PossibleAnswer()
5859
data class Action(
5960
@StringRes val label: Int,
6061
val actionType: SurveyActionType

Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyQuestions.kt

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ import androidx.compose.ui.tooling.preview.Preview
3838
import androidx.compose.ui.unit.dp
3939
import com.example.compose.jetsurvey.R
4040
import com.example.compose.jetsurvey.survey.question.ActionQuestion
41-
import com.example.compose.jetsurvey.survey.question.MultipleChoiceIconQuestion
4241
import com.example.compose.jetsurvey.survey.question.MultipleChoiceQuestion
43-
import com.example.compose.jetsurvey.survey.question.SingleChoiceIconQuestion
4442
import com.example.compose.jetsurvey.survey.question.SingleChoiceQuestion
4543
import com.example.compose.jetsurvey.survey.question.SliderQuestion
4644
import com.example.compose.jetsurvey.theme.JetsurveyTheme
@@ -147,32 +145,12 @@ private fun QuestionContent(
147145
}
148146
when (question.answer) {
149147
is PossibleAnswer.SingleChoice -> SingleChoiceQuestion(
150-
possibleAnswer = question.answer,
151-
answer = answer as Answer.SingleChoice?,
152-
onAnswerSelected = { answer -> onAnswer(Answer.SingleChoice(answer)) },
153-
modifier = Modifier.fillParentMaxWidth()
154-
)
155-
is PossibleAnswer.SingleChoiceIcon -> SingleChoiceIconQuestion(
156-
possibleAnswer = question.answer,
148+
options = question.answer.options,
157149
answer = answer as Answer.SingleChoice?,
158150
onAnswerSelected = { answer -> onAnswer(Answer.SingleChoice(answer)) },
159151
modifier = Modifier.fillMaxWidth()
160152
)
161153
is PossibleAnswer.MultipleChoice -> MultipleChoiceQuestion(
162-
possibleAnswer = question.answer,
163-
answer = answer as Answer.MultipleChoice?,
164-
onAnswerSelected = { newAnswer, selected ->
165-
// create the answer if it doesn't exist or
166-
// update it based on the user's selection
167-
if (answer == null) {
168-
onAnswer(Answer.MultipleChoice(setOf(newAnswer)))
169-
} else {
170-
onAnswer(answer.withAnswerSelected(newAnswer, selected))
171-
}
172-
},
173-
modifier = Modifier.fillParentMaxWidth()
174-
)
175-
is PossibleAnswer.MultipleChoiceIcon -> MultipleChoiceIconQuestion(
176154
possibleAnswer = question.answer,
177155
answer = answer as Answer.MultipleChoice?,
178156
onAnswerSelected = { newAnswer, selected ->
@@ -232,11 +210,11 @@ fun QuestionPreview() {
232210
id = 2,
233211
questionText = R.string.pick_superhero,
234212
answer = PossibleAnswer.SingleChoice(
235-
optionsStringRes = listOf(
236-
R.string.spark,
237-
R.string.lenz,
238-
R.string.bugchaos,
239-
R.string.frag
213+
options = listOf(
214+
AnswerOption(R.string.spark),
215+
AnswerOption(R.string.lenz),
216+
AnswerOption(R.string.bugchaos),
217+
AnswerOption(R.string.frag)
240218
)
241219
),
242220
description = R.string.select_one

Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyRepository.kt

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ package com.example.compose.jetsurvey.survey
1919
import android.os.Build
2020
import com.example.compose.jetsurvey.R
2121
import com.example.compose.jetsurvey.survey.PossibleAnswer.Action
22-
import com.example.compose.jetsurvey.survey.PossibleAnswer.MultipleChoice
23-
import com.example.compose.jetsurvey.survey.PossibleAnswer.SingleChoice
2422
import com.example.compose.jetsurvey.survey.SurveyActionType.PICK_DATE
2523
import com.example.compose.jetsurvey.survey.SurveyActionType.TAKE_PHOTO
2624

@@ -29,40 +27,40 @@ private val jetpackQuestions = mutableListOf(
2927
Question(
3028
id = 1,
3129
questionText = R.string.in_my_free_time,
32-
answer = MultipleChoice(
33-
optionsStringRes = listOf(
34-
R.string.read,
35-
R.string.work_out,
36-
R.string.draw,
37-
R.string.play_games,
38-
R.string.dance,
39-
R.string.watch_movies
30+
answer = PossibleAnswer.MultipleChoice(
31+
options = listOf(
32+
AnswerOption(R.string.read),
33+
AnswerOption(R.string.work_out),
34+
AnswerOption(R.string.draw),
35+
AnswerOption(R.string.play_games),
36+
AnswerOption(R.string.dance),
37+
AnswerOption(R.string.watch_movies)
4038
)
4139
),
4240
description = R.string.select_all
4341
),
4442
Question(
4543
id = 2,
4644
questionText = R.string.pick_superhero,
47-
answer = PossibleAnswer.SingleChoiceIcon(
48-
optionsStringIconRes = listOf(
49-
Pair(R.drawable.spark, R.string.spark),
50-
Pair(R.drawable.lenz, R.string.lenz),
51-
Pair(R.drawable.bug_of_chaos, R.string.bugchaos),
52-
Pair(R.drawable.frag, R.string.frag)
45+
answer = PossibleAnswer.SingleChoice(
46+
options = listOf(
47+
AnswerOption(R.string.spark, R.drawable.spark),
48+
AnswerOption(R.string.lenz, R.drawable.lenz),
49+
AnswerOption(R.string.bugchaos, R.drawable.bug_of_chaos),
50+
AnswerOption(R.string.frag, R.drawable.frag)
5351
)
5452
),
5553
description = R.string.select_one
5654
),
5755
Question(
5856
id = 7,
5957
questionText = R.string.favourite_movie,
60-
answer = SingleChoice(
58+
answer = PossibleAnswer.SingleChoice(
6159
listOf(
62-
R.string.star_trek,
63-
R.string.social_network,
64-
R.string.back_to_future,
65-
R.string.outbreak
60+
AnswerOption(R.string.star_trek),
61+
AnswerOption(R.string.social_network),
62+
AnswerOption(R.string.back_to_future),
63+
AnswerOption(R.string.outbreak)
6664
)
6765
),
6866
description = R.string.select_one
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/*
2+
* Copyright 2022 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.compose.jetsurvey.survey.question
18+
19+
import android.content.res.Configuration
20+
import androidx.compose.foundation.BorderStroke
21+
import androidx.compose.foundation.Image
22+
import androidx.compose.foundation.clickable
23+
import androidx.compose.foundation.layout.Box
24+
import androidx.compose.foundation.layout.Column
25+
import androidx.compose.foundation.layout.Row
26+
import androidx.compose.foundation.layout.Spacer
27+
import androidx.compose.foundation.layout.fillMaxWidth
28+
import androidx.compose.foundation.layout.padding
29+
import androidx.compose.foundation.layout.size
30+
import androidx.compose.foundation.layout.width
31+
import androidx.compose.foundation.selection.selectable
32+
import androidx.compose.foundation.selection.selectableGroup
33+
import androidx.compose.material3.Checkbox
34+
import androidx.compose.material3.MaterialTheme
35+
import androidx.compose.material3.RadioButton
36+
import androidx.compose.material3.Surface
37+
import androidx.compose.material3.Text
38+
import androidx.compose.runtime.Composable
39+
import androidx.compose.runtime.getValue
40+
import androidx.compose.runtime.mutableStateOf
41+
import androidx.compose.runtime.remember
42+
import androidx.compose.runtime.setValue
43+
import androidx.compose.ui.Alignment
44+
import androidx.compose.ui.Modifier
45+
import androidx.compose.ui.draw.clip
46+
import androidx.compose.ui.graphics.painter.Painter
47+
import androidx.compose.ui.res.painterResource
48+
import androidx.compose.ui.res.stringResource
49+
import androidx.compose.ui.tooling.preview.Preview
50+
import androidx.compose.ui.tooling.preview.PreviewParameter
51+
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
52+
import androidx.compose.ui.unit.dp
53+
import com.example.compose.jetsurvey.R
54+
import com.example.compose.jetsurvey.survey.Answer
55+
import com.example.compose.jetsurvey.survey.AnswerOption
56+
import com.example.compose.jetsurvey.survey.PossibleAnswer
57+
import com.example.compose.jetsurvey.theme.JetsurveyTheme
58+
59+
/**
60+
* Shows a list of possible answers with image and text and a radio button.
61+
* @param options a list of all possible answers with their icon and text value
62+
* @param answer the chosen answer (identified by text resource value), can be null if no answer is
63+
* chosen
64+
* @param onAnswerSelected callback that is called when a possible answer is clicked
65+
* @param modifier Modifier to be applied to the [SingleChoiceQuestion]
66+
*/
67+
@Composable
68+
fun SingleChoiceQuestion(
69+
options: List<AnswerOption>,
70+
answer: Answer.SingleChoice?,
71+
onAnswerSelected: (Int) -> Unit,
72+
modifier: Modifier = Modifier
73+
) {
74+
Column(modifier.selectableGroup()) {
75+
options.forEach { option ->
76+
Answer(
77+
text = stringResource(option.textRes),
78+
painter = option.iconRes?.let { painterResource(it) },
79+
selected = option.textRes == answer?.answer,
80+
onOptionSelected = { onAnswerSelected(option.textRes) },
81+
isSingleChoice = true,
82+
modifier = Modifier.padding(vertical = 8.dp)
83+
)
84+
}
85+
}
86+
}
87+
88+
@Composable
89+
fun MultipleChoiceQuestion(
90+
possibleAnswer: PossibleAnswer.MultipleChoice,
91+
answer: Answer.MultipleChoice?,
92+
onAnswerSelected: (Int, Boolean) -> Unit,
93+
modifier: Modifier = Modifier
94+
) {
95+
Column(modifier) {
96+
possibleAnswer.options.forEach { option ->
97+
val selected = answer?.answersStringRes?.contains(option.textRes) ?: false
98+
Answer(
99+
text = stringResource(option.textRes),
100+
painter = option.iconRes?.let { painterResource(it) },
101+
selected = selected,
102+
onOptionSelected = { onAnswerSelected(option.textRes, !selected) },
103+
isSingleChoice = false,
104+
modifier = Modifier.padding(vertical = 8.dp)
105+
)
106+
}
107+
}
108+
}
109+
110+
@Composable
111+
private fun Answer(
112+
text: String,
113+
painter: Painter?,
114+
selected: Boolean,
115+
onOptionSelected: () -> Unit,
116+
isSingleChoice: Boolean,
117+
modifier: Modifier = Modifier
118+
) {
119+
Surface(
120+
shape = MaterialTheme.shapes.small,
121+
color = if (selected) {
122+
MaterialTheme.colorScheme.primaryContainer
123+
} else {
124+
MaterialTheme.colorScheme.surface
125+
},
126+
border = BorderStroke(
127+
width = 1.dp,
128+
color = if (selected) {
129+
MaterialTheme.colorScheme.primary
130+
} else {
131+
MaterialTheme.colorScheme.outline
132+
}
133+
),
134+
modifier = modifier
135+
) {
136+
Row(
137+
modifier = Modifier
138+
.fillMaxWidth()
139+
.then(
140+
if (isSingleChoice) {
141+
Modifier.selectable(selected, onClick = onOptionSelected)
142+
} else {
143+
Modifier.clickable(onClick = onOptionSelected)
144+
}
145+
)
146+
.padding(16.dp),
147+
verticalAlignment = Alignment.CenterVertically
148+
) {
149+
if (painter != null) {
150+
Image(
151+
painter = painter,
152+
contentDescription = null,
153+
modifier = Modifier
154+
.size(56.dp)
155+
.clip(MaterialTheme.shapes.extraSmall)
156+
)
157+
Spacer(Modifier.width(8.dp))
158+
}
159+
Text(text, Modifier.weight(1f), style = MaterialTheme.typography.bodyLarge)
160+
Box(Modifier.padding(8.dp)) {
161+
if (isSingleChoice) {
162+
RadioButton(selected, onClick = null)
163+
} else {
164+
Checkbox(selected, onCheckedChange = null)
165+
}
166+
}
167+
}
168+
}
169+
}
170+
171+
@Preview(name = "Light", uiMode = Configuration.UI_MODE_NIGHT_NO)
172+
@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES)
173+
@Composable
174+
private fun AnswerPreview(
175+
@PreviewParameter(PreviewDataProvider::class, limit = 4) previewData: PreviewData
176+
) {
177+
JetsurveyTheme {
178+
Answer(
179+
text = "Preview",
180+
painter = painterResource(id = R.drawable.frag),
181+
selected = previewData.selected,
182+
isSingleChoice = previewData.isSingleChoice,
183+
onOptionSelected = { }
184+
)
185+
}
186+
}
187+
188+
private data class PreviewData(
189+
val selected: Boolean,
190+
val isSingleChoice: Boolean
191+
)
192+
193+
private class PreviewDataProvider : PreviewParameterProvider<PreviewData> {
194+
override val values = sequenceOf(
195+
PreviewData(selected = false, isSingleChoice = true),
196+
PreviewData(selected = true, isSingleChoice = true),
197+
PreviewData(selected = false, isSingleChoice = false),
198+
PreviewData(selected = true, isSingleChoice = false),
199+
)
200+
}
201+
202+
@Preview(name = "Light", uiMode = Configuration.UI_MODE_NIGHT_NO)
203+
@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES)
204+
@Composable
205+
private fun SingleChoiceIconQuestionPreview() {
206+
var selectedAnswer: Answer.SingleChoice? by remember {
207+
mutableStateOf(Answer.SingleChoice(R.string.bugchaos))
208+
}
209+
210+
JetsurveyTheme {
211+
SingleChoiceQuestion(
212+
options = listOf(
213+
AnswerOption(R.string.spark, R.drawable.spark),
214+
AnswerOption(R.string.lenz, R.drawable.lenz),
215+
AnswerOption(R.string.bugchaos, R.drawable.bug_of_chaos),
216+
AnswerOption(R.string.frag, R.drawable.frag)
217+
),
218+
answer = selectedAnswer,
219+
onAnswerSelected = { textRes -> selectedAnswer = Answer.SingleChoice(textRes) }
220+
)
221+
}
222+
}
223+
224+
@Preview(name = "Light", uiMode = Configuration.UI_MODE_NIGHT_NO)
225+
@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES)
226+
@Composable
227+
private fun SingleChoiceQuestionPreview() {
228+
var selectedAnswer: Answer.SingleChoice? by remember {
229+
mutableStateOf(Answer.SingleChoice(R.string.bugchaos))
230+
}
231+
232+
JetsurveyTheme {
233+
SingleChoiceQuestion(
234+
options = listOf(
235+
AnswerOption(R.string.star_trek),
236+
AnswerOption(R.string.social_network),
237+
AnswerOption(R.string.back_to_future),
238+
AnswerOption(R.string.outbreak)
239+
),
240+
answer = selectedAnswer,
241+
onAnswerSelected = { textRes -> selectedAnswer = Answer.SingleChoice(textRes) }
242+
)
243+
}
244+
}

0 commit comments

Comments
 (0)