Home > Devices > Fire Phone

Integrating Tilt Scrolling into Your App

Introduction

The TiltScrollController allows you to use head position data to enhance users' interactions with your app's views:

  • Use the TiltScrollController to scroll ScrollViews, ListViews, or GridViews, or custom scrollable views up and down.
  • The TiltScrollController controls the movement of an overlaid scrollable view on top of another scrollable view.

Adding a TiltScrollController to Your App

Integrating tilt scrolling (also known as auto scrolling) into your app requires a few basic programming steps:

  1. Create a new TiltScrollController.
  2. Bind your TiltScrollController to a scrollable view, such as a ListView, GridView, or ScrollView.
  3. Set up the appropriate triggers for enabling or disabling the controller.
  4. Register and de-register your event observers, as needed.

The following TiltScrollActivity sample shows all of these steps:

/**
 * TiltScrollActivity showcases the ability to scroll based on the
 * tilt of the device in relation to a user's head.
 *
 * A list of cities scrolls through a ListView according to a user's head angle.
 * Scrolling stops when a user touches the device screen.
 */
public class TiltScrollActivity extends Activity {
    // TAG used for logging.
    private static final String TAG = "TiltScrollActivity";
    // TiltScrollController used to control tilt scrolling.
    private TiltScrollController mTiltScrollController;

    /**
     * Called when the activity is first created.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // Create an instance of TiltScrollController.
        mTiltScrollController = new TiltScrollController(this);

        // Grabs a list of cities from raw resources.
        List<String> cities = readCities();

        // Create a ListAdapter and set it to a ListView which holds all the cities.
        ListAdapter adapter = new ArrayAdapter<String>(this, R.layout.city_layout, R.id.city_name, cities);
        ListView cityListView = (ListView) findViewById(R.id.city_listview);
        cityListView.setAdapter(adapter);

        // Attach the ViewGroup and ListView to the TiltScrollController.
        ViewGroup containerView = (ViewGroup) findViewById(R.id.container);
        mTiltScrollController.attach(containerView, cityListView);

        // Add a TouchEventTrigger to the TiltScrollController.
        // This trigger disables tilt scrolling when the view is in a touch event.
        mTiltScrollController.addScrollStateTrigger(new  TouchEventTrigger(cityListView));

        // Add a PanelVisibleTrigger to the TiltScrollController.
        // This trigger disables tilt scrolling when the Center panel is not the currently visible top panel.
        SidePanelLayout sidePanelLayout = (SidePanelLayout) findViewById(R.id.sidepanellayout);
        mTiltScrollController.addScrollStateTrigger(new  PanelVisibleTrigger(sidePanelLayout, PanelVisibleTrigger.Panel.Center));
    }

    /**
     * Called every time the activity is launched.
     * Registers for tilt event listeners.
     */
    @Override
    public void onResume() {
        super.onResume();
        mTiltScrollController.registerEventObservers();
    }

    /**
     * Called upon application pause or shutdown.
     * Release the TiltScrollController's event listeners.
     */
    @Override
    public void onPause() {
        mTiltScrollController.unregisterEventObservers();
        super.onPause();
    }

    /**
     * Read in a raw list of cities, and create a list.
     */
    private List<String> readCities() {
        InputStream inputStream = this.getResources().openRawResource(R.raw.populous_cities);
        InputStreamReader inputReader = new InputStreamReader(inputStream);
        BufferedReader reader = new BufferedReader(inputReader);

        // Create a List of cities
        List<String> cityList = new ArrayList<String>();
        String city;
        try {
            while ((city = reader.readLine()) != null) {
                cityList.add(city);
            }
        } catch (IOException e) {
            Log.e(TAG, "Failed to read list of cities.", e);
        }

        return cityList;
    }
}

Using Triggers to Disable Tilt Scrolling

The TiltScrollController becomes operational as soon as you call an attach method on it; however, you can use a trigger to disable the controller's auto-scrolling. TiltScrollController supports optional, injectable triggers, which prevent the feature from engaging. Three trigger types are available:

  • StateTrigger: Abstract trigger that toggles scrolling on or off when the trigger's state changes. You can derive a custom trigger from this trigger.
  • PanelVisibleTrigger: Trigger that toggles scrolling on or off, depending on whether a specified SidePanel is the currently visible top panel. Extends from StateTrigger.
  • TouchEventTrigger: Trigger that responds if a view is currently in a touch event. Extends from StateTrigger.

Note that, as the default behavior, the TiltScrollController will always control the auto-scrolling of your view when the activity is running if you do not add any triggers. Triggers simply add additional ways to disable auto-scrolling of a view. Additionally, If you add a trigger but do not attach anything, the trigger is not operational.

The following code snippet (also shown in the previous example) disengages the scrolling feature when the scrollable view is touched:

mTiltScrollController.addScrollStateTrigger(new TouchEventTrigger(cityListView));

In this case, when a user touches the ListView, the TiltScrollController will discontinue tracking head position and scrolling the view.

Determining Which Trigger to Use for Your Layout

You can use triggers with both SidePanel layouts and DrawerLayouts in your activity.

Note that if you are developing a new app for the Fire, your layout should use SidePanel layouts instead of DrawerLayouts. However, if you are looking to quickly port over an existing Android app with a DrawerLayout, you can still use DrawerLayouts and triggers with your Fire app. In general, you should use a PanelVisibleTrigger for SidePanel layouts and a custom trigger derived from a StateTrigger for DrawerLayouts. The next two sections discuss these recommendations in more detail.

Using Triggers with SidePanel Layouts

If you have developed an app from scratch for the Fire, your activity should use a SidePanelLayout to separate content into a main panel, left panel and right panel. If an activity has tilt-scrollable content in its main panel, the tilt-scrollable content should stop responding to head tracking when the left or right panel is on top of the main panel. Additionally, if an application has tilt-scrollable content in one of its side panels, the tilt-scrollable content should only respond to head tracking when that panel is on top of the main panel. You can use the PanelVisibleTrigger to automatically stop these scrolling behaviors, as shown in the following code snippet:

mainPanelTiltController.addScrollStateTrigger(new PanelVisibleTrigger(sidePanelLayout, PanelVisibleTrigger.Panel.Center));
// Have the tilt scroll controller for the main panel content only function when the center panel is the visible top panel.

rightPanelTiltController.addScrollStateTrigger(new PanelVisibleTrigger(sidePanelLayout, PanelVisibleTrigger.Panel.Right));
// Have the tilt scroll controller for the right panel content only function when the right panel is the visible top panel.

Using Triggers with DrawerLayouts

If you are porting over an existing app with a DrawerLayout, you can till use a TiltScrollController to work with drawers that slide out over your scrollable view. In this case, scrolling should stop when the drawer is pulled out.

You can stop scrolling on your view in this case by creating a custom DrawerLayout trigger, as shown in the following example:

package com.example.amazon.tiltscrolldrawerlayout;

import android.support.v4.widget.DrawerLayout;
import android.view.View;

import com.amazon.euclid.util.triggers.StateTrigger;

/**
 * A custom trigger that can toggle tilt-scrolling on or off, depending on the state of
 * a given DrawerLayout.
 */
public class DrawerLayoutTrigger extends StateTrigger {
    // DrawerLayout that this trigger depends on.
    private DrawerLayout mDrawerLayout;
    // A drawer View.
    private View mView;
    // A private implementation of DrawerLayout.DrawerListener
    private DrawerListener mDrawerListener;

    /**
     * Constructs a DrawerLayoutTrigger.
     */
    public DrawerLayoutTrigger(DrawerLayout drawerLayout, View view) {
        mDrawerLayout = drawerLayout;
        mView = view;
        // Create a new DrawerListener
        mDrawerListener = new DrawerListener();
    }

    /**
     * Registers events that should be present when the TiltScrollController registers its own events.
     */
    @Override
    public void registerForEvents() {
        // If the DrawerLayout is null, break.
        if (mDrawerLayout == null) {
            return;
        }
        // Set a listener to be notified of drawer events.
        mDrawerLayout.setDrawerListener(mDrawerListener);
        // Check to see if the drawer View is null, and set StateTrigger.mValue to be true.
        if (mView == null) {
            mValue = true;
        }
    }

    /**
     * Unregisters events that should not be present when the TiltScrollController unregisters its own events.
     */
    @Override
    public void unregisterForEvents() {
        // If the DrawerLayout is null, break.
        if (mDrawerLayout == null) {
            return;
        }
        // Remove any listeners.
        mDrawerLayout.setDrawerListener(null);
        // Set StateTrigger.mValue to be false.
        mValue = false;
    }

    /**
     * An implementation of DrawerLayout.DrawerListener.
     * A listener for monitoring events about drawers.
     */
    private class DrawerListener implements DrawerLayout.DrawerListener {
        /**
         * Called when a drawer has settled in a completely closed state.
         */
        @Override
        public void onDrawerClosed(View view) {
            // Set StateTrigger.mValue appropriately according to the current state of the given View.
            mValue = (mView == null || !mView.equals(view));
            // Notify the TiltScrollController that the trigger's state has changed.
            notifyTriggerStateChanged();
        }

        /**
         * Called when a drawer has settled in a completely open state.
         */
        @Override
        public void onDrawerOpened(View view) {
            // Set StateTrigger.mValue appropriately according to the current state of the given View.
            mValue = (mView != null && mView.equals(view));
            // Notify the TiltScrollController that the trigger's state has changed.
            notifyTriggerStateChanged();
        }

        /**
         * Called when a drawer's position changes.
         */
        @Override
        public void onDrawerSlide(View arg0, float arg1) {
            // Do nothing.
        }

        /**
         * Called when the drawer motion state changes.
         */
        @Override
        public void onDrawerStateChanged(int arg0) {
            // Do nothing.
        }
    }
}

After creating the DrawerLayoutTrigger, establish a connection with your TiltScrollController by adding a new trigger to the controller:

mTiltScrollController.addScrollStateTrigger(new DrawerLayoutTrigger((DrawerLayout)findViewById(R.id.drawer_layout), null));

For this example, "null" tells the trigger that the scrollable view is not in a drawer. If the scrollable is in a drawer, then pass the root of that drawer.

Integrating TiltScrollController with Custom Scrollable Views

You can use TiltScrollController to control the scrolling of a view that does not have built-in TiltScrollController support by extending the TiltScrollableView class. TiltScrollableView is an abstract class that implements the TiltScrollable interface and acts as a template for a scrollable view. In your subclass, implement the following methods from the TiltScrollable interface on your scrollable view:

  • scrollStart
  • scrollBy
  • scrollEnd
  • pixelRemainingInDirection

You can override the remaining TiltScrollable methods if their default implementation from TiltScrollController is not appropriate for your view.

The following code is an example TiltScrollableView subclass that implements tilt scrolling on a custom infinitely-scrollable view:

public class TiltScrollableCustomScrollableView extends TiltScrollableView {
    @Override
    public void scrollStart() {
    }

    @Override
    public void scrollBy(int x, int y) {
        MyCustomScrollView scrollView = getView().get();
        if (scrollView != null) {
            scrollView.smoothScrollBy(x, y);
        }
    }

    @Override
    public void scrollEnd() {
    }

    @Override
    public float pixelRemainingInDirection(TiltScrollDirection direction) {
        return 100f; // So that TiltScrollController always knows there is more content to be scrolled in either direction
    }
}

Next, in your code, create an instance of this class, and pass it into the version of the TiltScrollController attach method that takes a generic TiltScrollable instance:


MyCustomScrollView scrollView = (MyCustomScrollView) findViewById(R.id.my_custom_view);
TiltScrollableCustomScrollableView scrollableView = new TiltScrollableCustomScrollableView();
tiltController.attach((ViewGroup)findViewById(R.id.content_frame), scrollableView, scrollView);