I have recently been trying to implement a calendar day view (see picture) with items laying on top of a recycler view
Example of part of the calendar view
Currently I calculate the offset the item needs to have from the top of the recycler view with this bit of code
val dpPerMinute = 24f / 15f
val viewOffset = 12f
initialOffset = (startTime * dpPerMinute + viewOffset + 4f).dpToPx().toInt()
The background item height is 24dp and is equal to 15 minutes, viewOffset is just to offset the view to the center of the itemHeight and the 4 comes from padding
Whenever I scroll in the view I update the position of the item with this
val pos = -offset + initialOffset
view?.translationY = pos.toFloat()
Offset is the recycler views current scroll Y and initialOffset is calculated above
This works great on my Samsung, everything lines up perfectly. But then comes the problem, I tested it on another work phone (as we do, a OnePlus Nord) and now everything isn't lining up right. The offset becomes more off the further down it is.
Could this be a problem with converting dp to pixels on different phones? Has anyone else encountered the same problem as me with other phones and if that is the case how did you solve it?
The function converting dp to px looks like following:
#JvmOverloads #Dimension(unit = Dimension.PX) fun Number.dpToPx(
metrics: DisplayMetrics = Resources.getSystem().displayMetrics
): Float {
return toFloat() * metrics.density
}
In case anyone else has a similar problem, problem was dpToPx() not rounding the number correctly. I simply just rounded the converted number after it was calculated, solved the problem
Related
I want to have the total scroll position in px in LazyColumn.
The column itself has a ScrollState which has a value (Described in the document as the current scroll position value in pixels), But LazyColumn ScrollState hasn't this field, I want the exact equivalent of Column ScrollState scroll position value for LazyColumn, How I can achieve that with the help of LazyScrollState?
The reason it exists in Column is because it knows about all the items (rows) when it's being generated.
But LazyColumn only displays a portion of the total views at any given moment, and when you scroll, it continuously re-generates the views on the screen (plus a couple above and/or below the visible area), so it never actually calculates the total height of all the rows. If you wanted to get the scroll position, you would have to manually calculate it. If the height of each row is the same, it'll work pretty well. But if the heights are different, then you won't be able to get the exact scroll position (your calculations will fluctuate depending on the height of the rows that are currently displayed). But here's what you have to do to calculate it yourself:
Calculate the total size of all the items
val totalItems = lazyListState.layoutInfo.totalItemsCount
val itemLengthInPx = lazyListState.layoutInfo.visibleItemsInfo.firstOrNull()?.size ?: 0
val totalLengthInPx = totalItems * itemLengthInPx
Calculate the current scroll position
val scrollPos = (lazyListState.firstVisibleItemIndex * itemLengthInPx) / totalLengthInPx
But as I mentioned earlier, this calculation depends on the height of each item (itemLengthInPx), and it works perfectly if it's the same for all the views. But you can see how it'll fluctuate if each view has a different height
The scroll amount can be obtained, no calculation required, in the items themselves, by attaching an onGloballyPosition{ it.positionInParent()} modifier to one or more items.
Then, the items can do what they need to do with their own scroll position, such as offsetting some screen-drawing y coordinate.
Or, if you need the scroll offset in the parent LazyColumn, you could have one item (perhaps an item with zero height, though I've not tested that) at the very top of your list of items, that reports back its position to the parent (perhaps by updating a mutableState that was passed to the item by the parent) whenever it moves.
I had the same need and onGloballyPosition{ it.positionInParent()} addressed it very nicely.
You can get it by firstVisibleItemScrollOffset.
('androidx.activity:activity-compose:1.3.1')
val listState = rememberLazyListState()
val itemHeight = with(LocalDensity.current) { 80.dp.toPx() } // Your item height
val scrollPos = listState.firstVisibleItemIndex * itemHeight + listState.firstVisibleItemScrollOffset
Text(text = "scroll: $scrollPos")
LazyColumn(state = listState) {
// Your items here
}
Also, you can set the scroll position to listState, like this:
LaunchedEffect(key1 = "Key") {
delay(1000) // To show scroll explicitly, set the delay
listState.scrollBy(itemHeight * 2)
}
I'm using a horizontal RecyclerView with PagerSnapHelper to make it look like a ViewPager. It's displaying CardViews.
My question is, how can I make it show a small peek of the edge of the next and previous card? The user needs to see a little of those cards so they understand intuitively that they need to swipe horizontally so they can view other cards.
I'll also note that the current card always needs to be centered, even for the first one, which would not have a previous card peeking on the left. Also, the design requirements are out of my control; I need to peek, I can't use dot indicators or anything else.
I could use a LinearSnapHelper and make the width of the cards smaller, but then 1) the first item will be left-aligned instead of centered, since there's no card peeking on the left side and 2) how much of each card displays would vary based on the width of the phone.
This seems like it should be a common and simple task, so I hope I'm missing something obvious to make it happen.
After some trial and error, I found a solution. Fortunately, it wasn't as complicated as I thought, although not as clean as I hoped.
So start with a RecyclerView and its Adapter, use a LinearLayoutManager with a Horizontal orientation, add in a PagerSnapHelper and so on... then to fix the specific issue in this question I made some adjustments to the adapter:
private var orientation: Int? = null
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
orientation = (recyclerView.layoutManager as LinearLayoutManager).orientation
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// Kludge to adjust margins for horizontal, ViewPager style RecyclerView
if (orientation != LinearLayout.VERTICAL) {
holder.itemView.layoutParams = (holder.itemView.layoutParams as RecyclerView.LayoutParams).apply {
val displayMetrics = DisplayMetrics()
baseActivity.windowManager.defaultDisplay.getMetrics(displayMetrics)
// To show the edge of the next/previous card on the screen, we'll adjust the width of our MATCH_PARENT card to make
// it just slightly smaller than the screen. That way, no matter the size of the screen, the card will fill most of
// it and show a hint of the next cards.
val widthSubtraction = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 40f, displayMetrics).toInt()
width = displayMetrics.widthPixels - widthSubtraction
// We always want the spot card centered. But the RecyclerView will left-align the first card and right-align the
// last card, since there's no card peeking on that size. We'll adjust the margins in those two places to pad it out
// so those cards appear centered.
// Theoretically we SHOULD be able to just use half of the amount we shrank the card by, but for some reason that's
// not quite right, so I'm adding a fudge factor developed via trial and error to make it look better.
val fudgeFactor = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6f, displayMetrics).toInt()
val endAdjustment = (widthSubtraction / 2) - fudgeFactor
marginStart = if (position == 0) endAdjustment else 0
marginEnd = if (position == (itemCount - 1)) endAdjustment else 0
}
}
}
Of course, you'll want to change 40f and 6f to the appropriate values for your use case.
In case you're wondering, I have the orientation check because in my case, the same adapter is used for two different RecyclerViews. One is a simple vertical list. The other is horizontal and functions like a ViewPager, which greatly increased the complexity.
I'm having some difficulty in getting the height of a view. Currently, upon a cold start of the app, getMeasuredHeight() on the view returns 0.
Now, I know of ways to get around this. It's occurring because the view hasn't been laid out yet. Unfortunately, using something like a addOnGlobalLayoutListener won't suffice; I need the height of this view (a toolbar + TabLayout or just a toolbar depending on the user's settings) to set the offset for a SwipeRefreshLayout using setProgressCircleOffset(). Upon a cold start of the app, the SwipeRefreshLayout is shown to be "refreshing". Anytime data is loading or changing, it's shown to be " refreshing". If I don't know the height, then the SwipeRefreshLayout won't show that it's "refreshing". I can't delay the calculation as I need it at the beginning of a cold start; not to mention it may be one of two possible heights.
So my question is this: since we can get the DPI of a device, and I can generally estimate (by trial and error) the height (in dp) of the view (and it's alternate)--is it possible to create a "formula" or something that will adapt the offset based on screen size? I know I'd need to convert the dp to px, but beyond that, I'm unsure of how to go about this.
For example, I know the height of this view is 334px on my Nexus 6 (set to 513dpi); but it's definitely less on my stock Pixel C (the offset of 334px is too much on a Pixel C).
So I ended up solving this thanks to AMAN77.
I got the height of the device display (total height in pixels) then multiplied that by a percentage (that I got by trial and error) for the offset.
Here is what I did:
int screenHeight = getContext().getResources().getDisplayMetrics().heightPixels;
int headerOffset = Math.round((float) (screenHeight * 0.13));
//if the user has the "toolbar" setting enabled, we need a different offset
if (SettingValues.single) {
headerOffset = Math.round((float) (screenHeight * 0.07));
}
mSwipeRefreshLayout.setProgressViewOffset(false,
headerOffset - Reddit.pxToDp(42, getContext()),
headerOffset + Reddit.pxToDp(42, getContext()));
Worked like a charm!
from an answer to one of my other questions I found an Google Demo of a ListView subclass that allows item reorder.
The demo works great, but I am having some trouble to understand how the it works:
When an item is dragged above/below the bounds of the ListView, the ListView starts scrolling up/down to reveal new items. The necessary calculation uses different parameters of the underling ScrollView:
public boolean handleMobileCellScroll(Rect r) {
int offset = computeVerticalScrollOffset();
int height = getHeight();
int extent = computeVerticalScrollExtent();
int range = computeVerticalScrollRange();
int hoverViewTop = r.top;
int hoverHeight = r.height();
if (hoverViewTop <= 0 && offset > 0) {
smoothScrollBy(-mSmoothScrollAmountAtEdge, 0);
return true;
}
if (hoverViewTop + hoverHeight >= height && (offset + extent) < range) {
smoothScrollBy(mSmoothScrollAmountAtEdge, 0);
return true;
}
return false;
}
heightis the height of the ListView itself
offsetis the scroll position = how many units/pixels have been scrolled up/down
rangeis the height of the complete content.
extent - well, what is this?
ListView inherits computeVerticalScrollExtent() from View and the docu says:
Compute the vertical offset of the vertical scrollbar's thumb within
the horizontal range. This value is used to compute the position of
the thumb within the scrollbar's track.
If one looks at the code computeVerticalScrollExtent() is not implemented by one of the sublasses but only directly by View: It simply returns the height of the view.
This makes sense: If the ListView/ScrollView has a height of 500, the part of the scroll content that is visible at a time is also 500. Is this the meaning of the ScrollExtent? Why is ScrollExtent necessary? Why not simply use getHeight() directly?
I think I am missing something and I am happy about any hint!
compute*ScrollOffset - Defines the distance between the start of the scrollable area and the top of the current view window inside the scrollable area. So for example, if your list has 10 items and you've scrolled down so the 3rd item is at the top-most visible item, then the offset is 3 (or 3*itemHeight, see below).
compute*ScrollExtent - Defines the size of the current view window inside the scrollable area. So for example, if your list has 10 items and you can currently see 5 of those items, then the extent is 5 (or 5*itemHeight, see below).
compute*ScrollRange - Defines the size of the current scrollable area. So for example, if your list has 10 items then the range is 10 (or 10*itemHeight, see below).
Note that all these methods can return values in different units depending on their implementation, so for the examples above, I am using the indices, but in some cases these methods will return pixel values equivalent to the width or height of the items.
In particular, the LinearLayoutManager of the RecyclerView will return indices if the 'smooth scrollbar' feature is disabled, otherwise it will return pixel values. See ScrollbarHelper in the support library for more information.
Additional reading: https://groups.google.com/forum/#!topic/android-developers/Hxe-bjtQVvk
It's kinda late, but hope it's ok.
1) The method actually is implemented by the subclasses. For example, this is what it looks like for AbsListView (ListView's superclass).
2) View's height can be different from its vertical scroll's height - just imagine a View with weird top/bottom padding .
These two points pretty much make other questions irrelevant :)
This is a sample code which might help you to understand as to how to get scrollBar top and bottom using computeVerticalScrollExtent:
scrollbarTop = (float)this.computeVerticalScrollExtent()/this.computeVerticalScrollRange()*this.computeVerticalScrollOffset();
scrollbarBottom = (float)this.computeVerticalScrollExtent()/this.computeVerticalScrollRange()*(this.computeVerticalScrollExtent()+this.computeVerticalScrollOffset());
According to the article from here:
I found this explanation correct.
ListView with 30 items has scrollRange equals to 3000, that is due to scrollRange = numberOfItems * 100, thus scrollExtent = numberOfVisibleItems * 100 and scrollOffset = numberOfScrolledItems * 100. You can find evidance of these words in the source code of AbsListView
Does anyone know of a way to center a ListView based on its current selection or selection set with setSelection?
I did see this other StackOverflow question without any answers: Android ListView center selection
Thanks,
Kevin
First, get the height of the ListView using getHeight, which returns the height of the ListView in pixels.
Then, get the height of the row's View using the same method.
Then, use setSelectionFromTop and pass in half of the ListView's height minus half of the row's height.
Something like:
int h1 = mListView.getHeight();
int h2 = v.getHeight();
mListView.setSelectionFromTop(position, h1/2 - h2/2);
Or, instead of doing the math, you might just pick a constant for the offset from the top, but I would think it might be more fragile on different devices since the second argument for setSelectionFromTop appears to be in pixels rather than device independent pixels.
I haven't tested this code, but it should work as long as your rows are all roughly the same height.
You will need to have the scroll view and the view of the item selected. Then you can simply do:
scrollView.smoothScrollTo(0, selectedView.getTop() - (scrollView.getHeight() / 2) + (selectedView.getHeight() / 2), 0);
This will center the scroll view exactly on selectedView
I haven't tried any of this but based on the current selection could you use public void smoothScrollByOffset (int offset) to get the view to scroll to where you want so that your selection is in the middle of the view?