This question is very specific, What I am trying to do (with a list view) is described in great detail in the following article: http://www.pushing-pixels.org/2011/07/18/android-tips-and-tricks-synchronized-scrolling.html
Thanks #kaushal trivedi for the link
Details:
I have an android application I am working on that uses a list view with a custom adapter. The Listview Contains a Custom header of a non-fixed height. Also please note that the list items are also of variable height. My goal is to mimic the effect produced in the latest gmail app (as an example) where when you are viewing an email, and scroll past the header, it sticks to the top of the screen just under the action bar and the content continues to scroll under it. What I would like to do, is stick the bottom half of my header to the top of the screen.
My initial reasoning was to create an invisible view fixed in the desired location, and when the user scrolled to or past that location, make the view visible. The issue in this logic, is I need the exact pixel scroll height, which after many attempts I have determined very difficult to do. The exact issue I ran into is, it is not possible from what I can gather to retrieve the pixel level Y-scroll in an onScroll event, I have only been able to retrieve the value in the onScrollStateChanged event. Which as described above will not achieve the desired functionality.
Working with the onScroll event "int firstVisibleItem, int visibleItemCount, int totalItemCount" parameters is also not an option because of the fact that the content I want to "stick" is not the size of a list item, but a fraction of the size of the variable height header.
Is there a correct way to accomplish this effect? My current minSDK level is 10.
Update 10/10/13
I made some progress. The following code syncs the Y position floating view I have on the screen with the list view. b is the view I am setting just as an example.
NOTE: This is used in the onScroll event of the list view.
View c = view.getChildAt(0);
if (c != null) {
int currY = c.getTop();
int diffY = currY - lastY;
lastY = currY;
b.setTop(b.getTop() + diffY);
}
Now the issue is, the header of my List is a non fixed height as I said earlier. So I need to get the height of the header and apply an offset to "b" to place it at the bottom of the list header floating above the list.
This is the code I've tried so far.
header.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
The issue here us header.getMeasuredHeight(); always resolves to the same value no matter how tall the actual height is.
I understand I cannot get the height until after it is displayed. Is there a way I can get that value and set the offset after it is rendered?
Update 10/11/13
I Answered my last question as soon as I woke up this morning.
While the View.measure() code was returning a height. It appears to be the default height of the view, assuming there was no text (that would ultimately stretch the view). So I used the below event to listen for when the view is displayed, and then record its actual height (which works exactly as I had hoped :) )
ViewTreeObserver vto = header.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
b.setY(header.getMeasuredHeight() - 80); //80 is a temp offset
}
});
I have to go to work soon and being that I have still not fully achieved the desired effect, I will not mark this as answered yet. Hopefully I will be able to sit down and finish this in the next day or two. I am still open to suggestions on better ways of doing this.
Okay, so after a lot of time and research, I have found an answer to my question.
First off, Thank you #kaushal for this link: http://www.pushing-pixels.org/2011/07/18/android-tips-and-tricks-synchronized-scrolling.html
My solution ended up being somewhat complex. So instead of trying to describe it here, I made an example app and posted it here: https://github.com/gh123man/Partial-Header-ListView-Scroll-Sync
The specific file containing the code for the solution is here: https://github.com/gh123man/Partial-Header-ListView-Scroll-Sync/blob/master/src/com/example/partialheaderlistviewscrollsync/MainActivity.java
Related
It seems that every angle I manage to find doesn't end up working in the way I need it to. My goal is to be able to customize the positioning and size of any scrollbar on any view, be it a recyclerview, gridview, or listview. I've tried using layer-list xmls to adjust the height and positioning, a Seekbar turned vertically, as well as trying to create my own scrollbar thumb and track using imageviews.
In terms of the layer-list, it just didn't have an effect on the scrollbar at all. The other two attempts at a solution (using a Seekbar, using individual imageviews) were nearly effective, except I needed the current scrolled position (getScrollY()) to be able to make the scrollbars I made actually accurate instead of just visually being a scrollbar. However, even though getScrollY() is defined for recyclerview, gridview and more, it always returns a 0, so I am unable to get that information (except for scrollviews, perhaps; I believe that's the only view type that properly returns a getScrollY() value).
Is it even possible to customize the scrollbar in this manner? I'd be keen to see references or documentation that can point me in the right direction. It feels like this is generally a non-issue for most developers on Android, or at least in general isn't something many people have asked for.
Edit
To assist in visualizing what I have and what I desire, here's a screenshot of the scrollbar as it is right now:
The following image is marked up to show what my intended outcome for this scrollbar would be:
Views have the capability for a scrollbar but a lot don't show them by default.
So any View has a whole load of XML attributes to customise the appearance, size and position.
But these are useless if not shown.
A lot of ViewGroups sub classes setWillNotDraw to be true and this removes the capability to draw the built in scrollbars of the View.
So to get any view to show it's built in scrollbars you need to the setWillNotDraw(false)
Getting any View to show it's built in scrollbars is Step 1 but again not all Views Calculate automatically the length and position of scroll hence they return 0 for the scroll position.
The View has to implement the following methods and return the appropriate numbers for the scroll position to be correct and things like getScrollY to return more than 0
// Length of scrollbar track
#Override
protected int computeHorizontalScrollRange() {
return (int) value;
}
// Position from thumb from the left of view
#Override
protected int computeHorizontalScrollOffset() {
return (int) value;
}
#Override
protected int computeVerticalScrollRange() {
return (int) value;
}
#Override
protected int computeVerticalScrollOffset() {
return (int) value;
}
Off Course some View sub classes don't use the built in ones but draw there own.
I have written two ItemDecorator's for RecyclerView. Each adds some top offset in getItemOffsets(). Let's say:
First decorator adds 20dp top offset
Second decrator adds 30dp top offset
Now, when I add both of them to RecyclerView, each item is correctly offsetted by 50dp, that's good.
But here comes the question:
How do I get this offset in onDraw/onDrawOver?
Usually decorators draw their stuff by traversing parent.getChildAt(i) stuff and getting child.getTop() for example to draw above child view of RecyclerView.
But in this case, doing so would mix up the drawing of other decorator, because it would also use child.getTop().
So at the moment it seems like both decorators need to know about each other and each other's height.
Am I missing something here? I hope I am.
EDIT: I reported an issue to Android issue tracker and it seems this will be worked on. Star it to keep track of progress: https://code.google.com/p/android/issues/detail?id=195746
tl;dr No you are not missing anything. But you can get the values needed in getItemOffsets, albeit it seems a little bit dirty to me.
Basically there is only one option of getting the decorated height other than managing decorations yourself: LayoutManager.getDecoratedTop();
onDraw
In onDraw you get the whole canvas of the recyclerView, c.getClipBounds() does not hold any information. Albeit the javadoc of adding decorations says that decorations should just draw withing their bounds.
Also, getting parent.getLayoutManager().getDecoratedTop() will give you the same values in every decoration, since it's already too late here, those values are for layouting purposes.
We are too late, layouting is done and we missed it.
getItemOffsets
Please note that I tested the following with a LinearLayoutManager and it might as well not work with the others (Most definitely not with most custom ones). Since I am relying on measuring happening between those calls, which is not documented, the given solution might break with some future version.
I just felt I needed that disclaimer. I found a working solution (watch the mOffset):
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
mOffset = parent.getLayoutManager().getTopDecorationHeight(view);
outRect.set(0, 10, 0, 0);
}
This works, because the recyclerview, when calculating the total inset of the child, is updating the views package local LayoutParams variable. While we cannot access the layout parameter variable itself, calling getTopDecorationHeight actually uses the (currently) dirty inset, giving us the proper value.
Hence, you can get the offset of the previous decorations before applying your own!
To apply it, just remove the other decorations offset when drawing your decoration:
c.drawRect(child.getLeft() + child.getTranslationX(),
mOffset + layoutManager.getDecoratedTop(child) + child.getTranslationY(),
child.getRight() + child.getTranslationX(),
child.getBottom() + child.getTranslationY(), paint);
This will actually apply the other decorations offset, then draw your own from the correct top.
Some Problems
This is now a basic, working solution for the default usecase. BUT. If you have different offsets depending on the item VIEW_TYPE or the adapter position things get tricky.
You will either duplicate your logic and keep various offsets for various view types or you have to store / retrieve the offset for every view or viewtype.
To do so, you could either add some custom tag with view.setTag(key, object) or doing something similar with the RecyclerView.State object that's getting passed around.
Within Android, I'm trying to move a TextView from outside the parents bounds into view, but the contents never shows up, or remains clipped if it was partially within the bounds already.
Initial situation at start
Situation after animation
(Below this is another view, that was completely out of bounds and isn't drawn either)
I have 4 TextViews below each other in a custom Object extending RelativeLayout. Based on a percentage the top 2 should move outside it's bounds and the bottom 2 should move in (from the bottom).
I use the following code to update the properties of each TextView. In this class each variable **positionY* is filled with their initial position from the layout-xml. effect is percentage between 0 & 1. The animation works, but the views aren't drawn again.
public class ActionBarTitleView extends RelativeLayout {
public void updateTransition(float effect) {
float height = getHeight();
titleView1.setY(title1positionY - height*effect);
detailView1.setY(detail1positionY - height*effect);
titleView2.setY(title2positionY - height*effect);
detailView2.setY(detail2positionY - height*effect);
invalidate();
}
}
What I tried
After some researching I found a few hints what the issue might be, but so far none of the tried options had any effect. Below is a list of things I've found on SO and tried.
Calling invalidate() on the RelativeLayout - No effect.
Invalditing the TextViews - No effect.
clipChildren = false for the RelativeLayout - No effect.
setWillNotDraw = false for the RelativeLayout - No effect. (onDraw is being called)
I haven't tried to solve this with a ScrollView, but I don't want to really, cause that adds another layer in the hierachy for something pretty small.
I thought I understood the drawing logic, but perhaps I'm missing something, so I hope someone might be able to point me in the right direction.
What I ended up doing (September 3rd)
Since no real solution was offered, I tried again and came to the following "fix". I set both second labels to Visibility.GONE, but within the original bounds of the container view. Then when I start the animation, I set their correct values, then move them outside the bounds and finally setting Visiblity.VISIBLE. When the animation progresses the labels roll into view as supposed to. So a fix to the issue, but no real explanation why the TextViews aren't drawn again...
I want to make a UI element like a GridView, I want it's complete functionality but want it to be horizontally scrollable rather than vertically.
By horizontal scroll I mean it should be built that way and not put in a HorizontalScrollView.
My would be Custom GridView will have fixed number of rows say 4-5 and the columns should be extensible based on number of items in the Adapter. You can think of it as the opposite of what the native GridView does, yet it should maintain the functionality.
I have looked at the source code of how a GridView is implemented by Google, but I am able to understand very less and starting to make the View from scratch doesn't seem to be a good idea, since I am afraid I will not be able to do justice to memory optimization's the way Google did it.
I had observed that GridView extends AbsListView, so my question is, is it AbsListView which lets a GridView scroll vertically and add items from the adapter, or is it GridView which adds the vertical scrolling ability? Should I tweak GridView or AbsListView?
It would be even better to know if there's something which already does what I want to do?
This has already been implemented in native Gallery and YouTube app of Android Honeycomb 3.1 and above. So if anyone has an idea, please elaborate.
Snapshot of Honeycomb Gallery app:
Snapshot of Honeycomb YouTube app:
There is setRotation in API 11. You'll have to rotate the gridview by 90 degrees and child views by -90 degrees.
Documentation: http://developer.android.com/reference/android/view/View.html#setRotation(float)
Update:
To get a 3d effect on views following APIs would be useful
setCameraDistance(float) - set the z axis distance(depth)
setRotationX(float) - set the horizontal axis angle
setRotationY(float) - set the vertical axis angle
Set the camera distance to half of the screen height. Then set the rotationX based on the view's location on screen. The rotation angles should be something like (20, 10, 0, -10, -20) from left to right. Later you can play with rotationY angles to get some height perception.
Do all setting in extended GridView's overriden layout method.
#override
void layout(int t, int l, int r, int b) {
super.layout(t, l, r, b);
...
int columnStart = getFirstVisiblePosition()/no_of_columns;
int columnEnd = getLastVisiblePosition()/no_of_columns;
loop from 'columnStart' to 'columnEnd' 'no_of_colmns' times {
// set the camera distance and rotationX to views
// depending on the position of a view on screen.
}
}
OK, so I am starting to get a hang of building Android apps, well at least as much a programmer can after a few days - I am proud of what I have learned so far.
Anyways, I want to force login on the main activity - this I am doing by fetching a SharedPrefernece and than checking if that piece of information is null and than getting a PopupWindow which holds the "login" fields and options.
This PopupWindow has a Flipper inside, which is fine and I got working fine when the certain options are choosen.
I am having problems displaying this PopupWindow to just be the size of the content (wrap_content) as when I set the PopupWindow.setAtLocation()
Now, here is what I have been trying to do to get the size of the popup - as mentioned a few times on here:
popup.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
final PopupWindow pw = new PopupWindow(popup,popup.getMeasuredHeight(),popup.getMeasuredWidth(), true);
Note that popup is the inflator of the actual layout of the Popup, pw is the actual PopupWindow object.
Now, I want to get the actual size of just the popup window (so that way it isn't streched out over the page, but rather just in the center looking like a normal popup should.
Also, with the ViewFlipper. I want it to update the size of the popup when it switch pages (so the page should be sizing up and down per page) is there a way to make this work as well? I tried pw.update() but that didn't work out very well.
You want to measure the actual layout you will be adding to your PopupWindow. I just solved a similar problem of putting a ListView inside a PopupWindow. The trick is to override onMeasure() in your View. Remeasure that ViewFlipper everytime it changes.
#Override
public void onMeasure(int x, int y)
{
super.onMeasure(x, y);
if(x == View.MeasureSpec.UNSPECIFIED && y == View.MeasureSpec.UNSPECIFIED)
{
//measure your child views here
//tell the view it has been measured
this.setMeasuredDimension(width, height);
}
}