package com.marshalchen.common.uimodule.motion;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Matrix;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.util.AttributeSet;
import android.widget.ImageView;
import com.marshalchen.common.uimodule.R;
/*
* Copyright 2014 Nathan VanBenschoten
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
public class ParallaxImageView extends ImageView implements SensorEventListener {
private static final String TAG = ParallaxImageView.class.getName();
/**
* If the x and y axis' intensities are scaled to the image's aspect ratio (true) or
* equal to the smaller of the axis' intensities (false). If true, the image will be able to
* translate up to it's view bounds, independent of aspect ratio. If not true,
* the image will limit it's translation equally so that motion in either axis results
* in proportional translation.
*/
private boolean mScaledIntensities = false;
/**
* The intensity of the parallax effect, giving the perspective of depth.
*/
private float mParallaxIntensity = 1.2f;
/**
* The maximum percentage of offset translation that the image can move for each
* sensor input. Set to a negative number to disable.
*/
private float mMaximumJump = .1f;
// Instance variables used during matrix manipulation.
private SensorInterpreter mSensorInterpreter;
private SensorManager mSensorManager;
private Matrix mTranslationMatrix;
private float mXTranslation;
private float mYTranslation;
private float mXOffset;
private float mYOffset;
public ParallaxImageView(Context context) {
this(context, null);
}
public ParallaxImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ParallaxImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// Instantiate future objects
mTranslationMatrix = new Matrix();
mSensorInterpreter = new SensorInterpreter();
// Sets scale type
setScaleType(ScaleType.MATRIX);
// Set available attributes
if (attrs != null) {
final TypedArray customAttrs = context.obtainStyledAttributes(attrs, R.styleable.ParallaxImageView);
if (customAttrs != null) {
if (customAttrs.hasValue(R.styleable.ParallaxImageView_intensity))
setParallaxIntensity(customAttrs.getFloat(R.styleable.ParallaxImageView_intensity, mParallaxIntensity));
if (customAttrs.hasValue(R.styleable.ParallaxImageView_scaledIntensity))
setScaledIntensities(customAttrs.getBoolean(R.styleable.ParallaxImageView_scaledIntensity, mScaledIntensities));
if (customAttrs.hasValue(R.styleable.ParallaxImageView_tiltSensitivity))
setTiltSensitivity(customAttrs.getFloat(R.styleable.ParallaxImageView_tiltSensitivity,
mSensorInterpreter.getTiltSensitivity()));
if (customAttrs.hasValue(R.styleable.ParallaxImageView_forwardTiltOffset))
setForwardTiltOffset(customAttrs.getFloat(R.styleable.ParallaxImageView_forwardTiltOffset,
mSensorInterpreter.getForwardTiltOffset()));
customAttrs.recycle();
}
}
// Configure matrix as early as possible by posting to MessageQueue
post(new Runnable() {
@Override
public void run() {
configureMatrix();
}
});
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
configureMatrix();
}
/**
* Sets the intensity of the parallax effect. The stronger the effect, the more distance
* the image will have to move around.
*
* @param parallaxIntensity the new intensity
*/
public void setParallaxIntensity(float parallaxIntensity) {
if (parallaxIntensity < 1)
throw new IllegalArgumentException("Parallax effect must have a intensity of 1.0 or greater");
mParallaxIntensity = parallaxIntensity;
configureMatrix();
}
/**
* Sets the parallax tilt sensitivity for the image view. The stronger the sensitivity,
* the more a given tilt will adjust the image and the smaller needed tilt to reach the
* image bounds.
*
* @param sensitivity the new tilt sensitivity
*/
public void setTiltSensitivity(float sensitivity) {
mSensorInterpreter.setTiltSensitivity(sensitivity);
}
/**
* Sets the forward tilt offset dimension, allowing for the image to be
* centered while the phone is "naturally" tilted forwards.
*
* @param forwardTiltOffset the new tilt forward adjustment
*/
public void setForwardTiltOffset(float forwardTiltOffset) {
if (Math.abs(forwardTiltOffset) > 1)
throw new IllegalArgumentException("Parallax forward tilt offset must be less than or equal to 1.0");
mSensorInterpreter.setForwardTiltOffset(forwardTiltOffset);
}
/**
* Sets whether translation should be limited to the image's bounds or should be limited
* to the smaller of the two axis' translation limits.
*
* @param scaledIntensities the scaledIntensities flag
*/
public void setScaledIntensities(boolean scaledIntensities) {
mScaledIntensities = scaledIntensities;
}
/**
* Sets the maximum percentage of the image that image matrix is allowed to translate
* for each sensor reading.
*
* @param maximumJump the new maximum jump
*/
public void setMaximumJump(float maximumJump) {
mMaximumJump = maximumJump;
}
/**
* Sets the image view's translation coordinates. These values must be between -1 and 1,
* representing the transaction percentage from the center.
*
* @param x the horizontal translation
* @param y the vertical translation
*/
private void setTranslate(float x, float y) {
if (Math.abs(x) > 1 || Math.abs(y) > 1)
throw new IllegalArgumentException("Parallax effect cannot translate more than 100% of its off-screen size");
float xScale, yScale;
if (mScaledIntensities) {
// Set both scales to their offset values
xScale = mXOffset;
yScale = mYOffset;
} else {
// Set both scales to the max offset (should be negative, so smaller absolute value)
xScale = Math.max(mXOffset, mYOffset);
yScale = Math.max(mXOffset, mYOffset);
}
// Make sure below maximum jump limit
if (mMaximumJump > 0) {
// Limit x jump
if (x - mXTranslation / xScale > mMaximumJump) {
x = mXTranslation / xScale + mMaximumJump;
} else if (x - mXTranslation / xScale < -mMaximumJump) {
x = mXTranslation / xScale - mMaximumJump;
}
// Limit y jump
if (y - mYTranslation / yScale > mMaximumJump) {
y = mYTranslation / yScale + mMaximumJump;
} else if (y - mYTranslation / yScale < -mMaximumJump) {
y = mYTranslation / yScale - mMaximumJump;
}
}
mXTranslation = x * xScale;
mYTranslation = y * yScale;
configureMatrix();
}
/**
* Configures the ImageView's imageMatrix to allow for movement of the
* source image.
*/
private void configureMatrix() {
if (getDrawable() == null || getWidth() == 0 || getHeight() == 0) return;
int dWidth = getDrawable().getIntrinsicWidth();
int dHeight = getDrawable().getIntrinsicHeight();
int vWidth = getWidth();
int vHeight = getHeight();
float scale;
float dx, dy;
if (dWidth * vHeight > vWidth * dHeight) {
scale = (float) vHeight / (float) dHeight;
mXOffset = (vWidth - dWidth * scale * mParallaxIntensity) * 0.5f;
mYOffset = (vHeight - dHeight * scale * mParallaxIntensity) * 0.5f;
} else {
scale = (float) vWidth / (float) dWidth;
mXOffset = (vWidth - dWidth * scale * mParallaxIntensity) * 0.5f;
mYOffset = (vHeight - dHeight * scale * mParallaxIntensity) * 0.5f;
}
dx = mXOffset + mXTranslation;
dy = mYOffset + mYTranslation;
mTranslationMatrix.set(getImageMatrix());
mTranslationMatrix.setScale(mParallaxIntensity * scale, mParallaxIntensity * scale);
mTranslationMatrix.postTranslate(dx, dy);
setImageMatrix(mTranslationMatrix);
}
/**
* Registers a sensor manager with the parallax ImageView. Should be called in onResume
* from an Activity or Fragment.
*
*/
@SuppressWarnings("deprecation")
public void registerSensorManager() {
if (getContext() == null || mSensorManager != null) return;
// Acquires a sensor manager
mSensorManager = (SensorManager) getContext().getSystemService(Context.SENSOR_SERVICE);
if (mSensorManager != null) {
mSensorManager.registerListener(this,
mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION),
SensorManager.SENSOR_DELAY_FASTEST);
}
}
/**
* Unregisters the ParallaxImageView's SensorManager. Should be called in onPause from
* an Activity or Fragment to avoid continuing sensor usage.
*/
public void unregisterSensorManager() {
unregisterSensorManager(false);
}
/**
* Unregisters the ParallaxImageView's SensorManager. Should be called in onPause from
* an Activity or Fragment to avoid continuing sensor usage.
* @param resetTranslation if the image translation should be reset to the origin
*/
public void unregisterSensorManager(boolean resetTranslation) {
if (mSensorManager == null) return;
mSensorManager.unregisterListener(this);
mSensorManager = null;
if (resetTranslation) {
setTranslate(0, 0);
}
}
@Override
public void onSensorChanged(SensorEvent event) {
final float [] vectors = mSensorInterpreter.interpretSensorEvent(getContext(), event);
// Return if interpretation of data failed
if (vectors == null) return;
// Set translation on ImageView matrix
setTranslate(vectors[2], vectors[1]);
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) { }
}