package net.bible.android.control.page;
import java.util.ArrayList;
import java.util.List;
import net.bible.android.BibleApplication;
import net.bible.service.common.CommonUtils;
import android.annotation.TargetApi;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Build;
import android.util.Log;
import android.view.Display;
import android.view.Surface;
import android.view.WindowManager;
/** Manage the logic behind tilt-to-scroll
*
* @author Martin Denham [mjdenham at gmail dot com]
* @see gnu.lgpl.License for license details.<br>
* The copyright to this program is held by it's author.
*/
//Tilt-scroll is disabled on 2.1/ only enabled on 2.2+
@TargetApi(Build.VERSION_CODES.FROYO)
public class PageTiltScrollControl {
// must be null initially
private static Boolean mIsOrientationSensor = null;
private boolean mIsTiltScrollEnabled = false;
// the pitch at which a user views the text stationary
// this changes dynamically when the screen is touched
// both angles are degrees
private int mNoScrollViewingPitch = -38;
private boolean mNoScrollViewingPitchCalculated = false;
private boolean mSensorsTriggered = false;
private int mLastNormalisedPitchValue;
private static final int NO_SCROLL_VIEWING_TOLERANCE = 2; //3;
private static final int NO_SPEED_INCREASE_VIEWING_TOLERANCE = 2;
private static final int INVALID_STATE = -9999;
// this is decreased (subtracted from) to speed up scrolling
private static final int BASE_TIME_BETWEEN_SCROLLS = 48; //70(jerky) 40((fast);
private static final int MIN_TIME_BETWEEN_SCROLLS = 4;
/**
* Time between scroll = Periodic time = 1/frequency
* Scroll speed = frequency*wavelength // wavelength = 1 pixel so can ignore wavelength
* => speed = 1/Time between each scroll event
* If we use regular changes in periodic time then initial changes in tilt have little affect on speed
* but when tilt is greater small changes in tilt cause large changes in speed
* Therefore the following mTimeBetweenScrollListEvery5Degrees is used to even out speed changes
*
* This was my starting spreadsheet from which the below array was derived.
* The spreadsheet starts with regular changes in speed and calculates the required Periodic time
* degrees speed Periodic time (ms)
0 0.02 50
5 0.04 25
10 0.06 16.6666666666667
15 0.08 12.5
20 0.1 10
25 0.12 8.33333333333333
30 0.14 7.14285714285714
35 0.16 6.25
40 0.18 5.55555555555556
45 0.2 5
*/
private static int MIN_DEGREES_OFFSET = 0;
private static int MAX_DEGREES_OFFSET = 45;
private static float MIN_SPEED = 0.02f;
private static float MAX_SPEED = 0.2f;
// calculated to ensure even speed up of scrolling
private static Integer[] mTimeBetweenScrolls;
// current pitch of phone - varies dynamically
private float[] mOrientationValues;
private int mRotation = Surface.ROTATION_0;
// needed to find if screen switches to landscape and must different sensor value
private Display mDisplay;
public static final String TILT_TO_SCROLL_PREFERENCE_KEY = "tilt_to_scroll_pref";
private static final String TAG = "TiltScrollControl";
public static class TiltScrollInfo {
public int scrollPixels;
public boolean forward;
public int delayToNextScroll;
public static int TIME_TO_POLL_WHEN_NOT_SCROLLING = 500;
private TiltScrollInfo reset() {
scrollPixels = 0;
forward = true;
delayToNextScroll = TIME_TO_POLL_WHEN_NOT_SCROLLING;
return this;
}
}
// should not need more than one because the request come in one at a time
private TiltScrollInfo tiltScrollInfoSingleton = new TiltScrollInfo();
public PageTiltScrollControl() {
initialiseTiltSpeedPeriods();
}
public TiltScrollInfo getTiltScrollInfo() {
TiltScrollInfo tiltScrollInfo = tiltScrollInfoSingleton.reset();
int delayToNextScroll = BASE_TIME_BETWEEN_SCROLLS;
if (mOrientationValues!=null) {
int normalisedPitch = getNormalisedPitch(mRotation, mOrientationValues);
if (normalisedPitch!=INVALID_STATE) {
int devianceFromViewingAngle = getDevianceFromStaticViewingAngle(normalisedPitch);
if (devianceFromViewingAngle > NO_SCROLL_VIEWING_TOLERANCE) {
tiltScrollInfo.forward = normalisedPitch < mNoScrollViewingPitch;
// speedUp if tilt screen beyond a certain amount
if (tiltScrollInfo.forward) {
delayToNextScroll = getDelayToNextScroll(devianceFromViewingAngle-NO_SCROLL_VIEWING_TOLERANCE-NO_SPEED_INCREASE_VIEWING_TOLERANCE-1);
// speedup could be done by increasing scroll amount but that leads to a jumpy screen
tiltScrollInfo.scrollPixels = 1;
} else {
// TURNED OFF UPSCROLL
delayToNextScroll = BASE_TIME_BETWEEN_SCROLLS;
tiltScrollInfo.scrollPixels = 0;
}
}
}
}
if (mIsTiltScrollEnabled) {
tiltScrollInfo.delayToNextScroll = Math.max(MIN_TIME_BETWEEN_SCROLLS, delayToNextScroll);
}
return tiltScrollInfo;
}
/** start or stop tilt to scroll functionality
*/
public boolean enableTiltScroll(boolean enable) {
// Android 2.1 does not have Display.getRotation so disable tilt-scroll for 2.1
if (!CommonUtils.getSharedPreferences().getBoolean(TILT_TO_SCROLL_PREFERENCE_KEY, false) ||
!isTiltSensingPossible()) {
return false;
} else if (mIsTiltScrollEnabled != enable) {
mIsTiltScrollEnabled = enable;
if (enable) {
connectListeners();
} else {
disconnectListeners();
}
return true;
} else {
return false;
}
}
/** called when user touches screen to reset home position
*/
public void recalculateViewingPosition() {
mNoScrollViewingPitchCalculated = false;
mSensorsTriggered = false;
}
/** if screen rotates must switch between different values returned by orientation sensor
*/
private int getNormalisedPitch(int rotation, float[] orientationValues) {
float pitch = 0;
// occasionally the viewing position was being unexpectedly reset to zero - avoid by checking for the problematic state
if (rotation==0 &&
orientationValues[1]==0.0f &&
orientationValues[2]==0.0f) {
if (!mNoScrollViewingPitchCalculated) {
return INVALID_STATE;
} else {
return mLastNormalisedPitchValue;
}
}
switch (rotation) {
//Portrait for Nexus
case Surface.ROTATION_0:
pitch = orientationValues[1];
break;
//Landscape for Nexus
case Surface.ROTATION_90:
pitch = -orientationValues[2];
break;
case Surface.ROTATION_270:
pitch = orientationValues[2];
break;
case Surface.ROTATION_180:
pitch = -orientationValues[1];
break;
default:
Log.e(TAG, "Invalid Scroll rotation:"+rotation);
}
int normalisedPitch = Math.round(pitch);
mLastNormalisedPitchValue = normalisedPitch;
return normalisedPitch;
}
/** find angle between no-scroll-angle and current pitch
*/
private int getDevianceFromStaticViewingAngle(int normalisedPitch) {
if (!mNoScrollViewingPitchCalculated) {
// Log.d(TAG, "Recalculating home/noscroll pitch "+normalisedPitch);
// assume user's viewing pitch is the current one
mNoScrollViewingPitch = normalisedPitch;
// pitch can be 0 before the sensors have fired
if (mSensorsTriggered) {
mNoScrollViewingPitchCalculated = true;
}
}
return Math.abs(normalisedPitch-mNoScrollViewingPitch);
}
/** Get delay between scrolls for specified tilt
*
* negative tilts will return min delay
* 0-num elts in mTimeBetweenScrolls array will return associated period from array
* larger tilts will return max period from array
*
* @param tilt
* @return
*/
private int getDelayToNextScroll(int tilt) {
// speed changes with every degree of tilt
// ensure we have a positive number
tilt = Math.max(tilt, 0);
if (tilt < mTimeBetweenScrolls.length) {
return mTimeBetweenScrolls[tilt];
} else {
return mTimeBetweenScrolls[mTimeBetweenScrolls.length-1];
}
}
/**
* Orientation monitor (see Professional Android 2 App Dev Meier pg 469)
*/
private void connectListeners() {
mDisplay = ((WindowManager) BibleApplication.getApplication().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
SensorManager sm = (SensorManager) BibleApplication.getApplication().getSystemService(Context.SENSOR_SERVICE);
Sensor oSensor = sm.getDefaultSensor(Sensor.TYPE_ORIENTATION);
sm.registerListener(myOrientationListener, oSensor, SensorManager.SENSOR_DELAY_UI);
}
private void disconnectListeners() {
try {
SensorManager sm = (SensorManager) BibleApplication.getApplication().getSystemService(Context.SENSOR_SERVICE);
sm.unregisterListener(myOrientationListener);
} catch (IllegalArgumentException e) {
// Prevent occasional: IllegalArgumentException: Receiver not registered: android.hardware.SystemSensorManager
// If not registered then there is no need to unregister
Log.w(TAG, "Error disconnecting sensor listener", e);
}
}
final SensorEventListener myOrientationListener = new SensorEventListener() {
public void onSensorChanged(SensorEvent sensorEvent) {
if (sensorEvent.sensor.getType() == Sensor.TYPE_ORIENTATION) {
mOrientationValues = sensorEvent.values;
mRotation = mDisplay.getRotation();
mSensorsTriggered = true;
}
}
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
};
/** return true if both a sensor and android support are available to sense device tilt
*/
public static boolean isTiltSensingPossible() {
return isOrientationSensor() &&
CommonUtils.isFroyoPlus();
}
/**
* Returns true if at least one Orientation sensor is available
*/
public static boolean isOrientationSensor() {
if (mIsOrientationSensor == null) {
SensorManager sm = (SensorManager) BibleApplication.getApplication().getSystemService(Context.SENSOR_SERVICE);
if (sm != null) {
List<Sensor> sensors = sm.getSensorList(Sensor.TYPE_ORIENTATION);
mIsOrientationSensor = Boolean.valueOf(sensors.size() > 0);
} else {
mIsOrientationSensor = Boolean.FALSE;
}
}
return mIsOrientationSensor;
}
public boolean isTiltScrollEnabled() {
return mIsTiltScrollEnabled;
}
/** map degrees tilt to time between 1 pixel scrolls to save time at runtime
*/
private void initialiseTiltSpeedPeriods() {
float degreeRange = MAX_DEGREES_OFFSET - MIN_DEGREES_OFFSET;
float speedRange = MAX_SPEED - MIN_SPEED;
List<Integer> delayPeriods = new ArrayList<Integer>();
for (int deg=MIN_DEGREES_OFFSET; deg<=MAX_DEGREES_OFFSET; deg++) {
float speed = MIN_SPEED+((deg/degreeRange)*speedRange);
float period = 1/speed;
delayPeriods.add(Math.round(period));
}
mTimeBetweenScrolls = delayPeriods.toArray(new Integer[delayPeriods.size()]);
}
}