I'm working on an information widget for the LG V20/V10, to run in the second screen (very easy to add one: just set your category to 36864/0x9000). Currently, I have a battery view and a TextClock in the RemoteViews layout:
<!--info_widget.xml-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/main"
android:layout_width="1040px"
android:layout_height="160px">
<include
android:id="#+id/battery"
layout="#layout/battery_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_toLeftOf="#+id/clock"
android:layout_gravity="right" />
<include
android:id="#+id/clock"
layout="#layout/clock_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_gravity="right" />
</RelativeLayout>
<!--battery_view.xml-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="160px"
android:layout_gravity="center"
android:foregroundGravity="center"
android:orientation="vertical"
android:padding="2dp">
<ImageView
android:id="#+id/battery_view"
android:layout_width="70px"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="1"
android:scaleType="centerCrop"
android:tint="#fff"
app:srcCompat="#drawable/ic_battery_alert_black_24dp" />
<TextView
android:id="#+id/battery_percent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:textColor="#fff"
android:textSize="40px" />
</LinearLayout>
<!--clock_view.xml-->
<TextClock xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:padding="2dp"
android:textSize="64px"
android:id="#+id/clock_view"/>
In my code, I have these functions, which get called from onUpdate() for every widget ID (in Kotlin):
private fun updateBattery(views: RemoteViews) {
val level = mBatteryManager?.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
val charging = mBatteryManager?.isCharging
mBatteryState.updateState(level as Int, charging as Boolean)
views.setImageViewResource(R.id.battery_view, mBatteryState.imageResource)
var color = mPrefs?.getInt("battery_color", Color.WHITE)
var showPercent = mPrefs?.getBoolean("show_percent", true)
if (color == null) color = Color.WHITE
if (showPercent == null) showPercent = true
views.setInt(R.id.battery_view, "setColorFilter", color)
views.setTextColor(R.id.battery_percent, color)
if (showPercent) {
views.setViewVisibility(R.id.battery_percent, View.VISIBLE)
views.setTextViewText(R.id.battery_percent, mBatteryState.percent.toString() + "%")
} else {
views.setViewVisibility(R.id.battery_percent, View.GONE)
}
}
private fun updateClock(views: RemoteViews) {
var hour_24 = mPrefs?.getBoolean("24_hour", false)
var amPm = mPrefs?.getBoolean("am_pm", true)
var showDate = mPrefs?.getBoolean("show_date", false)
var color = mPrefs?.getInt("clock_color", Color.WHITE)
if (hour_24 == null) hour_24 = false
if (amPm == null) amPm = true
if (showDate == null) showDate = false
if (color == null) color = Color.WHITE
val format: CharSequence = if (showDate) "EE, d " else {""} + if (hour_24) "k" else {"h"} + ":mm" + if (amPm) " a" else {""}
views.setCharSequence(R.id.clock_view, "setFormat12Hour", format)
views.setCharSequence(R.id.clock_view, "setFormat24Hour", format)
views.setTextColor(R.id.clock_view, color)
}
I have a service that gets started, to listen for preference changes and receive broadcast intents, which calls the widget's onUpdate() whenever something happens.
Everything works fine in the battery function: I can use the configuration activity to change the tint color and whether or not the percentage is shown, and the changes take place immediately. For whatever, reason, though, the functions in the clock function don't seem to do a thing.
When the view is inflated, and I have a color already set to tint it, it's still white. If I change the format, nothing happens. It's almost as if the TextClock functions aren't actually being called.
I even checked the AOSP source, and it looks like these methods should be allowed from a RemoteViews object: setFormat12Hour(CharSequence charSequence)
What'd I do wrong?
Well I figured it out, and I honestly have no idea why it's like this. I noticed that when I added another widget to the info screen, it, too, was not responding to changes. So I looked at the differences between the two broken views and the one working view (the battery).
I found that the battery view is enclosed in a LinearLayout, and wouldn't you know it, enclosing the TextClock and the new widget (and ImageView) in dummy LinearLayouts fixed the issue:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<TextClock
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:padding="2dp"
android:textSize="64px"
android:id="#+id/textClock" />
</LinearLayout>
Related
My res xml has a linearlayout and a button
<LinearLayout
android:id="#+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginStart="32dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:gravity="center_horizontal"
android:orientation="horizontal"
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="TextView" />
</LinearLayout>
<Button
android:id="#+id/btn_add_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="84dp"
android:text="Button" />
click the button
some char array added to linearLayout one by one
val chars = "Hello".toCharArray()
btn_add_text.setOnClickListener {
linearLayout.removeAllViews()
chars.forEachIndexed { index, char ->
val tv = Textview(this)
tv.textSize = 36f
tv.text = char
tv.id = index
linearLayout.addView(tv)
linearLayout.invalidate()
}
After forEachIndexed loop has finished linearLayout refreshed and can see [H][e][l][l][o] five textviews.
But I want to make linearLayout refresh after each linearLayout.addView(tv).
As far as I know if you want a view to redraw you call invalidate and if you want to update the viewbounds you need to call requestLayout as well.
If you want to see step by step you can try this:
val handler = Handler()
btn_add_text.setOnClickListener {
linearLayout.removeAllViews()
chars.forEachIndexed { index, char ->
val tv = TextView(context!!)
tv.textSize = 24f
tv.text = char.toString()
tv.id = index
handler.postDelayed(Runnable {
linearLayout.addView(tv)
},500 * index.toLong())
}
}
I think linearlayout is refreshing so fast, you are not able to see intermediate refreshes, what you can do is, use a worker thread and make it sleep for 500 ms between each iteration, and post data to main thread via handler, your each change of charachter will be visible.
Background
I need to make a dialpad-like View, like on the Phone app.
I'm using a GridLayout of Views. Each cell is of the same size, and contains just a simple TextView that should change its font size if needed.
The problem
I've succeeded, but for some reason it doesn't work well according to the space that it is given.
If it has a lot of space, it works fine:
However, when it gets smaller (example: small screens, landscape, split-window...), only the top buttons of the grid become visible, and they didn't even change their font size, as if they all want to be of the biggest size they can:
What I've tried
I tried to modify various attributes of the views, but none helped.
I know though, that the dial-pad of the Phone app doesn't really change its font size. Up to some size, it gets shown normally, and if it's too small, it changes to a different layout. This is especially important for landscape and split-window modes.
Here's the code I've made (I change the value of "layout_constraintHeight_percent" to check the various sizes for the top area) :
gradle
...
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.core:core-ktx:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
...
QueryKeyboard.kt
class QueryKeyboard #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : GridLayout(context, attrs, defStyle) {
init {
orientation = HORIZONTAL
clipChildren = false
clipToPadding = false
columnCount = 3
rowCount = 4
//workaround for a weird issue of seeing just 3 huge buttons, instead of all
val runnable = Runnable {
for (i in 1..9)
addView(generateGridTextButton(i.toString()))
addView(generateGridTextButton("*"))
addView(generateGridTextButton("0"))
addView(generateGridTextButton("+"))
}
if (isInEditMode)
runnable.run()
else
this.doOnPreDraw { runnable.run() }
}
private fun generateGridTextButton(textToShowAndAddUponClick: CharSequence): TextView {
val tv = LayoutInflater.from(context).inflate(R.layout.grid_text_button, this, false) as TextView
tv.text = textToShowAndAddUponClick
return tv
}
}
grid_text_button.xml
<androidx.appcompat.widget.AppCompatTextView
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="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:breakStrategy="balanced"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="false"
android:gravity="center"
android:textColor="#000"
android:textSize="36dp"
app:autoSizeMaxTextSize="36dp"
app:autoSizeMinTextSize="12dp"
app:layout_columnWeight="1"
app:layout_gravity="fill"
app:layout_rowWeight="1"
tools:layout_gravity="center"
tools:targetApi="m"
tools:text="1" />
Usage in activity_main.xml :
<androidx.constraintlayout.widget.ConstraintLayout
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="match_parent" android:orientation="vertical" tools:context=".MainActivity">
<TextView
android:id="#+id/textView" android:layout_width="match_parent" android:layout_height="0px"
android:background="#33ff0000" android:gravity="center" android:text="some content"
app:layout_constraintBottom_toTopOf="#id/queryKeyboard" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.6" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.sample.myapplication.QueryKeyboard
android:id="#+id/queryKeyboard" android:layout_width="match_parent" android:layout_height="0px"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="#id/textView" />
</androidx.constraintlayout.widget.ConstraintLayout>
EDIT: I tried to wrap the TextView with FrameLayout, to show the size of each cell:
grid_text_button.xml
<FrameLayout
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="wrap_content" android:layout_height="wrap_content"
app:layout_columnWeight="1" app:layout_gravity="fill" app:layout_rowWeight="1">
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless" android:breakStrategy="balanced"
android:clickable="true" android:focusable="true" android:focusableInTouchMode="false" android:gravity="center"
android:textColor="#000" android:textSize="36sp" app:autoSizeMaxTextSize="36sp" app:autoSizeMinTextSize="12sp"
tools:layout_gravity="center" tools:targetApi="m" tools:text="1" />
</FrameLayout>
QueryKeyboard.kt
class QueryKeyboard #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : GridLayout(context, attrs, defStyle) {
private var cellBackgroundColor = 0xffff0000.toInt()
init {
orientation = HORIZONTAL
clipChildren = false
clipToPadding = false
columnCount = 3
rowCount = 4
//workaround for a weird issue of seeing just 3 huge buttons, instead of all
val runnable = Runnable {
for (i in 1..9) {
addView(generateGridTextButton(i.toString()))
}
addView(generateGridTextButton("*"))
addView(generateGridTextButton("0"))
addView(generateGridTextButton("+"))
}
if (isInEditMode)
runnable.run()
else
this.doOnPreDraw { runnable.run() }
}
private fun switchColor() {
cellBackgroundColor = if (cellBackgroundColor == 0xffff0000.toInt()) 0xff00ff00.toInt() else 0xffff0000.toInt()
}
private fun generateGridTextButton(textToShowAndAddUponClick: CharSequence): View {
val view = LayoutInflater.from(context).inflate(R.layout.grid_text_button, this, false)
switchColor()
view.setBackgroundColor(cellBackgroundColor)
view.textView.text = textToShowAndAddUponClick
return view
}
}
And here are the 2 cases, of when it works fine, and when it doesn't:
Same as before. Getting 3 cells, text not centered, and not auto-resizing its font.
The questions
Why don't the cells adjust their sizes, including the font size of each of them? How come I see just 3 cells when it's too small? How can I fix it?
Is there a better alternative? I guess I could use LinearLayout of multiple LinearLayout instances, but that's just weird for this case... After all, how often do you use GridLayout... :)
How can I detect when it's just too small, so that I switch to another layout, like on the Phone app, including all the various cases they used (even split-window)? Is it possible they just used qualifier for the layouts? If so, which is recommended for this case ?
Why don't the cells adjust their sizes, including the font size of each of them? How can I fix it?
A cell can dynamically adjust its size based on two main constraints: the weight of its parent and the value of its textsize or childview. Having a fixed textsize on cells can lead to inconsistency in design. To fix this issue you can either set a layout weight on parentview with cells width and height matching parents or create different dimensions of textsize for targeted devices.
Is there a better alternative? I guess I could use LinearLayout of multiple LinearLayout instances, but that's just weird for this case... After all, how often do you use GridLayout... :)
There is a better way and that is what you're currently implementing The GridLayout. Using indented LinearLayouts would limit you from lots of benefits and would make you write more code, take for example cases where you need to switch or animate cells, access the nth column of nth row, dynamically change cell span, etc. All these can be done via Gridlayout with a few lines of code. It is more powerful than you think.
How can I detect when it's just too small, so that I switch to another layout, like on the Phone app, including all the various cases they used (even split-window)? Is it possible they just used qualifier for the layouts? If so, which is recommended for this case ?
There's nothing for you to detect, just follow the guidelines and Android would do the detecting.
Here are a couple of ways you can manage your scenario based on Android Design guidelines
First:
Create a layout landscape and portrait mode for your activity (layout/activity.xml and layout-land/activity.xml)
layout/activity.xml
<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="match_parent"
android:orientation="vertical"
android:weightSum="2"
tools:context=".MainActivity">
<TextView
android:id="#+id/textView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="#33ff0000"
android:gravity="center" android:text="some content"
android:layout_weight="1" />
<com.sample.myapplication.QueryKeyboard
android:id="#+id/queryKeyboard"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
layout-land/activity.xml
<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="match_parent"
android:orientation="horizontal"
android:weightSum="2"
tools:context=".MainActivity">
<TextView
android:id="#+id/textView"
android:layout_width="0dp"
android:layout_height="match_parent"
android:background="#33ff0000"
android:gravity="center" android:text="some content"
android:layout_weight="1" />
<com.sample.myapplication.QueryKeyboard
android:id="#+id/queryKeyboard"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
Furthermore You need to handle textsize for various screen size.
This textsize calculation can be handled automatically by adding Intuit android library to your dependency list in gradle
dependencies {
implementation 'com.intuit.ssp:ssp-android:1.0.6'
}
Then in your grid button textsize call
android:textSize="#dimens/_30ssp"
OK I've changed the layout files a bit, to avoid the 3-cells issue. It still occurs, but on much smaller sizes. Sadly the font sizes issue remains the same, and even the very small this time.
If anyone finds out why this occurs, please let me know. For now I consider this as a bug, so I've reported here.
grid_text_button.xml
<FrameLayout
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="wrap_content"
android:layout_height="wrap_content" android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true" android:focusable="true" android:focusableInTouchMode="false" app:layout_columnWeight="1"
app:layout_gravity="fill" app:layout_rowWeight="1" tools:layout_gravity="center">
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:breakStrategy="balanced" android:gravity="center"
android:textColor="#000" app:autoSizeMaxTextSize="36sp" app:autoSizeMinTextSize="12sp" tools:targetApi="m"
tools:text="1" />
</FrameLayout>
QueryKeyboard.kt
class QueryKeyboard #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : GridLayout(context, attrs, defStyle) {
// private var cellBackgroundColor = 0xffff0000.toInt()
init {
orientation = HORIZONTAL
clipChildren = false
clipToPadding = false
columnCount = 3
rowCount = 4
for (i in 1..9)
addView(generateGridTextButton(i.toString()))
addView(generateGridTextButton("*"))
addView(generateGridTextButton("0"))
addView(generateGridTextButton("+"))
}
// private fun switchColor() {
// cellBackgroundColor = if (cellBackgroundColor == 0xffff0000.toInt()) 0xff00ff00.toInt() else 0xffff0000.toInt()
// }
private fun generateGridTextButton(textToShowAndAddUponClick: CharSequence): View {
val view = LayoutInflater.from(context).inflate(R.layout.grid_text_button, this, false)
// switchColor()
// view.setBackgroundColor(cellBackgroundColor)
view.textView.text = textToShowAndAddUponClick
return view
}
}
An alternative to the GridLayout implementation, that doesn't have this issue, but it's still weird that I would use it, is as I wrote, a LinearLayout of LinearLayouts :
class QueryKeyboard2 #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : LinearLayout(context, attrs, defStyleAttr) {
//private var cellBackgroundColor = 0xffff0000.toInt()
init {
orientation = VERTICAL
clipChildren = false
clipToPadding = false
val columnCount = 3
val rowCount = 4
val cellsList = ArrayList<View>()
for (i in 1..9)
cellsList.add(generateGridTextButton(i.toString()))
cellsList.add(generateGridTextButton("*"))
cellsList.add(generateGridTextButton("0"))
cellsList.add(generateGridTextButton("+"))
for (i in 0 until rowCount) {
val rowLayout = generateRowLayout(context)
for (j in 0 until columnCount) {
val cellView = cellsList[i * columnCount + j]
val cellLayoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT)
cellLayoutParams.weight = 1f
rowLayout.addView(cellView, cellLayoutParams)
}
// switchColor()
// rowLayout.setBackgroundColor(cellBackgroundColor)
addView(rowLayout)
}
}
private fun generateRowLayout(context: Context): LinearLayout {
val result = LinearLayout(context)
result.layoutParams = LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0)
(result.layoutParams as LayoutParams).weight = 1f
result.orientation = HORIZONTAL
return result
}
//private fun switchColor() {
// cellBackgroundColor = if (cellBackgroundColor == 0xffff0000.toInt()) 0xff00ff00.toInt() else 0xffff0000.toInt()
//}
private fun generateGridTextButton(textToShowAndAddUponClick: CharSequence): View {
val view = LayoutInflater.from(context).inflate(R.layout.grid_text_button, this, false)
//switchColor()
//view.setBackgroundColor(cellBackgroundColor)
view.textView.text = textToShowAndAddUponClick
return view
}
}
As for trying to make it shown only when there is enough space (height) , I've set the layout to use it on "res/layout-h400dp" (can be changed according to the needs), and a different one, where the dialpad is on the right, for the normal "res/layout".
I have a ViewHolder that is meant to appear differently depending on whether it is on the left or right side of a two-column RecyclerView with a GridLayoutManager. Note the connector lines on either side of the view:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_marginTop="12px"
android:layout_marginBottom="12px"
android:layout_gravity="center"
android:gravity="center_vertical"
android:layout_height="wrap_content"
android:id="#+id/citation_select_holder">
<ImageView
android:src="#drawable/connector_line"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="#+id/citation_select_connector_right"/>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="348px"
android:layout_height="104px"
android:layout_weight="1"
android:background="#drawable/button_background_white"
android:id="#+id/citation_select_citation_holder">
<LinearLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_marginLeft="28px"
>
<TextView
tools:text="123456"
android:textAppearance="#style/citation_select_item_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/citation_select_citation_number_text"/>
<TextView
tools:text="Pay by: Nov 18th, 2019"
android:textAppearance="#style/citation_select_item_due_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/citation_select_due_date_text"/>
<TextView
tools:text="a category label"
android:textAppearance="#style/citation_select_item_category"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/citation_select_category_text"/>
</LinearLayout>
<TextView
tools:text="$10.00"
android:textAppearance="#style/citation_select_item_cost"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:id="#+id/citation_select_cost_text" android:layout_marginRight="28px"/>
</RelativeLayout>
<ImageView
android:src="#drawable/connector_line"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="#+id/citation_select_connector_left"/>
</LinearLayout>
The connector line far from the side that the view appears on is meant to disappear when onBindViewHolder is called, and the margins updated accordingly.
if (position % 2 == 0) {
holder.itemView.findViewById<ImageView>(R.id.citation_select_connector_left).visibility = View.GONE
val marginLayoutParams1 = holder.citationHolder.layoutParams as GridLayoutManager.LayoutParams
marginLayoutParams1.setMargins(0, 12, 12, 12)
holder.itemView.findViewById<LinearLayout>(R.id.citation_select_holder).layoutParams =
marginLayoutParams1
} else {
holder.itemView.findViewById<ImageView>(R.id.citation_select_connector_right).visibility = View.GONE
val marginLayoutParams2 = holder.citationHolder.layoutParams as GridLayoutManager.LayoutParams
marginLayoutParams2.setMargins(12, 12, 0, 12)
holder.itemView.findViewById<LinearLayout>(R.id.citation_select_holder).layoutParams =
marginLayoutParams2
}
Scrolling is done exclusively via on-screen buttons in increments of six. The first two pages load normally:
But the pattern begins to break down at citation #14. Keep in mind that the citation numbers correspond are the view's position within the RecyclerView:
What is happening to change the behavior?
I think I know what can help you fix this. I presume that it is reusing the old view, as a RecyclerView should and nowhere in your code is there a line to set the visibility of the connector lines back to visible.
You should add to both your GONE visibilities also the code to set the other to visible:
if (position % 2 == 0) {
holder.itemView.findViewById<ImageView>(R.id.citation_select_connector_right).visibility = View.VISIBLE
holder.itemView.findViewById<ImageView>(R.id.citation_select_connector_left).visibility = View.GONE
val marginLayoutParams1 = holder.citationHolder.layoutParams as GridLayoutManager.LayoutParams
marginLayoutParams1.setMargins(0, 12, 12, 12)
holder.itemView.findViewById<LinearLayout>(R.id.citation_select_holder).layoutParams =
marginLayoutParams1
} else {
holder.itemView.findViewById<ImageView>(R.id.citation_select_connector_left).visibility = View.VISIBLE
holder.itemView.findViewById<ImageView>(R.id.citation_select_connector_right).visibility = View.GONE
val marginLayoutParams2 = holder.citationHolder.layoutParams as GridLayoutManager.LayoutParams
marginLayoutParams2.setMargins(12, 12, 0, 12)
holder.itemView.findViewById<LinearLayout>(R.id.citation_select_holder).layoutParams =
marginLayoutParams2
}
Additional explanation
The RecyclerView will reuse some old view, right? Well since both lines are VISIBLE at the start, you assume that's their default state. But when you set the lines to GONE you never put them back to visible, and thus if RecyclerView reuses that view, it'll not add the margin there and it will just be missing the connector line. You always want to have EVERY line of code in onBindViewHolder to have a matching line that reverts it.
Vucko's answer is good, and the overall point (always update every component of your viewholder) is something you should absolutely do.
I wanted to add, however, that it appears as though you are not following the ViewHolder pattern correctly: your onBindViewHolder() method should never call findViewById(). Instead, your ViewHolder class should find each view once and then save references to them.
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val connectorRight: ImageView = itemView.findViewById(R.id.citation_select_connector_right)
val connectorLeft: ImageView = itemView.findViewById(R.id.citation_select_connector_left)
// ...
}
And then you can use these fields directly inside onBindViewHolder():
if (position % 2 == 0) {
holder.connectorRight.visibility = View.VISIBLE
holder.connectorLeft.visibility = View.GONE
// ...
} else {
holder.connectorLeft.visibility = View.VISIBLE
holder.connectorRight.visibility = View.GONE
// ...
}
I'm building a UI that consists of multiple CardViews inside a RecyclerView.
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
android:id="#+id/card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardCornerRadius="5dp"
app:cardElevation="1dp"
app:cardBackgroundColor="#ddffca"
android:animateLayoutChanges="true"
xmlns:android="http://schemas.android.com/apk/res/android"
>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:padding="10dp"
>
<TextView
android:id="#+id/tvHello"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:text="Hello there!"
/>
<TextView
android:id="#+id/test"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:visibility="gone"
android:gravity="center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/tvHello"
android:text="GENERAL KENOBI!"
/>
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
Inside I have specified that on click even I will have my test text animated to appear. It's working great and I'm mostly happy with the results. But as soon as I add a few cards inside the RecyclerView the animation starts working a bit strange. What I mean is views that are not touched are not animating properly considering the other views have changed their size. Instead I see another view jumping to its new position without any animation.
How can I make it animate according to other views?
EDIT
I have also provided my code from onBindViewHolder. It's in Kotlin though:
override fun onBindViewHolder(
holder: OperationsViewHolder,
position: Int
) {
var card: CardView = holder.cardView
card.setOnClickListener {
if (!operations.get(position).selected!!) {
ObjectAnimator.ofFloat(card, "translationZ", 1f, 10f)
.start()
holder.test.visibility = View.VISIBLE;
operations.get(position)
.selected = true
} else {
ObjectAnimator.ofFloat(card, "translationZ", 10f, 1f)
.start()
holder.test.visibility = View.GONE;
operations.get(position)
.selected = false
}
}
}
EDIT 2 I have also tried adding android:animateLayoutChanges="true" to all elements, didn't help
Not sure if this fits the constraints of your question but you don't necessarily have to animate the expanding/collapsing manually. The RecyclerView can provide that to you out-of-the-box by using notifyItemChanged() properly in your Adapter.
override fun onBindViewHolder(
holder: OperationsViewHolder,
position: Int
) {
var card: CardView = holder.cardView
if (operations.get(position).selected!!) {
holder.test.visibility = View.VISIBLE;
} else {
holder.test.visibility = View.GONE;
}
card.setOnClickListener {
if (!operations.get(position).selected!!) {
operations.get(position)
.selected = true
} else {
operations.get(position)
.selected = false
}
notifyItemChanged(position)
}
}
The above code removes the animation logic and instead calls notifyItemChanged every time the CardView is clicked. This tells the RecyclerView to re-render the item at that particular position and gives you an animation for the re-render for free.
Below is my BindableItem Class which also act as an adapter while using Groupie
class FilterByAthleteTypeItem(var athleteResponse: AthleteModel, var onFilterAthleteItemClick: OnFilterAthleteItemClick) : BindableItem<FilterItemLayoutBinding>() {
override fun bind(viewBinding: FilterItemLayoutBinding, position: Int) {
ViewHolder(viewBinding, position)
viewBinding.executePendingBindings()
viewBinding.notifyChange()
}
override fun getLayout(): Int {
return R.layout.filter_item_layout
}
inner class ViewHolder(var binding: FilterItemLayoutBinding, position: Int) : RecyclerView.ViewHolder(binding.root), View.OnClickListener {
override fun onClick(p0: View?) {
athleteResponse.isChecked = binding.playlistSwitch.isChecked
onFilterAthleteItemClick.onFilterAthleteClicked(athleteResponse)
notifyChanged()
}
init {
val athleteModel = athleteResponse
binding.totalItems.text = athleteModel.areelCount.toString()
binding.playlistSwitch.isChecked = athleteModel.isChecked
binding.sportName.text = athleteModel.athleteType
binding.playlistSwitch.setOnClickListener(this)
when {
athleteModel.athleteType == "highschool" -> binding.playerLevelImage.setBackgroundColor(
ContextCompat.getColor(binding.root.context, R.color.black))
athleteModel.athleteType == "college" -> binding.playerLevelImage.setBackgroundColor(
ContextCompat.getColor(binding.root.context, R.color
.college))
athleteModel.athleteType == "pro" -> binding.playerLevelImage.setBackgroundColor(
ContextCompat.getColor(binding.root.context, R.color.pro))
athleteModel.athleteType == "enthusiast" -> binding.playerLevelImage.setBackgroundColor(
ContextCompat.getColor(binding.root.context,
R.color.enthusiast))
athleteModel.athleteType == "military" -> binding.playerLevelImage.setBackgroundColor(
ContextCompat.getColor(binding.root.context,
R.color.text_color_9b))
else -> binding.playerLevelImage.setBackgroundColor(
ContextCompat.getColor(binding.root.context,
R.color.white))
}
}
}
}
interface OnFilterAthleteItemClick {
fun onFilterAthleteClicked(athleteModel: AthleteModel)
}
Here is how I used it in MyActivity
Section section = new Section();
section.setHeader(headerItemGroupie);
if (!Utils.isNull(athleteModelList))
for (int i = 0; i < athleteModelList.size(); i++) {
AthleteModel athleteModel = athleteModelList.get(i);
athleteModel.setPosition(i);
athleteModelList.remove(i);
athleteModelList.add(i, athleteModel);
section.add(new FilterByAthleteTypeItem(athleteModelList.get(i), this));
}
groupAdapter.add(section);
Below is my layout Item file
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/white"
android:orientation="horizontal">
<com.areel.android.customview.CustomTextView
android:id="#+id/totalItems"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="28dp"
android:layout_weight="1.7"
android:paddingBottom="18dp"
android:paddingTop="18dp"
android:textColor="#color/text_color_9b"
android:textSize="12sp"
app:fontPath="#string/font_avenir_heavy"
app:letterSpacing="0.154"
tools:text="14,932"/>
<com.areel.android.customview.CustomTextView
android:id="#+id/sportName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:gravity="end"
android:textAllCaps="true"
android:textColor="#color/black"
android:textSize="12sp"
app:fontPath="#string/font_avenir_heavy"
app:letterSpacing="0.3"
tools:text="NAME OF SPORT"/>
<FrameLayout
android:id="#+id/frameLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1.8">
<ImageView
android:id="#+id/playerLevelImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="21dp"
android:scaleType="fitXY"/>
<android.support.v7.widget.AppCompatCheckBox
android:id="#+id/playlistSwitch"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:button="#drawable/switch_on_off"/>
</FrameLayout>
</LinearLayout>
and here is my recyclerView in layout
<android.support.v7.widget.RecyclerView
android:id="#+id/filter_list_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#+id/showFriendLayout"
android:clipChildren="false"
android:clipToPadding="false">
</android.support.v7.widget.RecyclerView>
Now when I scroll the recyclerView the background color and Image start shuffling means respective positions of background color doesn't remain appropriate while scrolling
the major problem is the last item has an image as a background, and when I scroll that Image overlaps other backgrounds and shuffle happened
But there is now way to change that image to color so I need more robust solution!!
I am adding the screen shots here
First one, What I have done
And I check items and Scrolls the recyclerview then that last image shuffles its position like below image
The FIfth Image also shuffles on Down side
Author of the library here. You have several problems in your posted code.
For starters, you shouldn't be calling executePendingBindings or notifyChange. You aren't using the feature of data binding where you bind a model object, so both of those are unnecessary.
Second, the whole point of Groupie (especially with data binding) is that you shouldn't have to create your own ViewHolder. In fact, yours isn't doing anything. You can move all the code from your ViewHolder class into FilterByAthleteTypeItem.bind().
Last, your problem with images loading in the wrong spots, or duplicated, is a very common issue in RecyclerViews. You didn't post your image loading code, but I'm guessing it's asynchronously loading images -- meaning they load into a certain view, regardless of whether it's been recycled -- and/or failing to clear the old image from reused viewholders. I recommend you avoid dealing with this problem entirely by using an image loading library like Picasso or Glide.
Hope that helps!