So, this should be pretty basic, but I am new to Kotlin. So I basically have the following ImageView inside a RelativeLayout view.
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:srcCompat="#drawable/moon_0"
android:id="#+id/imagen_luna"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_alignParentTop="true" />
The thing is that under certain condition this ImageView can collide visually with another element, so I have to modify the marginStart and the marginEnd properties. I saw in some tutorials that first I must obtain the current layout margins or something like that, but I am not sure why it's so complicated, in Swift I just modify the properties of each margin.
Is there an easy way to achieve this or in case there isn't what would be the easiest way to do this?
UPDATE - Added code in the Adapter
This is the code I have in my adapter for this specific ImageView:
override fun onBindViewHolder(holder: AnoViewHolder, position: Int) {
...
if(respec.nombre_icono_signo != "" && respec.imagen_luna != "") {
val layoutParamsLuna = holder.view?.imagen_luna.layoutParams as RelativeLayout.LayoutParams
layoutParamsLuna.setMargins(1, 8, 15, 8)
holder.view?.imagen_luna.layoutParams = layoutParamsLuna
}
...
}
The thing is that this does nothing and my theory is that the setMargins function accepts left and right margins, not start and end margins.
You can update margin in layoutParams of view
Ex:
val layoutParams= imagen_luna.layoutParams as RelativeLayout.LayoutParams
layoutParams.marginEnd=resources.getDimension(your_dimension).toInt()
layoutParams.marginStart=resources.getDimension(your_dimension).toInt()
layoutParams.bottomMargin=resources.getDimension(your_dimension).toInt()
layoutParams.topMargin=resources.getDimension(your_dimension).toInt()
//or
layoutParams.setMargins(start,top,end,bottom)
imagen_luna.layoutParams=layoutParams
You have to add value for margins in dimension file.because if you set setMargins(1, 8, 15, 8) like this,It's will margin in pixel not in dp.
So use margin values like below.
<dimen name="spacing_normal">16dp</dimen>
<dimen name="spacing_small">8dp</dimen>
<dimen name="spacing_tiny">4dp</dimen>
Update your dapter class like belo
override fun onBindViewHolder(holder: AnoViewHolder, position: Int) {
...
if(respec.nombre_icono_signo != "" && respec.imagen_luna != "") {
val layoutParamsLuna = holder.view?.imagen_luna.layoutParams as RelativeLayout.LayoutParams
layoutParamsLuna.setMargins(context.resources.getDimension(R.dimen.spacing_normal).toInt(),
context.resources.getDimension(R.dimen.spacing_normal).toInt(),
context.resources.getDimension(R.dimen.spacing_normal).toInt(),
context.resources.getDimension(R.dimen.spacing_normal).toInt())
holder.view?.imagen_luna.layoutParams = layoutParamsLuna
}else{
//reset margin for else case because recylerview reuse views so it's will keep previous states
}
...
}
Related
I'm developing a music collection app, and I'm struggling with my current goal :Designing a scrollable list of 3 textviews - track number, track title and track duration - which will represent songs in an album. I want the list's layout to be composed of 3 columns with fixed width,aligned as one would expect, without the title textview for a particular track, for example, stretching over above the duration textview of the track below.
I have considered the following options:
Using one RecyclerView with its ViewHolder layout composed of a horizontal LinearLayout of 3 textviews. The problem here is that I don't know how to keep the columns aligned this way. Should I set fixed widths for the textviews using definite dps? How then can I set the entire row to fill the device's entire width?
Using 3 RecyclerViews for each column. I've tried this idea halfway, and the columns are aligned nicely, but scrolling one RV doesn't scroll the others, obviously, and to fix that - as I've seen in another question here - I'll need to mess with the scrolling mechanism, and it seems it can still be very prone to errors with the RVs still might manage to get out of sync or the app crashing.
I know there is GridLayout (which I'm not familiar with and don't know if it can solve my problem), but since it's now considered legacy, I would rather refrain from using it.
Above all, I would like to know what way is considered "best practice", assuming there is one, to tackling such a problem.
Thanks!
This is an alternative solution inspired by Farshad Tahmasbi's answer I found here.
In this screenshot you can see the result. (The duration column is empty right now).
mTracksRecyclerView.setAdapter(new TrackListAdapter(getContext(), musicItem.getTracks()));
GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), 12);
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
#Override
public int getSpanSize(int position) {
int type = position % 3 ;
if (type == 0) {
// Position
return 2;
} else if (type == 1) {
// Title
return 8;
} else {
// Duration
return 2;
}
}
});
mTracksRecyclerView.setLayoutManager(gridLayoutManager);
I tried to create a scrollable list of 3 TextViews for a row.
Like you have mentioned, we can use a RecyclerView as the scrollable list.
activity_main.xml
<ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</ConstraintLayout>
For a list item to have 3 TextViews in a row, I used a GridLayout as follows.
list_item.xml
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="3"
android:rowCount="1" >
<TextView
android:id="#+id/trackNumber"
android:layout_columnWeight="1"
android:layout_width="0dp"
android:background="#android:color/holo_red_light" />
<TextView
android:id="#+id/trackTitle"
android:layout_columnWeight="6"
android:layout_width="0dp"
android:ellipsize="end"
android:background="#android:color/holo_green_light" />
<TextView
android:id="#+id/trackDuration"
android:layout_columnWeight="1"
android:layout_width="0dp"
android:background="#android:color/holo_blue_light" />
</GridLayout>
For each TextView a layout_columnWeight value is set to specify the relative proportion of horizontal space that should be allocated for it.
Each TextView's layout_width value is set to 0dp for it to take full width of the allocated space.
But instead of GridLayout, we can use a ConstraintLayout and achieve the same result.
As mentioned above in the question, GridLayout is marked as legacy.
It better to use ConstraintLayout.
list_item.xml
<ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="#+id/trackNumber"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintHorizontal_weight="1"
android:background="#android:color/holo_red_light"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="#id/trackTitle"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/trackTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintHorizontal_weight="6"
android:ellipsize="end"
android:background="#android:color/holo_green_light"
app:layout_constraintStart_toEndOf="#id/trackNumber"
app:layout_constraintEnd_toStartOf="#id/trackDuration"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/trackDuration"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintHorizontal_weight="1"
android:background="#android:color/holo_blue_light"
app:layout_constraintStart_toEndOf="#id/trackTitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</ConstraintLayout>
For each TextView a layout_constraintHorizontal_weight value is set to specify the relative proportion of horizontal space that should be allocated for it.
Each TextView's layout_width value is set to 0dp for it to take full width of the allocated space.
This is just a Kotlin class to store track information.
class TrackInfo(
var trackNumber: Int,
var trackTitle: String?,
var durationMinutes: Short,
var durationSeconds: Short
)
This is the Adapter class and ViewHolder class for the RecyclerView.
class TrackInfoAdapter(private val items: List<TrackInfo>, private val mContext: Context) :
RecyclerView.Adapter<ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false)
)
}
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val trackInfo = items[position]
holder.trackNumber.text = trackInfo.trackNumber.toString()
holder.trackTitle.text = trackInfo.trackTitle
// Use String.format() to add leading zero for single digit durationSeconds value
val formattedDurationSeconds = "%02d".format(trackInfo.durationSeconds)
holder.trackDuration.text = "${trackInfo.durationMinutes}:$formattedDurationSeconds"
}
}
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val trackNumber = view.trackNumber
val trackTitle = view.trackTitle
val trackDuration = view.trackDuration
}
This is the MainActivity.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Prepare list of track information
val trackInfoList = mutableListOf<TrackInfo>().apply {
// track 1
add(
TrackInfo(1, "Lady GaGa - Poker Face",
4, 4)
)
// track 2
add(
TrackInfo(2, "T.I. featuring Rihanna - Live Your Life",
4, 1)
)
// track 3
add(
TrackInfo(3, "Kanye West - Stronger",
5, 11)
)
// track 4
add(
TrackInfo(4, "Leon Haywood - I Wanna Do Something Freaky To You",
6, 0)
)
// track 5
add(
TrackInfo(5, "Hilary Duff - Reach Out",
4, 16)
)
}
recyclerView.layoutManager = LinearLayoutManager(baseContext)
// Set track information list to Adapter of RecyclerView
recyclerView.adapter = TrackInfoAdapter(trackInfoList, baseContext)
}
}
Result :
I hope this answers the first part of your question.
You have mentioned about 'stretching over above the duration textview, when there's a particular track without a title'.
For that, children of a GridLayout can be configured to span multiple cells using android:layout_columnSpan attribute.
But to handle trackTitle null cases and set properties like layout_columnSpan of TextViews accordingly, I think it would be better to write a custom view class for list item.
the best solution to this is using RecyclerView.
1 - you can set the width of the TextViews to match_parent if you want them to fill the width of the screen.
2 - you need only one recycler view that shows a list of a view.
if you can show me a picture of what you want to achieve
I wish to add a Custom ItemDecoration in my RecyclerView that is a layout defined in a XML file.
So far I was able to inflate the XML and position using canvas.translate (yet, without understanding everything).
Currently I have this code to draw:
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
val left = child.marginLeft
val top = child.top
context?.let {
//Inflate the Layout and set the Values (a text in this case)
val view = LayoutInflater.from(it).inflate(R.layout.my_decoration_layout, parent, false)
val textView = view.findViewById<TextView>(R.id.textView)
textView.text = "This is an ItemDecoration"
//Calculate the Size. Im using "hardcoded" values, but this does not seems to change how the View is rendered
view.measure(1000,1000)
view.layout(0, 0, 1000, 1000)
//Draw. I had to translate the canvas to apply the offset for each "ViewHolder"
canvas.save()
canvas.translate(left.toFloat(), top.toFloat())
view.draw(canvas)
canvas.restore()
}
}
}
The XML is (note the background colors to see the Rendered Area):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#android:color/holo_blue_light">
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#android:color/holo_red_light"/>
</LinearLayout>
With this, I could inflate and position my XML between each ViewHolder, but I still have two big issues:
My TextView is drawed over the ViewHolder, so I get here an overlap.
Would be great if this "Draw" operation pushes the ViewHolder.
Even my custom XML layout has it's width as "match_parent", it only wraps the text view.
If possible, Id like to know which exactly what "measure" and "layout" means and how it affect my View "area".
And how to prevent the overlap.
I "solved" the Overlap issue drawing a Text on the Canvas, using
Paint().apply {
color = Color.BLACK
style = Paint.Style.FILL
textSize = 40f
canvas.drawText(year.toString(), left.toFloat(), top.toFloat(), this)
}
But since my layout is a bit more complex, would be nice to understand how to do it with XML layouts.
Thanks
How can I get the height of recent android phones' notch, preferably in pixels?
I have an implementation where an image MUST be at the top in full screen.
With the addition of a notch to recently released phones, this image will be cut out by the notch. My idea is to get the notch height and add a margin above it dynamically to accommodate different notch sizes.
Any help or other approaches would be greatly appreciated, thanks in advance!
The easiest way to get the cutout's height in pixels is to override onAttachedToWindow() function of your View. View being attached to window is the main requirement to rootWindowInsets being non-null. Note that it's safe to check SDK version, because complete support of cutout has been started since Android Pie.
override fun onAttachedToWindow() {
super.onAttachedToWindow()
layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply {
topMargin =
if (SDK_INT >= Build.VERSION_CODES.P) rootWindowInsets?.displayCutout?.safeInsetTop ?: 0
else 0
}
}
In case that your View isn't CustomView you should add OnAttachStateChangeListener:
imageView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewDetachedFromWindow(v: View?) { }
override fun onViewAttachedToWindow(v: View?) {
// call the code from above starting from 'layoutParams = ...' on your View object
}
})
Don't forget to change LayoutParams(MATCH_PARENT, WRAP_CONTENT) for your own needs.
I used the method below(in Kotlin):
*It will only work in Android P and above.
*My activity is a full screen activity.
*My assumption is android phone with notches are relatively new will either come out of the box with Android P or have been rapidly updated to Android P.
Create a OnGlobalLayoutListener
private val onDisplayCutoutReadyListener : ViewTreeObserver.OnGlobalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
var displayCutout = window.decorView.rootWindowInsets.displayCutout
if (displayCutout.boundingRects.size>0) {
val notchRect = displayCutout.boundingRects.get(0)
Log.v(TAG,"notch height in pixels="+notchRect.height())
}
}
}
Add the listener to your activity's rootview using .viewTreeObserver.addOnGlobalLayoutListener()
layout_fullscreen.viewTreeObserver.addOnGlobalLayoutListener(onDisplayCutoutReadyListener)
My layout xml where layout_fullscreen is the rootview.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/layout_fullscreen"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_weight="1"
android:layout_height="0dp">
<com.baidu.mapapi.map.MapView
android:id="#+id/map_location"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
Ive got a view like below :
<org.rayanmehr.atlas.shared.customview.CustomTextView
android:id="#+id/tvDownVoteCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="6dp"
app:layout_constraintVertical_bias="0"
app:layout_constraintTop_toBottomOf="#+id/anchorHelper"
app:layout_constraintLeft_toRightOf="#+id/tvDownVoteIcon"
app:layout_constraintBottom_toBottomOf="#+id/imgComment"
/>
how can i modify the value of app:layout_constraintVertical_bias or any other constraint property programmatically without having top set the whole set of the properties again in my activity ?
Here is what I did (no need for ConstraintSet, we can work directly on the constraints themselves):
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) myView.getLayoutParams();
params.horizontalBias = 0.2f; // here is one modification for example. modify anything else you want :)
myView.setLayoutParams(params); // request the view to use the new modified params
it worked like a charm when I had a SeekBar and a TextView below it (aligned to it both left+right), and I wanted to update the TextView position to be under the SeekBar's cursor, so I had to update the horizontal bias params on the SeekBar's OnSeekBarChangeListener
If you are using Kotlin and you have androidx-core-ktx lib you can simply do:
someView.updateLayoutParams<ConstraintLayout.LayoutParams> { horizontalBias = 0.5f }
I just found the answer in here and you can use ConstraintSet to achieve this like below:
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(context, R.id.activity_constraint);
//for example lets change the vertical bias of tvDownVoteIcon
float biasedValue = 0.2f;
constraintSet.setVerticalBias(R.id.tvDownVoteIcon, biasedValue);
//or change the anchor
constraintSet.connect
(R.id.tvDownVoteIcon,ConstraintSet.RIGHT,R.id.txt,ConstraintSet.RIGHT,0);
//then apply
constraintSet.applyTo( (ConstraintLayout) findViewById(R.id.activity_constraint));
Disclaimer: I haven't used this specific functionality. This is just my interpretation on how I would try to do it.
I think that what you need is ConstraintSet. After obtaining it, you could modify it and apply it again. This is a relevant example from this article.
override fun onCreate(savedInstanceState: Bundle?) {
...
val constraintSet1 = ConstraintSet()
constraintSet1.clone(constraintLayout)
val constraintSet2 = ConstraintSet()
constraintSet2.clone(constraintLayout)
constraintSet2.centerVertically(R.id.image, 0)
var changed = false
findViewById(R.id.button).setOnClickListener {
TransitionManager.beginDelayedTransition(constraintLayout)
val constraint = if (changed) constraintSet1 else constraintSet2
constraint.applyTo(constraintLayout)
changed = !changed
}
}
I want to be able to to be able to set padding values if a boolean is true. The problem is that Android studio cannot parse the layout because it thinks 2dp is a decimal with a value of 2 and then doesn't know what to do with the p. how do I format this so that it understands i mean 2 density pixels.
Data layout:
<data class=".ItemBinding">
<variable name="isGroupType" type="Boolean"/>
</data>
View layout(whats important):
<android.support.v7.widget.AppCompatImageView
android:layout_width="64dp"
android:layout_height="64dp"
android:paddingBottom='#{isGroupType ? 2dp : 0dp}'
android:paddingTop='#{isGroupType ? 8dp : 0dp}'
android:paddingRight='#{isGroupType ? 2dp : 0dp}'
android:paddingLeft='#{isGroupType ? 2dp : 0dp}'/>
Store padding value in dimen.xml and use it. Please keep habit to write binding string with " " (double quotes)
android:paddingBottom="#{isGroupType ? #dimen/padding_normal : #dimen/padding_null}"
and so on for other paddings also.
For anyone looking to set margins via DataBinding, you'll have to use BindingAdapter as well:
#BindingAdapter("layoutMarginBottom")
fun setLayoutMarginBottom(view: View, dimen: Float) {
val layoutParams = view.layoutParams as MarginLayoutParams
layoutParams.bottomMargin = dimen.toInt()
view.layoutParams = layoutParams
}
And your xml property will look like this:
app:layoutMarginBottom="#{someCondition ? #dimen/zero_dp : #dimen/twenty_dp}"
Just as a heads-up this does not work with layout_margin's :(
Not sure why, but think it's due to the parent layout needs to be remeasured..
#Ravi's answer is correct.
But for more flexibility you can also try this:
#BindingAdapter({"padding", "shouldAdd"})
public static void setPadding(AppCompatImageView imageView, boolean shouldAdd, int padding){
if (shouldAdd){
imageView.setPadding(padding, padding, padding, padding);
}
}
Then:
<android.support.v7.widget.AppCompatImageView
android:layout_width="64dp"
android:layout_height="64dp"
shouldAdd="#{isGroupType}"
padding="#{10}"/>
#Ravi's answer is good, but it's working only for padding.
If You want to simply add margin, create empty view e.g TextView with padding.
Use a blank view
<View
android:layout_width = "8dp"
android:layout_height = "8dp"
>
Now constraint this view to the layout you want to control the visibility of .
Now disappear this view depending on the condition. would work the same as a margin
You can use logic and ternary statements in xml-binding, but you really shouldn't. It will come back to haunt you when you're looking the usual places you have logic and don't see what's going on.
BindingAdapter for all your margin needs:
fun bindingSetMargins(view: View, start: Float?, top: Float?, end: Float?, bottom: Float?) {
view.layoutParams = (view.layoutParams as ViewGroup.MarginLayoutParams).apply {
start?.toInt()?.let { leftMargin = it }
top?.toInt()?.let { topMargin = it }
end?.toInt()?.let { rightMargin = it }
bottom?.toInt()?.let { bottomMargin = it }
}
}