Android: Synchronized scrolling of two different views - android

I have a tricky problem related to synchronized scrolling of two
different views.
I've made my own custom grid view widget, which has "sticky" views to
the left and top that only in one direction with the grid. Think of a
calendar where you have times at the top, dates at the left, and when
you scroll horizontally through time, the date view should stay put,
and when you scroll vertically through the dates, the time view should
stay put.
The grid itself is implemented using a nested horizontal scrollview in
a vertical scrollview. The grid is working great, so no problem there.
Since the sticky views are not in the actual grid, I have overriden
onScrollChanged in the grid scrollviews and programatically call
scrollTo on the sticky views when the user scrolls the grid.
That works as expected, except that there is a slight time offset as
to when the two different views start scrolling and ends scrolling. It
makes sense when you consider that the scrolling likely is executed
linearly on the UI thread I suppose..
All the views are scroll views, and I have enabled smooth scrolling
and used smoothScrollTo, etc, instead to try to improve this, but it's
the same problem nonetheless. The problem is especially noticeable on
larger screens, such as the Samsung Galaxy Tab, whereas it's hardly
noticeable on small-medium screen devices.
Any help is appreciated! If there is an easy fix, great..if it means
new design (that meets the sticky view usecase above), then so be it.
Code to trigger prog. scroll, same for horizontal
#Override
protected void onScrollChanged(int x, int y, int oldx, int oldy) {
mListener.onScrollY(y);
super.onScrollChanged(x, y, oldx, oldy);
}
// which leads to,
// Handle vertical scroll
public void onScrollY(final int y) {
mCurrentY = y;
mVerticalScroll.smoothScrollTo(0, y);
}
XML layouts below, if that's of any help
The actual grid, which is a horizontal scroll view wrapped in a vertical scroll view and the grid items are added vertically in the nested linearlayout
>
< com.....VerticalScrollView
android:id="#+id/gridscroll"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_below="#id/timescroll"
android:layout_toRightOf="#id/vertscroll"
android:layout_alignTop="#id/vertscroll"
android:layout_marginLeft="2dp" android:scrollbars="none"
android:fadingEdge="none">
< com....HorizScrollView
android:id="#+id/horizscroll"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scrollbars="none"
android:fadingEdge="none">
< LinearLayout android:id="#+id/grid"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
< /LinearLayout>
< /com.....HorizScrollView>
< /com.....VerticalScrollView>
The horizontal sticky view
< com.....GridTimeScrollView
android:id="#+id/timescroller"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scrollbars="none"
android:fadingEdge="none">
< LinearLayout android:id="#+id/timelist"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal" />
< /com.....GridTimeScrollView>
The vertical sticky view
< com....GridVertListScrollView
android:id="#+id/vertscroller"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scrollbars="none"
android:fadingEdge="none">
< LinearLayout
android:id="#+id/vertitemlist"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" />
< /com.....GridVertListScrollView>

First of all, I think you should be aware of this: ScrollView Inside ScrollView
In short: using scrollviews inside scrollviews is a bad thing that breaks many optimizations.
Now, onto your question.
I've had a similar need to what you described. I ended up implementing a custom view and its onDraw method. This was in part because I was drawing something not trivial and you may not have to do it.
Anyway, I believe that your best option is:
Implement a custom view that extends relative layout
create the layout of this view with the top, left and "main" views that will be the scrollable components
add a OnGestureListener to this view and pass touch events in your activity into the custom view
when your gesture listener detects a fling or a scroll, invoke scrollBy in each of the scrolling views. When you do this, if you want the top view to scroll horizontally only, pass 0 as the vertical scroll distance.
In order to implement smooth fling movements, you need to create a scroller (in your custom view). When the gesture listener detects a fling event, set the scroller up. Then, override your custom view's computeScroll() method and update the scroll in each of child views. Check this example to know how to implement it. I apologize, I will try to post a better example when possible. Check my code below... it's simpler :)
Update: sample code
#Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
if (!scrolledLastFrame) {
lastX = scroller.getStartX();
lastY = scroller.getStartY();
}
int dx = scroller.getCurrX() - lastX;
int dy = scroller.getCurrY() - lastY;
lastX = scroller.getCurrX();
lastY = scroller.getCurrY();
doScroll(dx, dy);
scrolledLastFrame = true;
} else {
scrolledLastFrame = false;
}
}

Don't use smoothScrollTo, use scrollTo instead. smoothScrollTo will animate a scroll from its current position to the position you want, because you want them to be synced, you want the other scrollview to be instantly exactly where the other scrollview is, scrollTo does that.

Looking at the answers. Why don't you handle it with a OnTouch Listener. When the touch event is MOVE you call.
public void onScrollY(final int y) {
mCurrentY = y;
mVerticalScroll.smoothScrollTo(0, y);
}
with the value of getVerticalScroll on your other ScrollView. That's pretty easy. And return true so TouchEvents will be handled further.
Here's what it could look like
#Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
lv2.onScrollY(lv.getScrollY());
break;
default:
break;
}
return false;
}
Simple right?
Not sure if it's exactly what you wanted. But could you elaborate exactly what you're trying to achieve. Not in the sense of layout but in practice..whats the point of your UI?

Related

Android RecyclerView add horizontal gape in beginning and in ending

I am trying to replicate a view from iOS so that user have same look and feel throughout the android application as well.
I am having a RecyclerView with LinearLayoutManager and horizontal orientation. So far so good.
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
tools:listitem="#layout/item_recycler_view" />
With the output design:
However, in case of iOS design we have items starting from center however the horizontal view is completely scrollable (meaning the scrolling can be done to full width even if the item loading from center).
I know there is no use of adding padding/margin or using a different view like HorizontalScrollView. How can we obtain such behaviour so that i give nearly same experience to users.
Let me know if there is anything that i can provide to clarify the problem statement.
Quick solution
Add an empty item on the beginning and one on the end of your list, and make your index access account for those two extra items. That should help you get the desired effect.
Not so quick solution
Android allows us to write our own custom Layout Managers for RecyclerView. It comes with three types that will cover most of the user cases:
LinearLayoutManger (For lists in general);
GridLayoutManager (For grids);
StaggeredGridLayoutManager (For grids with items with custom sizes).
I believe you could write one to always start placing the first item on the center of the screen. That will require more work, but it won't mess with your data indexes.
Read this, and this, on how to create custom Layout Managers. Also, take a look at the docs. That should be a good place to start.
There are two ways you could do this. The simplest by far would be to add horizontal padding to your RecyclerView and set the view to not clip based on padding. Something like this:
android:paddingLeft="100dp"
android:paddingRight="100dp"
android:clipToPadding="false"
The other way would be to create an ItemDecoration and add it to your RecyclerView. You could then override the getItemOffsets() method to add a left-hand offset to your first item and a right-hand offset to your last item.
This second approach is better because it won't affect the RecyclerView's scrollbars, but it is a little more complex. Here's an example to get you started:
private static class MyItemDecoration extends RecyclerView.ItemDecoration {
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int parentWidth = parent.getWidth();
int childWidth = view.getWidth();
int margin = (parentWidth - childWidth) / 2;
int position = parent.getChildAdapterPosition(view);
outRect.left = position == 0 ? margin : 0;
outRect.right = position == (parent.getAdapter().getItemCount() - 1) ? margin : 0;
}
}
I think the only way will be adding different layout for first and last position in adapter of recyclerview.
It can be done using viewType parameter in createViewHolder(ViewGroup parent, int viewType)

Create a scrollable LinearLayout without a ScrollView

I spent a good part of a day looking into scrolling content in Android. However, in my app I need to apply a custom type of scrolling that contains among other things a listview. A listview has its own scrolling and a scrollview should never be used with a list view together.
But there must be some intrinsic scroll functionality for views. The documentation on View does indicate support but the methods provided do not seem to provide any listeners to detect scrolling.
A scrollview also is built on a FrameLayout which would also be problematic in my app.
You can make a LinearLayout scrollable with:
android:isScrollContainer="true"
but this doesn't seem to do anything.
In fact you can even set the scrollbars for a LinearLayout:
android:scrollbars="vertical"
but that also doesn't seem to do anything.
I don't want to control scrolling but rather have a listener that detects scrolling on the screen regardless whether a listview, framelayout or any other control is visible. I guess what I am looking for is writing my own custom ScrollView control but without the limitations of its FrameLayout and all the unnecessary overhead that I don't need. Any suggestions?
On way to resolve above problem is to have 2 ScrollView, one is the outer ScrollView that contain all the view and other inner Scrollview instead of listview and adding the items inside LinearLayout dynamically(Inside inner ScrollView)
Actually after doing some research, I come up with a solution for this problem:
At first I want to explain the problem in a very simple way.
1.LinearLayout will be scrollable.
2.To do this we can use ScrollView but sometimes we need to use ListView inside LinearLayout.
3.We know that inside ScrollView we cannot use another scrollview like ListView
How to solve that?
ListView is scrollable inherently so we can add header and footer in the ListView. As a conclusion:
1.Create Layout header.xml and footer.xml and list.xml
2.Find the ListView reference from list.xml in the main activity and dynamically add header and footer in the ListView reference.
Here is what seems like a viable solution which I tested out but not thoroughly. Create a custom control that extends a LinearLayout and then override the dispatchTouchEvent:
#Override
public boolean dispatchTouchEvent(MotionEvent event)
{
MotionEvent vtev = MotionEvent.obtain(event);
final int actionMasked = event.getActionMasked();
float x = event.getRawX();
float y = event.getRawY();
switch (actionMasked)
{
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
int z = 0;
z++;
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_POINTER_DOWN:
{
final int index = event.getActionIndex();
break;
}
case MotionEvent.ACTION_POINTER_UP:
break;
}
boolean ret = super.dispatchTouchEvent(event);
return ret;
}
}
You can then place child controls inside this custom LinearLayout and detect motion events, even when ListViews and ScrollViews are present. This doesn't mess with their own scrolling.

How to change a header TextView Text when during scrolling of Horizontal scrollView?

I have a horizontal scroll view, I also have a relativelayout as it's child. I am adding child views of this relativelayout dynamically. I have a header text which should be update when I scroll according to respective child views. How can I do this because I am able to get the current focused item in horizontal scroll. Please give me some suggestion or examples which can be helpful for me, thanks..
You should specify an OnTouchListener for your HorizontalScrollView and in it's onTouch() method detect the type of MotionEvent and change your TextView's color to the appropriate
If you are creating this childs dinamically you can set a tag to them with the content you want to show in the header TextView.
//Creating RelativeLayout childs
TextView newChild = new TextView(this).
newChild.setTag(textToShowWhenThisItemIsFocused);
Then if you know which item is focused you just have to get the tag.
// "selected" is the focused view
header.setText((String) selected.getTag());
When to use the second code depends on your implementation. Since you didn't provide any code it's hard to know how to monitor the scroll, but i.e. you could control Touch Events and update the header when the user is moving his finger over the screen (you should also take into account the inertia after the user stops tapping).
EDIT: How to get the focused View
First of all, I barely have experience doing things like this, so I'm not sure if this is going to work or if there are better ways to do it. I'll just tell you the way I would approach this problem.
To be able to know the focused View you need to know the coordinates where this View should be. Since it's an horizontal ScrollView we will need the X coordinate. Since we want the View in the middle of the ScrollView I would do it like this:
private int centerSV;
private ScrollView mScrollView;
...
centerSV = mScrollView.getWidth()/2;
Now we have the center of the ScrollView. Now we need to know which child is in this position:
private int getFocusedChildId(){
for(int i=0; i<mChilds.length; i++){
int childLeftCoord = mChilds[i].getLeft() - mScrollView.getScrollX();
if(childLeftCoord <= centerSV && centerSV <= childLeftCoord + mChilds[i].getWidth())
return mChilds[i].getId();
}
// No view found in the center, maybe ScrollView wasn't full. Return the first one
return mChilds[0].getId();
}
Again, I'm not sure if this is going to work, it's just an idea of how to approach your issue. Also, you should take this into account:
getWidth() and getHeight() of View returns 0

Scrolling in Horizontal ScrollView loses focus of focused element,

Before Scrolling:
During Scrolling:
What I expect during scrolling:
I have a problem with HorizontalScrollView. When I choose an element from this View, I set focus to that element by calling:
else if (v.getParent() == candidatesScrollView.getChildAt(0))
{
Button candidateButton = (Button) v;
v.requestFocusFromTouch();
v.setSelected(true);
(...)
}
After that, when I scroll the list without choosing other element, I lose focus of previously selected element. I made some research about this topic, but there was no solution that could work for me... How can I scroll my HorizontalScrollList without loosing focus from selected element? Any Help is Appreciated. It has been about 14 days since I asked that question and still didn't find solution. Please help.
Here is part of my XML:
<HorizontalScrollView
android:id="#+id/CandidatesHorizontalScrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="#+id/linearLayout2"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"
android:visibility="gone" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"
android:orientation="horizontal" >
<Button
android:id="#+id/horizontalscrollview1_button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
android:textSize="25sp" />
(...)
// 11 more buttons
</LinearLayout>
</HorizontalScrollView>
UPDATE
MY CURRENT SOLUTION #1 (not working correctly):
After scrolling, and then scrolling again (for example scrolling back), scrolling starts from selected element.
I created custom HorizontalScrollView class inside which I overridden onTouchEvent() method. I don't think this is optimal way of doing that, because in that case I have to do calculations every time I move even one pixel. for example, if I add toast.show() to the below method, it will try to show as many toast as many I moved pixels (If I move by 10 pixels, it will try to show 10 Toast). Anyway, it works for me and the selection and focus are being kept. Please help me modify this code to make finally a good answer for that known issue:
#Override
public boolean onTouchEvent(MotionEvent ev)
{
int i = 0;
Button button = null;
for (; i < 11; i++)
{
button = (Button)((LinearLayout)getChildAt(0)).getChildAt(i);
if(button.isSelected())
break;
}
super.onTouchEvent(ev);
button.setSelected(true);
button.requestFocusFromTouch();
return true;
}
To be sure that the above code will work, you need to have only one selected item in your HorizontalScrollView at a time, i.e when you press diferent button, you need to make the previous one setSelected(false)
MY CURRENT SOLUTION #2 (not working correctly):
Solution #2 that I tried to implement, thinking that first one is not elegant enough, involves usage of gesture detector. In my custom HorizontalListView class I have added the following code:
Constructor:
public MyHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
gestureDetector = new GestureDetector(context, new MyHorizontalScrollViewGestureDetector());
this.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
gestureDetector.onTouchEvent(event);
return true;
}
});
// TODO Auto-generated constructor stub
}
MyHorizontalScrollViewGestureDetector internal class:
public class MyHorizontalScrollViewGestureDetector extends SimpleOnGestureListener
{
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
{
//Here code similar like that one in solution #1
//But the View is not scrolling, even without that code
super.onScroll(e1, e2, distanceX, distanceY);
return true;
}
}
However, the list is not scrolling with that solution. I can add to onScroll method:
ScrollBy((int)positionX, (int)positionY);
which makes the list will scroll, but not in a good way ad it will freeze sometimes.
I am wondering why scrolling is not called by the super. method.
MY CURRENT SOLUTION #3 (working, but it is walk-around):
Because both solution 1 and 2 were not working, I decided to not play with focus anymore.
What I do now, is to change the Button Drawable whenever I click it and every time when I change to different Button. I use same Drawable as is used for focused button (Holo). In that case, I don't have to be worried about scrolling in HorizontalScrollView. This solution is some kind of walk-around, so I am looking forward to any comments, suggestions and edits.
Can the following solution be applicable (I took the idea from here):
You may want to try creating your own custom class that extends HorizontalScrollView and overriding the onScrollChanged() function as such:
public class TestHorizontalScrollView extends HorizontalScrollView {
public TestHorizontalScrollView(Context context) {
super(context);
}
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
// TODO Auto-generated method stub
Log.i("Scrolling", "X from ["+oldl+"] to ["+l+"]");
Button button = null;
for (; i < 11; i++)
{
button = (Button)((LinearLayout)getChildAt(0)).getChildAt(i);
if(button.isSelected())
break;
}
button.setSelected(true);
button.requestFocusFromTouch();
super.onScrollChanged(l, t, oldl, oldt);
}
}
Now that you have detected the scroll (we're speaking about one of the HorizontalScrollView) you can set again the selected status of the corresponding button. Can you please try this solution out and see if it works. I'm very interested in the resolution of this one, as the question is quite interesting also.
Anyway, I managed to find the following article. They state there that:
Imagine a simple application, ApiDemos for example, that shows a list
of text items. The user can freely navigate through the list using the
trackball and they can also scroll and fling the list using their
finger. The issue in this scenario is the selection. If I select an
item at the top of the list and then fling the list towards the
bottom, what should happen to the selection? Should it remain on the
item and scroll off the screen? In this case, what would happen if I
then decide to move the selection with the trackball? Or worse, if I
press the trackball to act upon the currently selected item, which is
not shown on screen anymore. After careful considerations, we decided
to remove the selection altogether.
In touch mode, there is no focus and no selection. Any selected item
in a list of in a grid becomes unselected as soon as the user enters
touch mode. Similarly, any focused widgets become unfocused when the
user enters touch mode. The image below illustrates what happens when
the user touches a list after selecting an item with the trackball.
Anyway, the statements there didn't make me happy and I went further to find this:
android:focusable="true"
android:focusableInTouchMode="true"
Also set android:clickable="true" (you might as well set android:focusable="true" of the LinearLayout).
Try adding those to the buttons and to the Layout that contains them.
Try everything.
I'l try backing you up as much as I can.
Cheers
I remember having a problem with scroll views that I think was similar to the problem you're having.
The solution I came up with was to override the onRequestFocusInDescendants method in the HorizontalScrollView class. This requires you creating your own class extended from HorizontalScrollView if you aren't already doing so.
In my case, I always returned true from the method. This tells the caller that you have taken care of the focus change so it shouldn't try to do anything further.
#Override
protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
return true;
}
Depending on your requirements, you may find it necessary to return true only under certain conditions (say when a scroll is in progress), otherwise forward the call to the superclass.
This question has no answer except for custom way of doing it, as you might have already done like changing the drawables. Reason is when you slide, the focus goes to the HorizontalScroll, and you can only focus on one item, it makes no sense to focus more than one view. So either implement a drawable (which you seem to have done) or extend a checkbox and override the functionality so when it is focused it is checked and changes the looks.
I have found a solution to my problem. You can see the updated question for details. Basically, instead of using focus I decided to use Drawable selector, which is much more easier.

Scrolling issues with a ListView inside a ScrollView

Here's the scenario conceptually (excluding linearlayouts)
ScrollView
Button
Checkboxes
Spinner
ListView (full-size, non-scrolling)
AdMob advert
i.e. a scrolling pane, which has a filtering UI at the top, followed by results, but the advert must always remain visible, and when scrolling down the filter UI must scroll away, leaving maximum space for results.
I'm aware there are issues with a ListView inside a ScrollView, though for me it is working well in many ways (I'm fixing the length of the ListView to stop it collapsing). So the screen scrolls nicely, the ad stays put at the bottom, and it looks good.
But the problem I'm seeing is, inexplicably, when the activity opens, the ScrollView is scrolled down a bit, so the ListView is at the top of the screen. I assume this is a default behaviour, so I set about trying to force the scroll position of the ScrollView to the top, but I've tried various methods, and see no effect:
scrollview.scrollTo(0, 1000/-1000);
scrollview.smoothScrollBy(0, 1000/-1000);
scrollview.fullScroll(View.FOCUS_UP);
Is there any way to force the ScrollView to start with the scroll position at the top?
If not, how can I have an ad that doesn't scroll off the bottom, but a filter UI that always scrolls off the top? Using ListView seems overkill as I don't need scrolling but it does provide many benefits so would be nice to avoid starting from scratch and rendering everything myself.
Use the following method and enjoy!
private void setListViewScrollable(final ListView list) {
list.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
listViewTouchAction = event.getAction();
if (listViewTouchAction == MotionEvent.ACTION_MOVE)
{
list.scrollBy(0, 1);
}
return false;
}
});
list.setOnScrollListener(new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view,
int scrollState) {
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (listViewTouchAction == MotionEvent.ACTION_MOVE)
{
list.scrollBy(0, -1);
}
}
});
}
listViewTouchAction is a global integer value.
If you can replace the line
list.scrollBy(0, 1);
with something else please share it with us.
Why are you using a listview if you're not scrolling? Why can't you just use a linearlayout or something more fit to this situation? You mention a filter, you could very easily roll your own filter especially since apparently you just have a few items in your listview.
Use something other that a ListView, you can dinamically generate linearLayouts to show the data you want. You should never use a listview inside a scrollView, it doesnt work for a simple reason, when you scroll, what should scroll, the listview or the scroll view. A couple of people from google have stated not to do this.
I've experienced the issue of a ScrollView starting off scrolled down slightly, the solution was to post a runnable which called the animateTo(0,0) method to get the list to scroll to the top. I found this only worked using anitmatTo(0,0) scrollTo(0,0) didn't seem to work.
Something along the lines of:
mListView.post(new Runnable(){ public void run() { mListView.animateScrollTo(0,0) });
Now as everyone has already stated you shouldn't do the whole ListView inside a ScrollView, but this may be a fix for the problem you had.
I have:
ScrollView
TextView
Button
TextView
ListView
and this work good for me:
scrollView.smoothScrollBy(0, 0);
without this view start from position of listview, after that it start from top
The solution for this issue is to make a request focus to an object in the top of the ScrollView. For example you can use a table layout wrapping the button and request focus to the table layout (If you focus the button it will change its color).
// onCreate method
TableLayout tablelayout = (TableLayout) findViewById(R.id.tablelayout);
tablelayout.setFocusableInTouchMode(true);
tablelayout.requestFocus();

Categories

Resources