Scrollviews synchronisation issue - android

I wrote a small app displaying 2 scrollViews next to each other. I need the scroll position of the 2 scrollViews to be synchronised. For that, I extended ScrollView and I overrode onScrollChanged to be notified when a scroll occurs, and then to sync the 2 scrollViews.
My two scroll views display a bunch of blue Views. The left scrollView has a red background, and the right has a green background.
Here is what happens with a scroll on the left scrollView:
=> The synchronisation is ok
And here is what happens with a scroll on the right scrollView:
=> The synchronisation is not good, there's a gap
(both screenshots were taken during the scrollviews' fling)
How to have a good synchronisation in both case?
The code of my Activity, my scrollView and my scrollView container is here.

It seems like you hit a system limitation here - the system draws the views in order they are declared in your XML.
Therefore, when you fling the first declared ScrollView, the second one gets updated but the first one is not updated again. When you fling the second one, however, the first one gets updated, then the second one gets updated, the change is reflected to the first one and it gets updated again.
I'm not sure that the above description is 100% accurate, but it is something along these lines.
I created a test case to check my hypothesis substituting the following for main.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/main_layout">
<View android:id="#+id/separator_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_centerHorizontal="true"
android:visibility="invisible"/>
<com.example.www.syncscrollviewtesting_stackoverflow.ObservableScrollView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_alignParentEnd="true"
android:layout_toEndOf="#id/separator_view"
android:background="#android:color/holo_green_light"
android:id="#+id/left_scrollview">
<com.example.www.syncscrollviewtesting_stackoverflow.Container
android:layout_width="match_parent"
android:layout_height="100000dp"
android:minHeight="100000dp"
android:id="#+id/left_container"/>
</com.example.www.syncscrollviewtesting_stackoverflow.ObservableScrollView>
<com.example.www.syncscrollviewtesting_stackoverflow.ObservableScrollView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:layout_toStartOf="#id/separator_view"
android:background="#android:color/holo_red_dark"
android:id="#+id/right_scrollview">
<com.example.www.syncscrollviewtesting_stackoverflow.Container
android:layout_width="match_parent"
android:layout_height="100000dp"
android:minHeight="100000dp"
android:id="#+id/right_container"/>
</com.example.www.syncscrollviewtesting_stackoverflow.ObservableScrollView>
</RelativeLayout>
The above XML allows you to position both ScrollViews on either sides of the separator_view. I found out that no matter how you position them, a fling of the ScrollView having red background (declared second) always causes the "lag", while a fling of the ScrollView having green background works fine.
I also tried to prevent unnecessary updates of the ScrollViews by adding this to their code:
#Override
protected void onScrollChanged(int x, int y, int oldx, int oldy) {
super.onScrollChanged(x, y, oldx, oldy);
if (!mIsDisabled && scrollViewListener != null) {
scrollViewListener.onScrollChanged(this, x, y, oldx, oldy);
}
}
public void setDisabled(boolean isDisabled) {
mIsDisabled = isDisabled;
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
if (mIsDisabled)
return false; // Ignore touch event when disabled
else
return super.onTouchEvent(ev);
}
... and this to Activity's:
#Override
public void onScrollChanged(ObservableScrollView scrollView, int x, int y, int oldx, int oldy) {
if (scrollView == mRightScrollView) {
mLeftScrollView.setDisabled(true);
mLeftScrollView.setScrollY(y);
mLeftScrollView.setDisabled(false);
} else {
mRightScrollView.setDisabled(true);
mRightScrollView.setScrollY(y);
mRightScrollView.setDisabled(false);
}
}
but it didn't help either...
So, I guess, you better find another approach which does not involve redrawing a whole lot of Views, or just accept the "lag".
Solution:
This solution was provided by the OP himself, based on my analysis of the situation: touch events can be forwarded from the right ScrollView (declared second in XML) to the left ScrollView. This way, given that flings on the left ScrollView do not cause lags, all touch events are treated as being initiated by the first declared ScrollView and the lag is avoided.

Apologies for my previous answer hadn't properly studied your question.
As I've said, I don't think you can sync 2 ScrollViews properly without getting any latency at all. However you can still use a single view.
This gets a little trickier because you also want to split your split into equal parts with weight attribute.
The solution can be found here. Building up on that I've come up with the following code:
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
android:id="#+id/spacer"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_centerHorizontal="true"/>
<LinearLayout
android:id="#+id/left"
android:layout_alignRight="#id/spacer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
...
</LinearLayout>
<LinearLayout
android:id="#+id/right"
android:layout_alignLeft="#id/spacer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
...
</LinearLayout>
</RelativeLayout>
</ScrollView>
Then when you want to toggle the left and right view overlapping call this:
leftParams.addRule(RelativeLayout.ALIGN_RIGHT, 0);
rightParams.addRule(RelativeLayout.ALIGN_LEFT, 0);
and
leftParams.addRule(RelativeLayout.ALIGN_RIGHT, R.id.spacer);
rightParams.addRule(RelativeLayout.ALIGN_LEFT, R.id.spacer);
after setting the params you need to redraw the view with:
left.requestLayout();
right.requestLayout();
A working example can be found on this GitHub page.

Related

ImageView larger than the screen as a background

Hello I have a ImageView(background) which's height should correspond to the screen height, the width on the other hand should be left intact.
Before this was done like this
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_height="match_parent">
<ImageView
android:id="#+id/songDetails_songImage"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:contentDescription="#string/songDetails_cd_songCover"
app:srcCompat="#drawable/default_song_cover" />
</HorizontalScrollView>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
...
This worked fine - the image view occupies whole background and the user can scroll right or left to see the whole image.
My goal is to animate the scroll, to do that I need to get rid of the horizontal view(to prevent user from scrolling) but whenever I change the HorizontalScrollView to Frame/Relative layouts(with the width set to wrap_content) the image view is scaled down to fit the screen.
Is there a way to keep the imageView width intact(with height matching that of the screen) so that it would remain bigger than the device screen?(without cropping)
I could then use custom animation to scroll right.
I have looked at similar questions but I wasn't able to find anything that would work in this case.
What you could do is to try to override the onTouchListener for the HorizontalScrollView.
For example, something like this:
scrollView.setOnTouchListener(new OnTouch());
private class OnTouch implements OnTouchListener
{
#Override
public boolean onTouch(View v, MotionEvent event) {
return true;
}
}
This would disable the user from scrolling the HorizontalScrollView while you could probably still scroll it programmatically.

Android: Passing touch event to overlapped view

I have a Relative layout which has two child layouts. First child is linear layout which is container of a fragment. The fragment has few buttons. The second child is a linear layout which has a View which is blank and transparent. The second child overlaps the first child.The first child is smaller than second child. I want to send touch events from second child to first child so that those buttons on the fragment which first child contains receives click.
I read few posts on internet for solving my problem but could not solve it.
So far i have overriden dispatchTouchEvent of Activity and have tried to detect whether touch is in bounds of first child and if so i do firstChild.onTouch(ev). Actually i just dont know what to do and just trying get it working. So please, help me.
#Override
public boolean dispatchTouchEvent(MotionEvent ev)
{
float x,y,backViewX,backViewY,frontViewX,frontViewY;
x = ev.getX();
y = ev.getY();
frontViewX = frontView.getX();
frontViewY = frontView.getY();
backViewX = backView.getX();
backViewY = backView.getY();
if(y >= backViewY && y <= (backViewY+backViewHeight))
{
if(x >= backViewX && x<=(backViewX+backViewWidth))
{
return backView.onTouchEvent(ev);
}
}
return super.dispatchTouchEvent(ev);
}
Orignal Layout is not the exact as i described but it is little more complex but the concept is same as i described....i am shortning the layout for simplifying things.The frontLayout here overlaps layoutBackFragmentArea.
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/back_view_background">
<LinearLayout
android:id="#+id/layoutBackFragmentArea"
android:layout_width="match_parent"
android:layout_height="#dimen/back_view_height"
android:orientation="vertical"
android:gravity="center_vertical"
android:background="#android:color/transparent">
</LinearLayout>
<LinearLayout
android:id="#+id/layoutFrontArea"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_vertical"
android:background="#android:color/transparent">
<View
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</RelativeLayout>
layoutBackFragmentArea will contain a fragment which has UI which should receive click events.

Smooth hide/show animation for View and a second view filling the empty space

I'm trying to animate a view and hide it after some DP's were scrolled and i made everything fine, but the problem is that it will flick horribly when you are scrolling slowly before or after the Y value that is supposed to trigger the animation.
I think the flick is because i have to set its visibility to Gone and update the other view as match_parent, it won't work with just the TraslationY:
view.animate()
.translationY(-view.getBottom())
.alpha(0)
.setDuration(HEADER_HIDE_ANIM_DURATION)
.setInterpolator(new DecelerateInterpolator());
I tried to set the layout to relative and View 2 as match_parent to see if i could avoid the visibility change but it didn't work...
I have implemented all required code from Google I/O 2014 BaseActivity.java file:
https://github.com/google/iosched/blob/master/android/src/main/java/com/google/samples/apps/iosched/ui/BaseActivity.java#L17
And the animation works... but i assume that, as my customview isn't an actionbar with overlay properties, the customview won't leave and LinearLayout below won't fill the empty space (there is none).
SO, i made it to work with an animationlistener and setting customview visibility to gone when the animation is over but it will flick in a horrible way when you are close to the expected Y point that trigger the animation (flick as customview visibility is gone and LinearLayout below needs to resize itself to fill the empty space, and will quickly repeat if you scroll slowly around there).
XML:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="false"
android:animateLayoutChanges="true">
<com.project.app.layouts.TabsLayout
android:id="#+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="#+id/tabs">
<FrameLayout
android:id="#+id/content_frame_header"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<android.support.v4.view.ViewPager
android:id="#+id/viewpager"
android:layout_width="match_parent"
android:layout_height="0px"
android:layout_weight="1"
android:background="#android:color/white"/>
</LinearLayout>
</RelativeLayout>
Is there any way to do this when it's not an actionbar?
EDIT:
Added the comment about how it will flick when you scroll slowly around Y point that triggers the animation to hide/show.
I recommend you to use android:hardwareAccelerated="true" attribute in your Manifest file. It will use your device's GPU to draw views and animations.
I suggest you to check the value of view.getBottom() in both cases (when it works and when not).
It may be that it flicks because the value returned by view.getBottom() is very big.
Add this line in your code:
Log.i("YourAppName", "view.getBottom(): -" + view.getBottom());
view.animate()
.translationY(-view.getBottom())
.alpha(0)
.setDuration(HEADER_HIDE_ANIM_DURATION)
.setInterpolator(new DecelerateInterpolator());
Then check your log to see if the values are the same or not.
I have made it in a slightly different way. Note that the call I'm using requires SDK 19 (KitKat), but you can still do it using ViewPropertyAnimatorCompat
I have a FrameLayout that has the header view and the main view, with the header view on front.
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:fab="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<include layout="#layout/main_layout" android:id="#+id/main_layout" />
<include layout="#layout/header_layout" android:id="#+id/header_layout" />
</FrameLayout>
Once the views are measured (posting a runnable during onResume) I set the topPadding of the main view to be the Height of the header.
In the hide and show animation, I add an update listener and inside it I update the top padding of the main view to be the Height of the header + the Translation on Y.
final View header = findViewById(R.id. header_layout);
header.animate()
.translationY(-header.getBottom())
.setDuration(200)
.setInterpolator(new DecelerateInterpolator())
.setUpdateListener(new AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator animation) {
int top = (int) (header.getHeight()+header.getTranslationY());
findViewById(R.id.main_view).setPadding(0, top, 0, 0);
}
});
This makes it a bit smoother, since the padding gets updated together with the translation.
Actually, setting an AnimationListener using ViewPropertyAnimatorCompat does not work. The listener is never called, so for backwards compatibility I opted for this solution, not elegant, but at least it work on pre-KitKat devices:
final View mainView = findViewById(R.id.main_view);
Runnable mainUpdateRunnable = new Runnable() {
#Override
public void run() {
int top = (int) (header.getHeight()+header.getTranslationY());
mainView.setPadding(0, top, 0, 0);
if (mAnimatingHeader) {
mainView.post(this);
}
}
};
mainView.post(mainUpdateRunnable);
The variable mAnimatingHeader is updated using the AnimationListener (which works)

listView pointToPosition is incorrect 'half the time'

I'm trying to use list.pointToPosition to determine which list item position the user's finger is touching. My list contains a TextView followed by a ListView in a RelativeLayout. The problem that I've discovered is that pointToPosition seems to ignore the fact that I've got a TextView and returns the wrong position of the item.
Let me explain further with some pictures and some samples of what's being returned. I've enabled the debug feature that let's me track finger touches. You'll see it in the blue line in the following images.
In the following image, you'll notice at the top of the screen is a TextView labeled "LIGHTS". Just below that is my ListView. Look at the blue line that tracks my finger touch and slide - you'll notice that it starts near the top of the row labeled "Front Porch". Performing "pointToPosition" at that location returns the correct value of 4.
Now however, if I start my touch half way down that same row, pointToPosition returns a value of 5. You can see my touch/drag in the following screen shot.
When I change the size of my TextView to have a height of 1dp, the problem goes away; thus I'm lead to believe that the pointToPosition method assumes that my ListView is the only thing on the page.
I could implement a hack and deal with this by calculating the offset caused by the TextView, but my feeling is that this will eventually come back to bite me when this bug (assuming it's a bug) gets fixed. What I'm wondering is if there's a proper way to fix it.
Source Code Samples:
layout.xml
<com.mls.util.RelativeLayoutTouchInterceptor xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
android:background="#drawable/app_bg"
android:id="#+id/rlRoot" android:clickable="true"
tools:context=".DeviceListActivity" >
<TextView android:id="#+id/list_header"
style="?android:listSeparatorTextViewStyle"
android:text="Lights"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</TextView>
<ListView
android:id="#+id/dlList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="65dp"
android:layout_below="#+id/list_header"
android:layout_alignParentLeft="true">
</ListView>
</com.mls.util.RelativeLayoutTouchInterceptor>
RelativeLayoutTouchInterceptor.java
public class RelativeLayoutTouchInterceptor extends RelativeLayout {
...
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN: {
downStart = MotionEvent.obtain(event);
ListView list = (ListView)this.findViewById(R.id.dlList);
int position = list.pointToPosition((int)downStart.getX(), (int)downStart.getY());
}
...
}
The reason this happens is because you're overriding the onInterceptTouchEvent of the RelativeLayout, not the ListView. It's reporting the y coordinate within the layout itself, which includes the TextView.
If you want to get the coordinates relative to the ListView only, you could just override it for the ListView itself. It is an indirect subclass of ViewGroup, so it has access to the method.
There's no bug that will be fixed to worry about, it's working like it should.
I went ahead and added the perceived hack to fix this of adding the height of the TextView to the Y position provided to pointToPosition

Problems animating an Android layout; elements snap back to start position

I have been searching for an answer to this for days, and while some things kinda work (and most don't), I'm hoping I can find the best practice for what I'm trying to do.
I'm trying to get a notification bar to display in my app. Ideally, it would slide down from the top, while shifting other elements in the layout to accommodate. Here's an illustration to help: illustration
Here is how the layout is structured (I took out a bit for brevity):
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false">
<!-- notification -->
<RelativeLayout
android:id="#+id/notification_bar"
android:layout_width="match_parent"
android:layout_height="40dip"
android:background="#drawable/notification_background"
android:visibility="visible">
<!-- end notifcation -->
</RelativeLayout>
<!-- header -->
<RelativeLayout
android:id="#+id/header"
android:layout_width="match_parent"
android:layout_height="47dip"
android:layout_alignParentTop="true" >
<ImageView
android:id="#+id/header_bg"
android:layout_width="match_parent"
android:layout_height="47dip"
android:src="#drawable/home_header" />
<!-- end header -->
</RelativeLayout>
<!-- buttons -->
<LinearLayout
android:id="#+id/buttons"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="#+id/footer"
android:layout_below="#id/notification_bar"
android:layout_gravity="center_horizontal">
<ImageButton
android:id="#+id/button1"
android:src="#drawable/button1"
android:layout_width="86dip"
android:layout_height="65dip"
android:layout_weight=".4" />
<ImageButton
android:id="#+id/button2"
android:src="#drawable/button2"
android:layout_width="98dip"
android:layout_height="73dip"
android:layout_weight=".2" />
<ImageButton
android:id="#+id/button3"
android:src="#drawable/button3"
android:layout_width="86dip"
android:layout_height="71dip"
android:layout_weight=".4" />
<!-- end buttons -->
</LinearLayout>
<!-- footer -->
<LinearLayout
android:id="#id/footer"
android:layout_width="match_parent"
android:layout_height="76dip"
android:layout_alignParentBottom="true"
android:background="#drawable/home_footer" >
<!-- end footer -->
</LinearLayout>
</RelativeLayout>
MAIN PROBLEM:
To start, I animated the notification bar. It moves, and at the end of the animation, it snaps back into place. Yes, I have fillAfter set to true. It doesn't make a difference. Regardless, the 3 items that should shift are clickable, and from what I've read, the elements haven't actually moved, they just look like they have.
SECONDARY PROBLEM:
The entire view is a RelativeLayout, however the three elements are in a LinearLayout set via the XML to be layout_below the notification bar. I had hoped that shifting the notification bar would squeeze this LinearLayout, shifting the buttons to accommodate, but no such luck. If I have to shift the three elements as separate animations, that's fine. I've tried that, but they suffer from the same "snap-back" issue the notification bar does. I was hoping there would be a simpler, more logical approach, however.
I've found a number of posts about this snap-back problem, but none of the solutions quite work (or make sense to me, granted a bit of a noob). It sounds like something needs to happen in the onAnimationEnd handler. I think it's something with adjusting the LayoutParams, but I'm not sure how or what to do there.
I'm targeting for API 8 (2.2), so the Honeycomb animation APIs won't help. I've looked into NineOldAndroids, which looks promising, but figure there has got to be a way to do this with the native API.
** Bonus points if we can get the notification bar to be dismissed, and everything moves back to its original position.
** UPDATE: The following code KIND OF works **
Here is the animation method to slide the notification bar out:
private void showNotification() {
mNotificationBar.setVisibility(View.VISIBLE);
Animation slideOut = AnimationUtils.loadAnimation(this, R.anim.slide_notification_out);
slideOut.setAnimationListener(new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {
// do nothing
}
#Override
public void onAnimationRepeat(Animation animation) {
// do nothing
}
#Override
public void onAnimationEnd(Animation animation) {
// do SOMETHING
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) mNotificationBar.getLayoutParams();
params.addRule(RelativeLayout.BELOW, mHeader.getId());
mNotificationBar.setLayoutParams(params);
mNotificationBar.clearAnimation();
}
});
mNotificationBar.startAnimation(slideOut);
}
Altering the LayoutParams on AnimationEnd keeps the Notification bar in place. AND, when the animation is done, the Buttons layout squeezes to accommodate! BUT, the button layout doesn't smoothly animate like the Notification Bar, it just snaps into place at the end of the animation. Also, the Notification Bar also jumps a bit at the very end of the animation, I'm guessing because the layout is being redrawn. SO CLOSE, but so far.
Snap back problem
You need to define the notification in the final place that you want it to appear in the layout. For you it's probably as the first item in the LinearLayout you refer above. Then you set visibilityto gone.
Finally you use a piece of code similar to the one bellow (I´m using it to animate buttons into the screen):
private void buttonFadeOut() {
linear_layout_buttons.startAnimation(AnimationUtils.loadAnimation(MyMapActivity.this, android.R.anim.slide_out_right));
linear_layout_buttons.setVisibility(View.GONE);
}
private void buttonFadeIn() {
if(linear_layout_buttons.getVisibility() == View.GONE){
linear_layout_buttons.startAnimation(AnimationUtils.loadAnimation(MyMapActivity.this, R.anim.slide_in_right));
linear_layout_buttons.setVisibility(View.VISIBLE);
}
}
Squeeze problem
This one I've never tried with an animation, but you can set the android:layout_weight="1.0" in each one of the items in your relative layout, to split the available space equaly between them (or play with the value to assign diferent space for each).
Regards.

Categories

Resources