0

I'm using a custom snapping ScrollView in my app. It extends HorizontalScrollView and overrides onTouchEvent(). This is used to snap the child Views in the middle of the ScrollView. There is also a leading and a trailing space which are wide enough so the first and last elements can be placed in the middle.
I use two class variables:

int mActiveFeature //Position of the element nearest to the middle, initially set to 1
List<Integer> mAvailableItems //IDs of contained menu elements in order without the spaces

Here is my onTouchEvent:

@Override
public boolean onTouchEvent(MotionEvent event) {
    final int menuItemCount = this.mAvailableItems.size();
    switch(event.getAction()) {
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            float scrollX = getScrollX();
            float layoutWidth =
                    SnappingScroll.this.getWidth();
            float featureWidth =
                    (int) getResources().getDimension(R.dimen.item_width);
            float spaceWidth =
                    (int) getResources().getDimension(R.dimen.space_width);

            View oldItem =
                    findViewById(
                            SnappingScroll.this.mAvailableItems.get(
                                    SnappingScroll.this.mActiveFeature - 1
                            )
                    );
            oldItem.setAlpha(0.5f);
            oldItem.setClickable(false);

            SnappingScroll.this.mActiveFeature = (int) Math.min(
                    Math.round((scrollX - spaceWidth + (layoutWidth / 2) + (featureWidth / 2)) / featureWidth),
                    (float) menuItemCount
            );

            View newItem =
                    findViewById(
                            SnappingScroll.this.mAvailableItems.get(
                                    SnappingScroll.this.mActiveFeature - 1
                            )
                    );
            newItem.setAlpha(1.0f);
            newItem.setClickable(true);

            int scrollTo = (int) (
                    SnappingScroll.this.mActiveFeature * featureWidth
                            + spaceWidth
                            - layoutWidth / 2
                            - featureWidth / 2
            );
            smoothScrollTo(scrollTo, 0);
            return true;
        default:
            return super.onTouchEvent(event);
    }

}

Here is the XML file I'm using

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:background="@android:color/black"
    android:orientation="vertical">
    <com.example.SnappingScroll
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center_horizontal">
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:gravity="center_vertical">
            <Space
                android:id="@+id/leading_space"
                android:layout_width="@dimen/space_width"
                android:layout_height="match_parent" />
            <ImageView
                    android:id="@+id/item_1"
                    android:layout_width="@dimen/item_width"
                    android:layout_height="match_parent"
                    android:alpha="0.5"
                    android:clickable="false"
                    android:onClick="clickFunction"
                    android:src="@drawable/m_1"/>
            <ImageView
                    android:id="@+id/item_2"
                    android:layout_width="@dimen/item_width"
                    android:layout_height="match_parent"
                    android:alpha="0.5"
                    android:clickable="false"
                    android:onClick="clickFunction"
                    android:src="@drawable/m_2"/>
            <ImageView
                    android:id="@+id/item_3"
                    android:layout_width="@dimen/item_width"
                    android:layout_height="match_parent"
                    android:alpha="0.5"
                    android:clickable="false"
                    android:onClick="clickFunction"
                    android:src="@drawable/m_3"/>
            <Space
                android:id="@+id/trailing_space"
                android:layout_width="@dimen/space_width"
                android:layout_height="match_parent" />
        </LinearLayout>
    </com.example.SnappingScroll>
</LinearLayout>

I want all elements at half transparency and not clickable, except the active feature. At the moment, you can scroll, and as you let loose, the View snaps to the nearest element. Then the last snapped to element is deactivated and the new snapped to element is activated. This works fine, but I want the user to see, which element the ScrollView will snap to, as he lifts his finger. So the calculations for the new active feature and the changing of the transparency should be done while scrolling. Is this possible? I already tried to put the code in a case for MotionEvent.ACTION_MOVE, but then the View wasn't scrolling at all. As I put the code into a case for MotionEvent.ACTION_SCROLL, nothing happened and the snapping wasn't triggered.


Edit:
At first, I put the code in an additional OnTouchListener, which was redundant, so now I override the onTouchEvent function and edited the question correspondingly.

EarlGrey
  • 531
  • 7
  • 29

1 Answers1

0

After some further reading and a bit of try & error, I came up with the solution.
So first, the needed MotionEvent is ACTION_MOVE. I tried this before, but it hasn't worked, as I returned true and false on my own, never calling the implementation of the superclass. So I always return super.onTouchEvent(event), as this handles the scrolling itself.
Only my ACTION_UP and ACTION_CANCEL handling isn't calling to the superclass, as this would do normal scrolling and no snapping.

@Override
public boolean onTouchEvent(MotionEvent event) {
    final int menuItemCount = this.mAvailableItems.size();

    int oldFeature = this.mActiveFeature;
    float scrollX = getScrollX();
    float layoutWidth =
            SnappingScroll.this.getWidth();
    float featureWidth =
            (int) getResources().getDimension(R.dimen.item_width);
    float spaceWidth =
            (int) getResources().getDimension(R.dimen.space_width);
    View newItem, oldItem;

    switch(event.getAction()) {
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            newItem =
                    findViewById(
                            SnappingScroll.this.mAvailableItems.get(
                                    SnappingScroll.this.mActiveFeature - 1
                            )
                    );
            newItem.setClickable(true);

            int scrollTo = (int) (
                    SnappingScroll.this.mActiveFeature * featureWidth
                            + spaceWidth
                            - layoutWidth / 2
                            - featureWidth / 2
            );
            smoothScrollTo(scrollTo, 0);
            return true;
        case MotionEvent.ACTION_MOVE:
            SnappingScroll.this.mActiveFeature = (int) Math.min(
                    Math.round((scrollX - spaceWidth + (layoutWidth / 2) + (featureWidth / 2)) / featureWidth),
                    (float) menuItemCount
            );

            if(oldFeature == this.mActiveFeature) {
                return super.onTouchEvent(event);
            } else {
                oldItem =
                        findViewById(
                                SnappingScroll.this.mAvailableItems.get(
                                        oldFeature - 1
                                )
                        );
                oldItem.setAlpha(0.5f);
                oldItem.setClickable(false);

                newItem =
                        findViewById(
                                SnappingScroll.this.mAvailableItems.get(
                                        SnappingScroll.this.mActiveFeature - 1
                                )
                        );
                newItem.setAlpha(1.0f);
                return super.onTouchEvent(event);
            }
        default:
            return super.onTouchEvent(event);
    }

}

Then there was one last problem to solve. ScrollView can be flinged, so the scrolling doesn't stop, when you lift your finger. This interferred with the snapping, so I overrid the fling() method:

@Override
public void fling (int velocityY)
{
    super.fling(0);
}

Now, there is no flinging and the ScrollView snapps to the most middle element. The transparency values are set while scrolling, so the user always knows, to which element the ScrollView will snap.

EarlGrey
  • 531
  • 7
  • 29