smoothScrollTo(position) scrolls to bottom instead of targeted position in RecyclerView - java

My vertical RecyclerView (looking like a simple list) in Kotlin scrolls neatly to the next element when I press a button. 👍🏻
However when the next element is off screen (for example because in the meantime the user scrolled to a different position with a gesture) the problem is that this doesn't work anymore. Instead, it automatically scrolls all the way to the bottom of the RecyclerView.
Any idea how to fix that? I'd appreciate it!
Thanks in advance for your efforts.
I'm overriding SmoothScrollLinearLayoutManager:
class SmoothScrollLinearLayoutManager(private val mContext: Context, orientation: Int, reverseLayout: Boolean) : LinearLayoutManager(mContext, orientation, reverseLayout) {
override fun smoothScrollToPosition(recyclerView: RecyclerView, state: RecyclerView.State?,
position: Int) {
val smoothScroller = object : TopSnappedSmoothScroller(recyclerView.context) {
//This controls the direction in which smoothScroll looks for your view
override fun computeScrollVectorForPosition(targetPosition: Int): PointF {
return PointF(0f, 1f)
}
//This returns the milliseconds it takes to scroll one pixel.
protected override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi
}
}
smoothScroller.targetPosition = position
Log.i("Target", smoothScroller.targetPosition.toString())
startSmoothScroll(smoothScroller)
}
private open inner class TopSnappedSmoothScroller(context: Context) : LinearSmoothScroller(context) {
override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {
return this#SmoothScrollLinearLayoutManager
.computeScrollVectorForPosition(targetPosition)
}
override fun getVerticalSnapPreference(): Int {
return LinearSmoothScroller.SNAP_TO_START
}
}
private open inner class CenterSnappedSmoothScroller(context: Context) : LinearSmoothScroller(context) {
override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {
return this#SmoothScrollLinearLayoutManager
.computeScrollVectorForPosition(targetPosition)
}
override fun getVerticalSnapPreference(): Int {
return LinearSmoothScroller.SNAP_TO_END
}
}
// Scrolling speed
companion object {
private val MILLISECONDS_PER_INCH = 110f
}
}
The button triggers this function to scroll to the next element in the list (RecyclerView):
private fun fastForwardTapped() {
// Update selected position in RecyclerView
selectedPosition += 1
recyclerView.smoothScrollToPosition(selectedPosition)
}

It had to do with how I overrode LinearLayoutManager.
I ended up using this solution:
https://stackoverflow.com/a/36784136/8400139
It works.

Related

Android: Recyclerview with ItemTouchHelper.Callback flickering on lower items in list

I am currently trying to implement a RecyclerView list with drag and drop reordering. For this I use the ItemTouchHelper.SimpleCallback
class SoftkeyScreenListReorderHelperCallback(
private val adapter: SoftkeyScreenListAdapter
) : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.START or ItemTouchHelper.END, 0) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return adapter.itemMoved(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
}
My Adapter got the itemMoved() method, which is called in the onMove() method in the callback. Here I just swap the items and notify the adapter about the change.
fun itemMoved(fromPosition: Int, toPosition: Int): Boolean {
Collections.swap(list, fromPosition, toPosition)
notifyItemMoved(fromPosition, toPosition)
return true
}
For my RecyclerView I implemented the following
binding.recyclerview.apply {
[...] // adapter init
myAdapter.setHasStableIds(true)
adapter = myAdapter
val touchHelper = ItemTouchHelper(SoftkeyScreenListReorderHelperCallback(adapter as SoftkeyScreenListAdapter))
touchHelper?.attachToRecyclerView(this)
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
setHasFixedSize(true)
}
It works, but I always get flickering for the items below (after) the new item position. Assume I have 5 Items {1,2,3,4,5} and want to swap 1 with 3, then 4 and 5 are flickering. 1, 2 and 3 don't.
I already set the recyclerview size fixed, enabled stable ids and disabled animations, but it does not help. Does anyone has a clue what could be the reason for that and how to fix?
try this
recyclerView.getItemAnimator().setChangeDuration(0);
Possibly, you are getting or loading your data from a place or using a library that requires some time. If that is the case, this answer could help
Try this:
After adapter initialization:
adapter.setHasStableIds(true);
In adapter class:
#Override
public long getItemId(int position) {
return itemList.get(position).hashCode();
}

recover EditText from a RecyclerView item (from the adabter to myActivity)?

hi guys I need help I'm using android studio java for the first time ...
this is my interface
enter image description here
I wanna recover the EditText value in a list when the user clicks in "valider la commande"
In order to retrieve that value, you should have an array declared in your recycler views adapter, in which you write each value whenever the user changes it.
You should also add a public method to retrieve that array from your adapter class. So on, in your button onClickListener you just retrieve that array from your adapter using the public method.
Here is an example:
class AsistenteAdapter(var names: ArrayList<String>): RecyclerView.Adapter<AsistenteAdapter.ViewHolder>() {
//DECLARE HERE YOUR ARRAY VARIABLE (SAME SIZE AS names.size)
class ViewHolder(var binding: ComponentApuntadoContainerBinding): RecyclerView.ViewHolder(binding.root){}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ComponentApuntadoContainerBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
//ADD HERE YOUR LOGIC TO SAVE THE VALUE
//Put on text changed listener to your EDITTEXT, so that whenever the text is changed, the value is saved in your array
}
override fun getItemCount(): Int {
return names.size
}
public fun getValues(): //ARRAY {
return //YOUR ARRAY VARIABLE
}
}

How to swipe back an item in RecyclerView after swipe

After swiping an item in RecyclerView I want it to go back without swiping it back manually.
Here is an swipeable item in RecyclerView.
Item in RecyclerView
Swiping...
After swipe event I want this item to go back, as if it was swiped not far enough, but event must happen. How can I do this?
After swipe
Here is my SwipeHelper, which keeps background static:
abstract class ProfileSwipeHelper : ItemTouchHelper.SimpleCallback(0,
ItemTouchHelper.LEFT
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return true
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
if (viewHolder != null) {
ItemTouchHelper.Callback.getDefaultUIUtil().onSelected((viewHolder as ProfilesAdapter.ViewHolder).foreground)
}
}
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
getDefaultUIUtil().onDraw(c, recyclerView,
(viewHolder as ProfilesAdapter.ViewHolder).foreground, dX, dY,
actionState, isCurrentlyActive)
}
override fun onChildDrawOver(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder?,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
getDefaultUIUtil().onDrawOver(
c, recyclerView,
(viewHolder as ProfilesAdapter.ViewHolder).foreground, dX, dY,
actionState, isCurrentlyActive)
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
getDefaultUIUtil().clearView((viewHolder as ProfilesAdapter.ViewHolder).foreground)
}
}
And here onSwiped event in main activity, only with Toast:
//Main Activity
val context : Context = this
val deleteSwipeHandler1 = object : ProfileSwipeHelper() {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
Toast.makeText(context, "swiped", Toast.LENGTH_SHORT).show()
}
}
ItemTouchHelper(deleteSwipeHandler1).attachToRecyclerView(rv_profiles)
You can use Multiswipe library. Read complete explanations here. If you want to use Java read this. Below is
a concise explanation without using complete options of this library:
add jitpack and Multiswipe library to your project:
in settings.gradle or root build.gradle:
repositories {
//...
maven { url 'https://jitpack.io' }
}
in app's build.gradle:
dependencies {
implementation 'com.github.ygngy:multiswipe:1.2.1'
}
implement MultiSwipe in ViewHolder:
import androidx.recyclerview.widget.RecyclerView
import android.view.View
import com.github.ygngy.multiswipe.MultiSwipe
import com.github.ygngy.multiswipe.LeftSwipeList
import com.github.ygngy.multiswipe.RightSwipeList
class ViewHolder(private val view: View) : RecyclerView.ViewHolder(view), MultiSwipe {
var mLeftSwipeList: LeftSwipeList? = null
var mRightSwipeList: RightSwipeList? = null
// todo other ViewHolder codes...
fun bind() {
// Each swipe contains of at least an id and an icon
val likeSwipe = Swipe(
context = context, // context used to extract default colors and margins from resources
id = SWIPE_TO_LIKE_ID, // swipe id will be sent to onSwipeDone when swipe is completed
activeIcon = getDrawable(R.drawable.ic_like_24)!!, // swipe icon
activeLabel = getString(R.string.like), // OPTIONAL swipe label
acceptIcon = getDrawable(R.drawable.ic_like_accept_24)!!,// OPTIONAL icon used when swipe displacement is greater than "accept boundary"
acceptLabel = getString(R.string.like_accept),// OPTIONAL label used when swipe swipe displacement is greater than "accept boundary"
inactiveIcon = getDrawable(R.drawable.ic_disabled_like_24)!!// OPTIONAL icon used when this swipe could be next swipe
)
// Create other swipes (shareSwipe, copySwipe, ...) in a similar way.
// If row has left swipes, create left swipe list in the desired order like below:
mLeftSwipeList = LeftSwipeList (shareSwipe, copySwipe, cutSwipe)
// If row has right swipes, create right swipe list in the desired order like below:
mRightSwipeList = RightSwipeList (likeSwipe, editSwipe, delSwipe)
}
// Don't recreate swipes or any object here
override val leftSwipeList: LeftSwipeList?
get() = mLeftSwipeList
// Don't recreate swipes or any object here
override val rightSwipeList: RightSwipeList?
get() = mRightSwipeList
// Here handle swipe event and/or return some data to MultiSwipeListener
override fun onSwipeDone(swipeId: Int): Any? {
// Instead you may choose to only return data
// from this method to consume event at Activity or Fragment
when(swipeId) {
SWIPE_TO_SHARE_ID -> {
// todo share
}
SWIPE_TO_COPY_ID -> {
// todo copy
}
//...
}
return MyData()// return any data to Activity or Fragment
}
}
attach library to recyclerview at activity or fragment:
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.github.ygngy.multiswipe.MultiSwipeListener
import com.github.ygngy.multiswipe.SwipeDirection
import com.github.ygngy.multiswipe.multiSwiping // importing extension function
class DemoActivity : AppCompatActivity() {
// todo other activity codes...
override fun onCreate(savedInstanceState: Bundle?) {
// todo other onCreate codes...
// attaching Multiswipe to RecycerView
recyclerView.multiSwiping(
swipeThreshold = 0.5f, // OPTIONAL, the fraction of view for complete swipe threshold
object: MultiSwipeListener { // OPTIONAL listener
// This method is called after onSwipeDone of ViewHolder
// and data is the returned value of onSwipeDone of ViewHolder
override fun onSwipeDone(swipeId: Int, data: Any?) {
// data is the return value of "ViewHolder.onSwipeDone"
// cast to data you returned from "ViewHolder.onSwipeDone"
val myData = data as MyData?
when(swipeId) {
SWIPE_TO_SHARE_ID -> shareItem(myData)
SWIPE_TO_COPY_ID -> copyItem(myData)
//...
}
}
/***
This method will be called when direction changes in each swipe.
This method could be used to hide on screen widgets such as FABs.
direction may be:
- START (when user opens start side of view),
- END (when user opens end side of view),
- NONE (when swipe is closing without user interaction)
***/
override fun swiping(direction: SwipeDirection, swipeListSize: Int) {
// here i hide FAB when user is swiping end side actively
if (direction == SwipeDirection.END) fab.hide() else fab.show()
}
})
}
}

Can You smooth scroll to new position within RecyclerView (before it is shown) and then show insert animation?

Hello fellow programmers!
I am currently facing problem with RecyclerView's add animation with smooth scroll simulataneously.
Problem
I have implemented RecyclerView with ListAdapter and DiffUtil for cool animations (insert, remove). The problem is that I can't get it to smooth scroll to added position without cancelling insert animation.
User is supposed to type in 'exercise name' and then push 'add' button to add it to current 'exercise list'. Then this exercise should be added to RecyclerView list. Everything works fine here, but I can't achieve smooth scroll to new item position without cancelling (or accelerating) insert animation.
viewModel.exerciseListLiveData.observe(viewLifecycleOwner, {
adapter.submitList(it) {
// Cancels insert animation (or accelerates it rapidly) - bad UX...
layoutManager.smoothScrollToPosition(recyclerView, RecyclerView.State(), 0)
}
})
On the other hand, substituting smoothScrollToPosition with scrollToPosition works fine but when there's more items (recycler's view is filled from top to bottom) it blinks on scroll. I know it's rather jump than smooth scroll but then, why is it working with add animation so it doesn't cancel or accelerate?
viewModel.exerciseListLiveData.observe(viewLifecycleOwner, {
adapter.submitList(it) {
// Works fine (recyclerView is scrolling to added position)
// but when there's no room to display more items - it scrolls to it with blink...
// which I simply can't stand!
layoutManager.smoothScrollToPosition(recyclerView, RecyclerView.State(), 0)
}
})
Possible solution?
I'm supposed to first smoothScrollToPosition(0) to top-most position, then call submitList(it) and finally call scrollToPosition(0) but I can't achieve that (it is doing so fast, simultaneously, that it's layering each other). Maybe I should use coroutines? Don't know...
How am I supposed to add proper delay? Maybe I am doing something wrong because i read that some programmers had this autoscroll when added item.
Code sample
Adapter
class ExercisesListAdapter :
ListAdapter<Exercise, ExercisesListAdapter.ExerciseItemViewHolder>(ExercisesListDiffUtil()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExerciseItemViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding: ItemRecyclerviewExerciseBinding =
DataBindingUtil.inflate(inflater, R.layout.item_recyclerview_exercise, parent, false)
return ExerciseItemViewHolder(binding)
}
override fun onBindViewHolder(holder: ExerciseItemViewHolder, position: Int) {
val currentExercise = getItem(position)
holder.bind(currentExercise)
}
override fun getItemId(position: Int): Long {
return getItem(position).id
}
// Item ViewHolder class
class ExerciseItemViewHolder(private val binding: ItemRecyclerviewExerciseBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(itemExercise: Exercise) {
binding.textCardExerciseName.text = itemExercise.name
}
}
}
DiffUtil
class ExercisesListDiffUtil: DiffUtil.ItemCallback<Exercise>() {
override fun areItemsTheSame(oldItem: Exercise, newItem: Exercise): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Exercise, newItem: Exercise): Boolean {
return oldItem == newItem
}
}
ViewModel
class CreateWorkoutViewModel(private val repository: WorkoutRepository) : ViewModel() {
private var exerciseId = 0L
private var exerciseList = mutableListOf<Exercise>()
val exerciseName = MutableLiveData<String?>()
private val _exerciseListLiveData = MutableLiveData<List<Exercise>>()
val exerciseListLiveData: LiveData<List<Exercise>>
get() = _exerciseListLiveData
fun onAddExercise() {
if (canValidateExerciseName())
addExerciseToList()
}
fun onAddWorkout() {
exerciseList.removeAt(2)
_exerciseListLiveData.value = exerciseList.toList()
}
private fun canValidateExerciseName(): Boolean {
return !exerciseName.value.isNullOrBlank()
}
private fun addExerciseToList() {
exerciseList.add(0, Exercise(exerciseId++, exerciseName.value!!))
_exerciseListLiveData.value = exerciseList.toList()
}
}
Fragment
class CreateWorkoutFragment : Fragment() {
lateinit var binding: FragmentCreateWorkoutBinding
lateinit var viewModel: CreateWorkoutViewModel
lateinit var recyclerView: RecyclerView
lateinit var adapter: ExercisesListAdapter
lateinit var layoutManager: LinearLayoutManager
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding =
DataBindingUtil.inflate(inflater, R.layout.fragment_create_workout, container, false)
setViewModel()
setRecyclerView()
// Observers:
// Update RecyclerView list on change
viewModel.exerciseListLiveData.observe(viewLifecycleOwner, {
adapter.submitList(it) {
layoutManager.smoothScrollToPosition(recyclerView, RecyclerView.State(), 0)
}
})
return binding.root
}
private fun setRecyclerView() {
recyclerView = binding.recyclerviewExercises
setLayoutManager()
setAdapter()
}
private fun setLayoutManager() {
layoutManager = LinearLayoutManager(context)
recyclerView.layoutManager = layoutManager
}
private fun setAdapter() {
adapter = ExercisesListAdapter()
adapter.setHasStableIds(true)
recyclerView.adapter = adapter
}
private fun setViewModel() {
val dao = WorkoutDatabase.getInstance(requireContext()).workoutDAO
val repository = WorkoutRepository(dao)
val factory = CreateWorkoutViewModelFactory(repository)
viewModel = ViewModelProvider(this, factory).get(CreateWorkoutViewModel::class.java)
binding.viewModel = viewModel
binding.lifecycleOwner = this
}
}
EDIT
I got it working but it's not 'elegant' way and insert animation is canceled through process :/
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if(layoutManager.findFirstCompletelyVisibleItemPosition() == 0)
layoutManager.scrollToPosition(0)
else {
recyclerView.clearAnimation()
layoutManager.smoothScrollToPosition(recyclerView, RecyclerView.State(), 0)
}
}
})

View's getWidth() and getHeight() returns 0

I am creating all of the elements in my android project dynamically. I am trying to get the width and height of a button so that I can rotate that button around. I am just trying to learn how to work with the android language. However, it returns 0.
I did some research and I saw that it needs to be done somewhere other than in the onCreate() method. If someone can give me an example of how to do it, that would be great.
Here is my current code:
package com.animation;
import android.app.Activity;
import android.os.Bundle;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.Button;
import android.widget.LinearLayout;
public class AnimateScreen extends Activity {
//Called when the activity is first created.
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout ll = new LinearLayout(this);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
layoutParams.setMargins(30, 20, 30, 0);
Button bt = new Button(this);
bt.setText(String.valueOf(bt.getWidth()));
RotateAnimation ra = new RotateAnimation(0,360,bt.getWidth() / 2,bt.getHeight() / 2);
ra.setDuration(3000L);
ra.setRepeatMode(Animation.RESTART);
ra.setRepeatCount(Animation.INFINITE);
ra.setInterpolator(new LinearInterpolator());
bt.startAnimation(ra);
ll.addView(bt,layoutParams);
setContentView(ll);
}
Any help is appreciated.
The basic problem is, that you have to wait for the drawing phase for the actual measurements (especially with dynamic values like wrap_content or match_parent), but usually this phase hasn't been finished up to onResume(). So you need a workaround for waiting for this phase. There a are different possible solutions to this:
1. Listen to Draw/Layout Events: ViewTreeObserver
A ViewTreeObserver gets fired for different drawing events. Usually the OnGlobalLayoutListener is what you want for getting the measurement, so the code in the listener will be called after the layout phase, so the measurements are ready:
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
view.getHeight(); //height is ready
}
});
Note: The listener will be immediately removed because otherwise it will fire on every layout event. If you have to support apps SDK Lvl < 16 use this to unregister the listener:
public void removeGlobalOnLayoutListener (ViewTreeObserver.OnGlobalLayoutListener victim)
2. Add a runnable to the layout queue: View.post()
Not very well known and my favourite solution. Basically just use the View's post method with your own runnable. This basically queues your code after the view's measure, layout, etc. as stated by Romain Guy:
The UI event queue will process events in order. After
setContentView() is invoked, the event queue will contain a message
asking for a relayout, so anything you post to the queue will happen
after the layout pass
Example:
final View view=//smth;
...
view.post(new Runnable() {
#Override
public void run() {
view.getHeight(); //height is ready
}
});
The advantage over ViewTreeObserver:
your code is only executed once and you don't have to disable the Observer after execution which can be a hassle
less verbose syntax
References:
https://stackoverflow.com/a/3602144/774398
https://stackoverflow.com/a/3948036/774398
3. Overwrite Views's onLayout Method
This is only practical in certain situation when the logic can be encapsulated in the view itself, otherwise this is a quite verbose and cumbersome syntax.
view = new View(this) {
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
view.getHeight(); //height is ready
}
};
Also mind, that onLayout will be called many times, so be considerate what you do in the method, or disable your code after the first time
4. Check if has been through layout phase
If you have code that is executing multiple times while creating the ui you could use the following support v4 lib method:
View viewYouNeedHeightFrom = ...
...
if(ViewCompat.isLaidOut(viewYouNeedHeightFrom)) {
viewYouNeedHeightFrom.getHeight();
}
Returns true if view has been through at least one layout since it was
last attached to or detached from a window.
Additional: Getting staticly defined measurements
If it suffices to just get the statically defined height/width, you can just do this with:
View.getMeasuredWidth()
View.getMeasuredHeigth()
But mind you, that this might be different to the actual width/height after drawing. The javadoc describes the difference in more detail:
The size of a view is expressed with a width and a height. A view
actually possess two pairs of width and height values.
The first pair is known as measured width and measured height. These
dimensions define how big a view wants to be within its parent (see
Layout for more details.) The measured dimensions can be obtained by
calling getMeasuredWidth() and getMeasuredHeight().
The second pair is simply known as width and height, or sometimes
drawing width and drawing height. These dimensions define the actual
size of the view on screen, at drawing time and after layout. These
values may, but do not have to, be different from the measured width
and height. The width and height can be obtained by calling getWidth()
and getHeight().
We can use
#Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
//Here you can get the size!
}
You are calling getWidth() too early. The UI has not been sized and laid out on the screen yet.
I doubt you want to be doing what you are doing, anyway -- widgets being animated do not change their clickable areas, and so the button will still respond to clicks in the original orientation regardless of how it has rotated.
That being said, you can use a dimension resource to define the button size, then reference that dimension resource from your layout file and your source code, to avoid this problem.
I used this solution, which I think is better than onWindowFocusChanged(). If you open a DialogFragment, then rotate the phone, onWindowFocusChanged will be called only when the user closes the dialog):
yourView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
// Ensure you call it only once :
yourView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
// Here you can get the size :)
}
});
Edit : as removeGlobalOnLayoutListener is deprecated, you should now do :
#SuppressLint("NewApi")
#SuppressWarnings("deprecation")
#Override
public void onGlobalLayout() {
// Ensure you call it only once :
if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
yourView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
else {
yourView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
// Here you can get the size :)
}
If you need to get width of some widget before it is displayed on screen, you can use getMeasuredWidth() or getMeasuredHeight().
myImage.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
int width = myImage.getMeasuredWidth();
int height = myImage.getMeasuredHeight();
As Ian states in this Android Developers thread:
Anyhow, the deal is that layout of the
contents of a window happens
after all the elements are constructed and added to their parent
views. It has to be this way, because
until you know what components a View
contains, and what they contain, and
so on, there's no sensible way you can
lay it out.
Bottom line, if you call getWidth()
etc. in a constructor, it will return
zero. The procedure is to create all
your view elements in the constructor,
then wait for your View's
onSizeChanged() method to be called --
that's when you first find out your
real size, so that's when you set up
the sizes of your GUI elements.
Be aware too that onSizeChanged() is
sometimes called with parameters of
zero -- check for this case, and
return immediately (so you don't get a
divide by zero when calculating your
layout, etc.). Some time later it
will be called with the real values.
I would rather use OnPreDrawListener() instead of addOnGlobalLayoutListener(), since it is called a bit earlier than other listeners.
view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener()
{
#Override
public boolean onPreDraw()
{
if (view.getViewTreeObserver().isAlive())
view.getViewTreeObserver().removeOnPreDrawListener(this);
// put your code here
return true;
}
});
Adjusted the code according to comment of #Pang. onPreDraw method should return true to proceed with the current drawing pass.
AndroidX has multiple extension functions that help you with this kind of work, inside androidx.core.view
You need to use Kotlin for this.
The one that best fits here is doOnLayout:
Performs the given action when this view is laid out. If the view has been laid out and it has not requested a layout, the action will be performed straight away otherwise, the action will be performed after the view is next laid out.
The action will only be invoked once on the next layout and then removed.
In your example:
bt.doOnLayout {
val ra = RotateAnimation(0,360,it.width / 2,it.height / 2)
// more code
}
Dependency: androidx.core:core-ktx:1.0.0
A Kotlin Extension to observe on the global layout and perform a given task when height is ready dynamically.
Usage:
view.height { Log.i("Info", "Here is your height:" + it) }
Implementation:
fun <T : View> T.height(function: (Int) -> Unit) {
if (height == 0)
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
function(height)
}
})
else function(height)
}
It happens because the view needs more time to be inflated. So instead of calling view.width and view.height on the main thread, you should use view.post { ... } to make sure that your view has already been inflated. In Kotlin:
view.post{width}
view.post{height}
In Java you can also call getWidth() and getHeight() methods in a Runnable and pass the Runnable to view.post() method.
view.post(new Runnable() {
#Override
public void run() {
view.getWidth();
view.getHeight();
}
});
One liner if you are using RxJava & RxBindings. Similar approach without the boilerplate. This also solves the hack to suppress warnings as in the answer by Tim Autin.
RxView.layoutChanges(yourView).take(1)
.subscribe(aVoid -> {
// width and height have been calculated here
});
This is it. No need to be unsubscribe, even if never called.
Maybe this helps someone:
Create an extension function for the View class
filename: ViewExt.kt
fun View.afterLayout(what: () -> Unit) {
if(isLaidOut) {
what.invoke()
} else {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
what.invoke()
}
})
}
}
This can then be used on any view with:
view.afterLayout {
do something with view.height
}
Height and width are zero because view has not been created by the time you are requesting it's height and width .
One simplest solution is
view.post(new Runnable() {
#Override
public void run() {
view.getHeight(); //height is ready
view.getWidth(); //width is ready
}
});
This method is good as compared to other methods as it is short and crisp.
If you are using Kotlin
customView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
customView.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
else {
customView.viewTreeObserver.removeGlobalOnLayoutListener(this)
}
// Here you can get the size :)
viewWidth = customView.width
}
})
Answer with post is incorrect, because the size might not be recalculated.
Another important thing is that the view and all it ancestors must be visible. For that I use a property View.isShown.
Here is my kotlin function, that can be placed somewhere in utils:
fun View.onInitialized(onInit: () -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (isShown) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
onInit()
}
}
})
}
And the usage is:
myView.onInitialized {
Log.d(TAG, "width is: " + myView.width)
}
For Kotlin:
I have faced a production crash due to use view.height/ view.width which lead to NaN while I was using View.post() which sometimes view diemsions returned with 0 value.
So,
Use view.doOnPreDraw { // your action here} which is:
OneShotPreDrawListener so it called only one time.
Implements OnPreDrawListener which make sure view is layouted and measured
well , you can use addOnLayoutChangeListener
you can use it in onCreate in Activity or onCreateView in Fragment
#Edit
dont forget to remove it because in some cases its trigger infinite loop
myView.addOnLayoutChangeListener(object : View.OnLayoutChangeListener{
override fun onLayoutChange(
v: View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int
) {
if (v?.width > 0 && v?.height > 0){
// do something
Log.i(TAG, "view : ${view.width}")
// remove after finish
v?.removeOnLayoutChangeListener(this)
}
}
})
Cleanest way of doing this is using post method of view :
kotlin:
view.post{
var width = view.width
var height = view.height
}
Java:
view.post(new Runnable() {
#Override
public void run() {
int width = view.getWidth();
int height = view.getHeight();
}
});
Gone views returns 0 as height if app in background.
This my code (1oo% works)
fun View.postWithTreeObserver(postJob: (View, Int, Int) -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
val widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
measure(widthSpec, heightSpec)
postJob(this#postWithTreeObserver, measuredWidth, measuredHeight)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
#Suppress("DEPRECATION")
viewTreeObserver.removeGlobalOnLayoutListener(this)
} else {
viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}
})
}
We need to wait for view will be drawn. For this purpose use OnPreDrawListener. Kotlin example:
val preDrawListener = object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
view.viewTreeObserver.removeOnPreDrawListener(this)
// code which requires view size parameters
return true
}
}
view.viewTreeObserver.addOnPreDrawListener(preDrawListener)
In my case, I can't get a view's height by post or by addOnGlobalLayoutListener, it's always 0. Because my view is in a fragment, and the fragment is the second tab in MainActivity. when I open MainActivity, I enter the first tab, so the second tab doesn't show on the screen. But onGlobalLayout() or post() function still has a callback.
I get the view's height when the second fragment is visible on the screen. And this time I get the correct height.
Usage:
imageView.size { width, height ->
//your code
}
View extention:
fun <T : View> T.size(function: (Int, Int) -> Unit) {
if (isLaidOut && height != 0 && width != 0) {
function(width, height)
} else {
if (height == 0 || width == 0) {
var onLayoutChangeListener: View.OnLayoutChangeListener? = null
var onGlobalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
onGlobalLayoutListener = object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (isShown) {
removeOnLayoutChangeListener(onLayoutChangeListener)
viewTreeObserver.removeOnGlobalLayoutListener(this)
function(width, height)
}
}
}
onLayoutChangeListener = object : View.OnLayoutChangeListener {
override fun onLayoutChange(
v: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
val width = v?.width ?: 0
val height = v?.height ?: 0
if (width > 0 && height > 0) {
// remove after finish
viewTreeObserver.removeOnGlobalLayoutListener(onGlobalLayoutListener)
v?.removeOnLayoutChangeListener(this)
function(width, height)
}
}
}
viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener)
addOnLayoutChangeListener(onLayoutChangeListener)
} else {
function(width, height)
}
}
}
public final class ViewUtils {
public interface ViewUtilsListener {
void onDrawCompleted();
}
private ViewUtils() {
}
public static void onDraw(View view, ViewUtilsListener listener) {
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
if (view.getHeight() != 0 && view.getWidth() != 0) {
if (listener != null) {
listener.onDrawCompleted();
}
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
});
}
}
you can use like this ;
ViewUtils.onDraw(view, new ViewUtils.ViewUtilsListener() {
#Override
public void onDrawCompleted() {
int width = view.getWidth();
int height = view.getHeight();
}
});
private val getWidth: Int
get() {
if (Build.VERSION.SDK_INT >= 30) {
val windowMetrics =windowManager.currentWindowMetrics
val bounds = windowMetrics.bounds
var adWidthPixels = View.width.toFloat()
if (adWidthPixels == 0f) {
adWidthPixels = bounds.width().toFloat()
}
val density = resources.displayMetrics.density
val adWidth = (adWidthPixels / density).toInt()
return adWidth
} else {
val display = windowManager.defaultDisplay
val outMetrics = DisplayMetrics()
display.getMetrics(outMetrics)
val density = outMetrics.density
var adWidthPixels = View.width.toFloat()
if (adWidthPixels == 0f) {
adWidthPixels = outMetrics.widthPixels.toFloat()
}
val adWidth = (adWidthPixels / density).toInt()
return adWidth
}
}
replace (View) with the view you want to measure
This is a little old, but was having trouble with this myself (needing to animate objects in a fragment when it is created). This solution worked for me, I believe it is self explanatory.
class YourFragment: Fragment() {
var width = 0
var height = 0
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val root = inflater.inflate(R.layout.fragment_winner_splash, container, false)
container?.width.let {
if (it != null) {
width = it
}
}
container?.height.let {
if (it != null) {
height = it
}
}
return root
}
If you're worried about overworking the onDraw method, you can always set the dimension as null during construction and then only set the dimension inside of onDraw if it's null.
That way you're not really doing any work inside onDraw
class myView(context:Context,attr:AttributeSet?):View(context,attr){
var height:Float?=null
override fun onDraw(canvas:Canvas){
if (height==null){height=this.height.toFloat()}
}
}

Categories

Resources