Skip to content

Commit 7742cd1

Browse files
authored
Merge pull request #86 from hmrc/editable_llstView_new_component_library
HMA-6127:new_component_EditableListView_to_library
2 parents 1d9f28c + f5123c3 commit 7742cd1

18 files changed

+721
-1
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright 2022 HM Revenue & Customs
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+
* http://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+
package uk.gov.hmrc.components.organism.editable
17+
18+
import android.content.Context
19+
import android.util.AttributeSet
20+
import android.view.LayoutInflater
21+
import android.view.View
22+
import android.view.ViewGroup
23+
import android.view.accessibility.AccessibilityEvent
24+
import android.widget.LinearLayout
25+
import androidx.annotation.DrawableRes
26+
import androidx.core.view.AccessibilityDelegateCompat
27+
import androidx.core.view.ViewCompat
28+
import uk.gov.hmrc.components.R
29+
import uk.gov.hmrc.components.databinding.ComponentEditableListViewBinding
30+
import kotlin.random.Random
31+
32+
open class EditableListView @JvmOverloads constructor(
33+
context: Context,
34+
attrs: AttributeSet? = null,
35+
) : LinearLayout(context, attrs) {
36+
37+
private val binding: ComponentEditableListViewBinding =
38+
ComponentEditableListViewBinding.inflate(LayoutInflater.from(context), this, true)
39+
40+
private var buttonText: Pair<String, String> = Pair("", "")
41+
private var buttonAccessibility: Pair<String, String> = Pair("", "")
42+
private var buttonIcon: Pair<Int, Int> = Pair(0, 0)
43+
private lateinit var editableListViewAdapter: EditableListViewAdapter
44+
private var editableItems = ArrayList<EditableItem>()
45+
private var editMode = false
46+
47+
init {
48+
attrs?.let {
49+
val typedArray =
50+
context.theme.obtainStyledAttributes(it, R.styleable.EditableListView, 0, 0)
51+
val title = typedArray.getString(R.styleable.EditableListView_title) ?: ""
52+
setButtonData(
53+
typedArray.getString(R.styleable.EditableListView_buttonStartEditingText) ?: "",
54+
typedArray.getString(R.styleable.EditableListView_buttonFinishEditingText) ?: ""
55+
)
56+
setButtonIconData(
57+
typedArray.getResourceId(R.styleable.EditableListView_buttonStartEditingIcon, NO_ICON),
58+
typedArray.getResourceId(R.styleable.EditableListView_buttonFinishEditingIcon, NO_ICON)
59+
)
60+
setButtonAccessibility(
61+
typedArray.getString(R.styleable.EditableListView_startEditingAccessibility) ?: "",
62+
typedArray.getString(R.styleable.EditableListView_endEditingAccessibility) ?: ""
63+
)
64+
setTitle(title)
65+
typedArray.recycle()
66+
}
67+
binding.title.id = Random.nextInt()
68+
binding.iconButton.setOnClickListener {
69+
if (::editableListViewAdapter.isInitialized) {
70+
editableListViewAdapter.isEditEnable = !editableListViewAdapter.isEditEnable
71+
}
72+
setEditModeUI(!editMode)
73+
}
74+
setFocusListener()
75+
}
76+
77+
private fun setEditModeUI(isInEditMode: Boolean) {
78+
this.editMode = isInEditMode
79+
binding.iconButton.apply {
80+
accessibilityTraversalBefore =
81+
if (isInEditMode) binding.title.id else nextFocusForwardId
82+
setIconResource(if (isInEditMode) buttonIcon.second else buttonIcon.first)
83+
announceForAccessibility(if (isInEditMode) buttonAccessibility.second else buttonAccessibility.first)
84+
text = if (isInEditMode) buttonText.second else buttonText.first
85+
}
86+
}
87+
88+
fun setButtonData(startEditingText: String, endEditingText: String) {
89+
buttonText = Pair(startEditingText, endEditingText)
90+
binding.iconButton.text = if (editMode) endEditingText else startEditingText
91+
}
92+
93+
fun setButtonIconData(
94+
@DrawableRes startEditingIcon: Int,
95+
@DrawableRes endEditingIcon: Int
96+
) {
97+
buttonIcon = Pair(startEditingIcon, endEditingIcon)
98+
binding.iconButton.setIconResource(if (editMode) buttonIcon.second else buttonIcon.first)
99+
}
100+
101+
fun setButtonAccessibility(
102+
startEditingAccessibility: String,
103+
endEditingAccessibility: String
104+
) {
105+
buttonAccessibility = Pair(startEditingAccessibility, endEditingAccessibility)
106+
}
107+
108+
fun setData(editableItem: ArrayList<EditableItem>) {
109+
this.editableItems = editableItem
110+
editableListViewAdapter = EditableListViewAdapter(editableItem)
111+
binding.listItems.apply {
112+
adapter = editableListViewAdapter
113+
}
114+
}
115+
116+
fun setTitle(title: CharSequence?) {
117+
binding.title.text = title
118+
}
119+
120+
interface EditableItem {
121+
var name: String
122+
var value: String
123+
var buttonText: String
124+
var valueContentDescription: String?
125+
val onClickListener: (Int) -> Unit
126+
}
127+
128+
private fun setFocusListener() {
129+
ViewCompat.setAccessibilityDelegate(
130+
binding.root,
131+
object : AccessibilityDelegateCompat() {
132+
override fun onRequestSendAccessibilityEvent(
133+
viewGroup: ViewGroup,
134+
child: View,
135+
event: AccessibilityEvent
136+
): Boolean {
137+
if (event.eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
138+
if (child.id == binding.title.id) {
139+
binding.iconButton.accessibilityTraversalBefore = nextFocusForwardId
140+
}
141+
}
142+
return super.onRequestSendAccessibilityEvent(viewGroup, child, event)
143+
}
144+
}
145+
)
146+
}
147+
148+
companion object {
149+
const val NO_ICON: Int = 0
150+
}
151+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2022 HM Revenue & Customs
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+
* http://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+
package uk.gov.hmrc.components.organism.editable
17+
18+
import android.view.LayoutInflater
19+
import android.view.ViewGroup
20+
import androidx.core.view.isVisible
21+
import androidx.recyclerview.widget.RecyclerView
22+
import uk.gov.hmrc.components.databinding.EditableListItemsBinding
23+
24+
class EditableListViewAdapter(
25+
private val values: List<EditableListView.EditableItem>,
26+
) : RecyclerView.Adapter<EditableListViewAdapter.ViewHolder>() {
27+
28+
var isEditEnable: Boolean = false
29+
@Suppress("NotifyDataSetChanged")
30+
set(value) {
31+
field = value
32+
notifyDataSetChanged()
33+
}
34+
35+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
36+
ViewHolder(
37+
EditableListItemsBinding.inflate(
38+
LayoutInflater.from(parent.context),
39+
parent,
40+
false
41+
)
42+
)
43+
44+
override fun getItemCount() = values.size
45+
46+
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
47+
holder.bind(values[position], position)
48+
}
49+
50+
inner class ViewHolder(private val binding: EditableListItemsBinding) :
51+
RecyclerView.ViewHolder(binding.root) {
52+
53+
fun bind(result: EditableListView.EditableItem, position: Int) = with(binding) {
54+
columnOne.text = result.name
55+
columnTwo.text = result.value
56+
iconButton.text = result.buttonText
57+
val positionLabel = position + 1
58+
divider.isVisible = positionLabel < itemCount
59+
itemView.contentDescription =
60+
if (isEditEnable) {
61+
"${result.name}: ${valueAccessibility(result)}, Tap to ${result.buttonText}," +
62+
" Item $positionLabel of $itemCount."
63+
} else {
64+
"${result.name}: ${valueAccessibility(result)}, Item $positionLabel of $itemCount."
65+
}
66+
iconButton.setOnClickListener {
67+
result.onClickListener(adapterPosition)
68+
}
69+
70+
if (isEditEnable) {
71+
motionLayout.transitionToEnd()
72+
} else {
73+
motionLayout.transitionToStart()
74+
}
75+
}
76+
77+
private fun valueAccessibility(editableItem: EditableListView.EditableItem): String =
78+
editableItem.valueContentDescription ?: editableItem.value
79+
}
80+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:app="http://schemas.android.com/apk/res-auto"
4+
xmlns:tools="http://schemas.android.com/tools"
5+
android:layout_width="match_parent"
6+
android:layout_height="match_parent"
7+
android:orientation="vertical">
8+
9+
<uk.gov.hmrc.components.atom.heading.Heading5
10+
android:id="@+id/title"
11+
android:layout_marginBottom="@dimen/hmrc_spacing_8"
12+
android:paddingHorizontal="@dimen/hmrc_spacing_16"
13+
android:layout_width="match_parent"
14+
android:layout_height="wrap_content"
15+
android:gravity="start"
16+
tools:text="Title" />
17+
18+
<androidx.recyclerview.widget.RecyclerView
19+
android:id="@+id/list_items"
20+
android:paddingHorizontal="@dimen/hmrc_spacing_16"
21+
android:layout_width="match_parent"
22+
android:layout_height="wrap_content"
23+
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
24+
tools:listitem="@layout/editable_list_items" />
25+
26+
<uk.gov.hmrc.components.atom.button.IconButton
27+
android:id="@+id/icon_button"
28+
android:layout_width="match_parent"
29+
android:layout_height="wrap_content" />
30+
31+
</LinearLayout>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:app="http://schemas.android.com/apk/res-auto"
4+
xmlns:tools="http://schemas.android.com/tools"
5+
android:id="@+id/motionLayout"
6+
android:layout_width="match_parent"
7+
android:layout_height="wrap_content"
8+
app:layoutDescription="@xml/scene_editing_mode"
9+
tools:showPaths="true">
10+
11+
<TextView
12+
android:id="@+id/column_one"
13+
style="@style/Text.Body"
14+
android:layout_marginHorizontal="@dimen/hmrc_spacing_16"
15+
android:importantForAccessibility="no"
16+
android:layout_width="wrap_content"
17+
android:layout_height="wrap_content"
18+
android:gravity="start" />
19+
20+
<TextView
21+
android:id="@+id/column_two"
22+
style="@style/Text.Body"
23+
android:layout_marginHorizontal="@dimen/hmrc_spacing_16"
24+
android:importantForAccessibility="no"
25+
android:layout_width="wrap_content"
26+
android:layout_height="wrap_content"
27+
android:gravity="center_vertical|end" />
28+
29+
<uk.gov.hmrc.components.atom.button.SecondaryButton
30+
android:id="@+id/icon_button"
31+
android:minWidth="0dp"
32+
android:minHeight="0dp"
33+
android:importantForAccessibility="no"
34+
android:paddingHorizontal="0dp"
35+
android:layout_width="wrap_content"
36+
android:layout_height="wrap_content" />
37+
38+
<View
39+
android:id="@+id/divider"
40+
android:layout_width="match_parent"
41+
android:layout_height="1dp"
42+
android:background="@drawable/components_divider"
43+
android:importantForAccessibility="no"
44+
android:visibility="gone" />
45+
46+
</androidx.constraintlayout.motion.widget.MotionLayout>

components/src/main/res/values/attrs.xml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@
2929
<attr name="materialTextButtonStyle" format="reference" />
3030
<!-- Material icon button style. -->
3131
<attr name="materialIconButtonStyle" format="reference" />
32+
<!-- Button Start Editing Text to Display-->
33+
<attr name="buttonStartEditingText" />
34+
<!-- Button Finished Editing Text to Display-->
35+
<attr name="buttonFinishEditingText" />
36+
<!-- Button Start Editing Icon to Display-->
37+
<attr name="buttonStartEditingIcon" />
38+
<!-- Button Finished Editing Icon to Display -->
39+
<attr name="buttonFinishEditingIcon" />
40+
<!-- Start Editing Accessibility message to read. -->
41+
<attr name="startEditingAccessibility" />
42+
<!-- End Editing Accessibility message to read. -->
43+
<attr name="endEditingAccessibility" />
3244

3345
<declare-styleable name="InsetTextView">
3446
<!-- Text to display. -->
@@ -254,4 +266,22 @@
254266
<attr name="color3" format="color" />
255267
</declare-styleable>
256268

269+
<declare-styleable name="EditableListView">
270+
<!-- Title to display. -->
271+
<attr name="title" />
272+
<!-- Button Start Editing Text to Display-->
273+
<attr name="buttonStartEditingText" />
274+
<!-- Button Finished Editing Text to Display-->
275+
<attr name="buttonFinishEditingText" />
276+
<!-- Button Start Editing Icon to Display-->
277+
<attr name="buttonStartEditingIcon" />
278+
<!-- Button Finished Editing Icon to Display -->
279+
<attr name="buttonFinishEditingIcon" />
280+
<!-- Start Editing Accessibility message to read. -->
281+
<attr name="startEditingAccessibility" />
282+
<!-- End Editing Accessibility message to read. -->
283+
<attr name="endEditingAccessibility" />
284+
285+
</declare-styleable>
286+
257287
</resources>

0 commit comments

Comments
 (0)