Parallax with accelerometers without using libraries - android

I want a custom view that moves depending on the accelerometers and creating the parallax effect.
Now I have the custom view listening the accelerometers values but, how I can use these values to move the view properly?
code:
public class ParallaxView extends AppCompatImageView
implements SensorEventListener {
private static final int SENSOR_DELAY = SensorManager.SENSOR_DELAY_FASTEST;
//...
public ParallaxView(Context context) {
super(context);
}
public ParallaxView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ParallaxView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void init() {
WindowManager windowManager = (WindowManager) getContext().getSystemService(WINDOW_SERVICE);
mDisplay = windowManager.getDefaultDisplay();
mSensorManager = (SensorManager) getContext().getSystemService(SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
}
public void setNewPosition(
#Nullable Float sensorX,
#Nullable Float sensorY) {
// ???
}
//...
#Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER){
setNewPosition(event.values[0], event.values[1]);
}
}
#Override
public void onAccuracyChanged(Sensor sensor, int i) {
}
public void registerSensorListener() {
mSensorManager.registerListener(this, mAccelerometer, SENSOR_DELAY);
}
public void unregisterSensorListener() {
mSensorManager.unregisterListener(this);
}
}
Use this view in the Activity:
#Override
protected void onCreate(Bundle savedInstanceState) {
//...
mParallaxView.init();
}
#Override
protected void onResume() {
mParallaxView.registerSensorListener();
super.onResume();
}
#Override
protected void onPause() {
mParallaxView.unregisterSensorListener();
super.onPause();
}
Thanks in advance

Finally I created my custom view for get what I want.
Here the repository: https://github.com/GVMarc/ParallaxView
Here the code:
public class ParallaxView extends AppCompatImageView implements SensorEventListener {
private static final int DEFAULT_SENSOR_DELAY = SensorManager.SENSOR_DELAY_FASTEST;
public static final int DEFAULT_MOVEMENT_MULTIPLIER = 3;
public static final int DEFAULT_MIN_MOVED_PIXELS = 1;
private static final float DEFAULT_MIN_SENSIBILITY = 0;
private float mMovementMultiplier = DEFAULT_MOVEMENT_MULTIPLIER;
private int mSensorDelay = DEFAULT_SENSOR_DELAY;
private int mMinMovedPixelsToUpdate = DEFAULT_MIN_MOVED_PIXELS;
private float mMinSensibility = DEFAULT_MIN_SENSIBILITY;
private float mSensorX;
private float mSensorY;
private Float mFirstSensorX;
private Float mFirstSensorY;
private Float mPreviousSensorX;
private Float mPreviousSensorY;
private float mTranslationX = 0;
private float mTranslationY = 0;
private SensorManager mSensorManager;
private Sensor mAccelerometer;
public enum SensorDelay {
FASTEST,
GAME,
UI,
NORMAL
}
public ParallaxView(Context context) {
super(context);
}
public ParallaxView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ParallaxView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void init() {
mSensorManager = (SensorManager) getContext().getSystemService(SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
}
private void setNewPosition() {
int destinyX = (int) ((mFirstSensorX - mSensorX) * mMovementMultiplier);
int destinyY = (int) ((mFirstSensorY - mSensorY) * mMovementMultiplier);
calculateTranslationX(destinyX);
calculateTranslationY(destinyY);
}
private void calculateTranslationX(int destinyX) {
if (mTranslationX + mMinMovedPixelsToUpdate < destinyX)
mTranslationX++;
else if (mTranslationX - mMinMovedPixelsToUpdate > destinyX)
mTranslationX--;
}
private void calculateTranslationY(int destinyY) {
if (mTranslationY + mMinMovedPixelsToUpdate < destinyY)
mTranslationY++;
else if (mTranslationY - mMinMovedPixelsToUpdate > destinyY)
mTranslationY--;
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
setTranslationX(mTranslationX);
setTranslationY(mTranslationY);
invalidate();
}
#Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
mSensorX = event.values[0];
mSensorY = -event.values[1];
manageSensorValues();
}
}
private void manageSensorValues() {
if (mFirstSensorX == null)
setFirstSensorValues();
if (mPreviousSensorX == null || isSensorValuesMovedEnough()) {
setNewPosition();
setPreviousSensorValues();
}
}
private void setFirstSensorValues() {
mFirstSensorX = mSensorX;
mFirstSensorY = mSensorY;
}
private void setPreviousSensorValues() {
mPreviousSensorX = mSensorX;
mPreviousSensorY = mSensorY;
}
private boolean isSensorValuesMovedEnough() {
return mSensorX > mPreviousSensorX + mMinSensibility ||
mSensorX < mPreviousSensorX - mMinSensibility ||
mSensorY > mPreviousSensorY + mMinSensibility ||
mSensorY < mPreviousSensorX - mMinSensibility;
}
public void registerSensorListener() {
mSensorManager.registerListener(this, mAccelerometer, mSensorDelay);
}
public void registerSensorListener(SensorDelay sensorDelay) {
switch (sensorDelay) {
case FASTEST:
mSensorDelay = SensorManager.SENSOR_DELAY_FASTEST;
break;
case GAME:
mSensorDelay = SensorManager.SENSOR_DELAY_GAME;
break;
case UI:
mSensorDelay = SensorManager.SENSOR_DELAY_UI;
break;
case NORMAL:
mSensorDelay = SensorManager.SENSOR_DELAY_NORMAL;
break;
}
registerSensorListener();
}
public void unregisterSensorListener() {
mSensorManager.unregisterListener(this);
}
public void setMovementMultiplier(float multiplier) {
mMovementMultiplier = multiplier;
}
public void setMinimumMovedPixelsToUpdate(int minMovedPixelsToUpdate) {
mMinMovedPixelsToUpdate = minMovedPixelsToUpdate;
}
public void setMinimumSensibility(int minSensibility) {
mMinSensibility = minSensibility;
}
#Override
public void onAccuracyChanged(Sensor sensor, int i) {
}
}

Here is a ParallaxImageView class I have used before:
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 yourpackagename.R;
/**
* I did not write this, I just cant remember where I got it from and thought it could be useful for others
*/
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.15f;
/**
* 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) { }
}
SensorInterpreter
import android.content.Context;
import android.hardware.SensorEvent;
import android.view.Surface;
import android.view.WindowManager;
/**
* I did not write this, I just cant remember where I got it from and thought it could be useful for others
*/
public class SensorInterpreter {
private static final String TAG = SensorInterpreter.class.getName();
private float[] mVectors;
private float mTiltSensitivity = 2.0f;
private float mForwardTiltOffset = 0.3f;
public SensorInterpreter() {
mVectors = new float[3];
}
public final float[] interpretSensorEvent(Context context, SensorEvent event) {
if (event == null || event.values.length < 3 || event.values[0] == 0
|| event.values[1] == 0 || event.values[2] == 0)
return null;
// Acquire rotation of screen
final int rotation = ((WindowManager) context
.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay()
.getRotation();
// Adjust for forward tilt based on screen orientation
switch (rotation) {
case Surface.ROTATION_90:
mVectors[0] = event.values[0];
mVectors[1] = event.values[2];
mVectors[2] = -event.values[1];
break;
case Surface.ROTATION_180:
mVectors[0] = event.values[0];
mVectors[1] = event.values[1];
mVectors[2] = event.values[2];
break;
case Surface.ROTATION_270:
mVectors[0] = event.values[0];
mVectors[1] = -event.values[2];
mVectors[2] = event.values[1];
break;
default:
mVectors[0] = event.values[0];
mVectors[1] = -event.values[1];
mVectors[2] = -event.values[2];
break;
}
// Adjust roll for sensitivity differences based on pitch
// double tiltScale = 1/Math.cos(mVectors[1] * Math.PI/180);
// if (tiltScale > 12) tiltScale = 12;
// if (tiltScale < -12) tiltScale = -12;
// mVectors[2] *= tiltScale;
// Make roll and pitch percentages out of 1
mVectors[1] /= 90;
mVectors[2] /= 90;
// Add in forward tilt offset
mVectors[1] -= mForwardTiltOffset;
if (mVectors[1] < -1)
mVectors[1] += 2;
// Adjust for tilt sensitivity
mVectors[1] *= mTiltSensitivity;
mVectors[2] *= mTiltSensitivity;
// Clamp values to image bounds
if (mVectors[1] > 1)
mVectors[1] = 1f;
if (mVectors[1] < -1)
mVectors[1] = -1f;
if (mVectors[2] > 1)
mVectors[2] = 1f;
if (mVectors[2] < -1)
mVectors[2] = -1f;
return mVectors;
}
public float getForwardTiltOffset() {
return mForwardTiltOffset;
}
public void setForwardTiltOffset(float forwardTiltOffset) {
mForwardTiltOffset = forwardTiltOffset;
}
public float getTiltSensitivity() {
return mTiltSensitivity;
}
public void setTiltSensitivity(float tiltSensitivity) {
mTiltSensitivity = tiltSensitivity;
}
Add this to attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ParallaxImageView">
<attr name="intensity" format="float" />
<attr name="tiltSensitivity" format="float" />
<attr name="forwardTiltOffset" format="float" />
<attr name="scaledIntensity" format="boolean" />
</declare-styleable>
Add this to layout xml
<yourpackagenamehere.ParallaxImageView
android:id="#+id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop" />
Add this to Activity class
private ParallaxImageView background;
background = (ParallaxImageView) findViewById(R.id.background);
background.setImageResource(R.drawable.main_back);
#Override
public void onResume() {
background.registerSensorManager();
super.onResume();
}
#Override
public void onPause() {
background.unregisterSensorManager();
super.onPause();
}

Related

Android Custom View Edge Clipping with ripple animation

I'm using a custom view to get ripple effect for the pre-lollipop devices. But I also need to customize the container shape like a curved shape.I want to be the button like this.
As you can see in the second and third buttons when we tap the view the ripple effect animation goes outside of the container view. So how to resolve this?
Please note that I want this ripple effect for the Kitkat version with the ability to change the ripple color. So is this possible?
Here is my custom view which is used for the ripple effect
public class MyRippleView extends FrameLayout {
private int WIDTH;
private int HEIGHT;
private int frameRate = 10;
private int rippleDuration = 400;
private int rippleAlpha = 90;
private Handler canvasHandler;
private float radiusMax = 0;
private boolean animationRunning = false;
private int timer = 0;
private int timerEmpty = 0;
private int durationEmpty = -1;
private float x = -1;
private float y = -1;
private int zoomDuration;
private float zoomScale;
private ScaleAnimation scaleAnimation;
private Boolean hasToZoom;
private Boolean isCentered;
private Integer rippleType;
private Paint paint;
private Bitmap originBitmap;
private int rippleColor;
private int ripplePadding;
private GestureDetector gestureDetector;
private final Runnable runnable = new Runnable() {
#Override
public void run() {
invalidate();
}
};
private OnRippleCompleteListener onCompletionListener;
public MyRippleView(Context context) {
super(context);
}
public MyRippleView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public MyRippleView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
/**
* Method that initializes all fields and sets listeners
*
* #param context Context used to create this view
* #param attrs Attribute used to initialize fields
*/
private void init(final Context context, final AttributeSet attrs) {
if (isInEditMode())
return;
final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RippleView);
rippleColor = typedArray.getColor(R.styleable.RippleView_rv_color, getResources().getColor(R.color.rippelColor));
rippleType = typedArray.getInt(R.styleable.RippleView_rv_type, 0);
hasToZoom = typedArray.getBoolean(R.styleable.RippleView_rv_zoom, false);
isCentered = typedArray.getBoolean(R.styleable.RippleView_rv_centered, false);
rippleDuration = typedArray.getInteger(R.styleable.RippleView_rv_rippleDuration, rippleDuration);
frameRate = typedArray.getInteger(R.styleable.RippleView_rv_framerate, frameRate);
rippleAlpha = typedArray.getInteger(R.styleable.RippleView_rv_alpha, rippleAlpha);
ripplePadding = typedArray.getDimensionPixelSize(R.styleable.RippleView_rv_ripplePadding, 0);
canvasHandler = new Handler();
zoomScale = typedArray.getFloat(R.styleable.RippleView_rv_zoomScale, 1.03f);
zoomDuration = typedArray.getInt(R.styleable.RippleView_rv_zoomDuration, 200);
typedArray.recycle();
paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setColor(rippleColor);
paint.setAlpha(rippleAlpha);
this.setWillNotDraw(false);
gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
#Override
public void onLongPress(MotionEvent event) {
super.onLongPress(event);
animateRipple(event);
sendClickEvent(true);
}
#Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return true;
}
#Override
public boolean onSingleTapUp(MotionEvent e) {
return true;
}
});
this.setDrawingCacheEnabled(true);
this.setClickable(true);
}
#Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (animationRunning) {
if (rippleDuration <= timer * frameRate) {
animationRunning = false;
timer = 0;
durationEmpty = -1;
timerEmpty = 0;
canvas.restore();
invalidate();
if (onCompletionListener != null) onCompletionListener.onComplete(this);
return;
} else
canvasHandler.postDelayed(runnable, frameRate);
if (timer == 0)
canvas.save();
canvas.drawCircle(x, y, (radiusMax * (((float) timer * frameRate) / rippleDuration)), paint);
paint.setColor(Color.parseColor("#ffff4444"));
if (rippleType == 1 && originBitmap != null && (((float) timer * frameRate) / rippleDuration) > 0.4f) {
if (durationEmpty == -1)
durationEmpty = rippleDuration - timer * frameRate;
timerEmpty++;
final Bitmap tmpBitmap = getCircleBitmap((int) ((radiusMax) * (((float) timerEmpty * frameRate) / (durationEmpty))));
canvas.drawBitmap(tmpBitmap, 0, 0, paint);
tmpBitmap.recycle();
}
paint.setColor(rippleColor);
if (rippleType == 1) {
if ((((float) timer * frameRate) / rippleDuration) > 0.6f)
paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timerEmpty * frameRate) / (durationEmpty)))));
else
paint.setAlpha(rippleAlpha);
}
else
paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timer * frameRate) / rippleDuration))));
timer++;
}
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
WIDTH = w;
HEIGHT = h;
scaleAnimation = new ScaleAnimation(1.0f, zoomScale, 1.0f, zoomScale, w / 2, h / 2);
scaleAnimation.setDuration(zoomDuration);
scaleAnimation.setRepeatMode(Animation.REVERSE);
scaleAnimation.setRepeatCount(1);
}
/**
* Launch Ripple animation for the current view with a MotionEvent
*
* #param event MotionEvent registered by the Ripple gesture listener
*/
public void animateRipple(MotionEvent event) {
createAnimation(event.getX(), event.getY());
}
/**
* Launch Ripple animation for the current view centered at x and y position
*
* #param x Horizontal position of the ripple center
* #param y Vertical position of the ripple center
*/
public void animateRipple(final float x, final float y) {
createAnimation(x, y);
}
/**
* Create Ripple animation centered at x, y
*
* #param x Horizontal position of the ripple center
* #param y Vertical position of the ripple center
*/
private void createAnimation(final float x, final float y) {
if (this.isEnabled() && !animationRunning) {
if (hasToZoom)
this.startAnimation(scaleAnimation);
radiusMax = Math.max(WIDTH, HEIGHT);
if (rippleType != 2)
radiusMax /= 2;
radiusMax -= ripplePadding;
if (isCentered || rippleType == 1) {
this.x = getMeasuredWidth() / 2;
this.y = getMeasuredHeight() / 2;
} else {
this.x = x;
this.y = y;
}
animationRunning = true;
if (rippleType == 1 && originBitmap == null)
originBitmap = getDrawingCache(true);
invalidate();
}
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (gestureDetector.onTouchEvent(event)) {
animateRipple(event);
sendClickEvent(false);
}
return super.onTouchEvent(event);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
this.onTouchEvent(event);
return super.onInterceptTouchEvent(event);
}
/**
* Send a click event if parent view is a Listview instance
*
* #param isLongClick Is the event a long click ?
*/
private void sendClickEvent(final Boolean isLongClick) {
if (getParent() instanceof AdapterView) {
final AdapterView adapterView = (AdapterView) getParent();
final int position = adapterView.getPositionForView(this);
final long id = adapterView.getItemIdAtPosition(position);
if (isLongClick) {
if (adapterView.getOnItemLongClickListener() != null)
adapterView.getOnItemLongClickListener().onItemLongClick(adapterView, this, position, id);
} else {
if (adapterView.getOnItemClickListener() != null)
adapterView.getOnItemClickListener().onItemClick(adapterView, this, position, id);
}
}
}
private Bitmap getCircleBitmap(final int radius) {
final Bitmap output = Bitmap.createBitmap(originBitmap.getWidth(), originBitmap.getHeight(), Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(output);
final Paint paint = new Paint();
final Rect rect = new Rect((int)(x - radius), (int)(y - radius), (int)(x + radius), (int)(y + radius));
paint.setAntiAlias(true);
canvas.drawARGB(0, 0, 0, 0);
canvas.drawCircle(x, y, radius, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(originBitmap, rect, rect, paint);
return output;
}
/**
* Set Ripple color, default is #FFFFFF
*
* #param rippleColor New color resource
*/
#ColorRes
public void setRippleColor(int rippleColor) {
this.rippleColor = getResources().getColor(rippleColor);
}
public int getRippleColor() {
return rippleColor;
}
public RippleType getRippleType()
{
return RippleType.values()[rippleType];
}
/**
* Set Ripple type, default is RippleType.SIMPLE
*
* #param rippleType New Ripple type for next animation
*/
public void setRippleType(final RippleType rippleType)
{
this.rippleType = rippleType.ordinal();
}
public Boolean isCentered()
{
return isCentered;
}
/**
* Set if ripple animation has to be centered in its parent view or not, default is False
*
* #param isCentered
*/
public void setCentered(final Boolean isCentered)
{
this.isCentered = isCentered;
}
public int getRipplePadding()
{
return ripplePadding;
}
/**
* Set Ripple padding if you want to avoid some graphic glitch
*
* #param ripplePadding New Ripple padding in pixel, default is 0px
*/
public void setRipplePadding(int ripplePadding)
{
this.ripplePadding = ripplePadding;
}
public Boolean isZooming()
{
return hasToZoom;
}
/**
* At the end of Ripple effect, the child views has to zoom
*
* #param hasToZoom Do the child views have to zoom ? default is False
*/
public void setZooming(Boolean hasToZoom)
{
this.hasToZoom = hasToZoom;
}
public float getZoomScale()
{
return zoomScale;
}
/**
* Scale of the end animation
*
* #param zoomScale Value of scale animation, default is 1.03f
*/
public void setZoomScale(float zoomScale)
{
this.zoomScale = zoomScale;
}
public int getZoomDuration()
{
return zoomDuration;
}
/**
* Duration of the ending animation in ms
*
* #param zoomDuration Duration, default is 200ms
*/
public void setZoomDuration(int zoomDuration)
{
this.zoomDuration = zoomDuration;
}
public int getRippleDuration()
{
return rippleDuration;
}
/**
* Duration of the Ripple animation in ms
*
* #param rippleDuration Duration, default is 400ms
*/
public void setRippleDuration(int rippleDuration)
{
this.rippleDuration = rippleDuration;
}
public int getFrameRate()
{
return frameRate;
}
/**
* Set framerate for Ripple animation
*
* #param frameRate New framerate value, default is 10
*/
public void setFrameRate(int frameRate)
{
this.frameRate = frameRate;
}
public int getRippleAlpha()
{
return rippleAlpha;
}
/**
* Set alpha for ripple effect color
*
* #param rippleAlpha Alpha value between 0 and 255, default is 90
*/
public void setRippleAlpha(int rippleAlpha)
{
this.rippleAlpha = rippleAlpha;
}
public void setOnRippleCompleteListener(OnRippleCompleteListener listener) {
this.onCompletionListener = listener;
}
/**
* Defines a callback called at the end of the Ripple effect
*/
public interface OnRippleCompleteListener {
void onComplete(MyRippleView rippleView);
}
public enum RippleType {
SIMPLE(0),
DOUBLE(1),
RECTANGLE(2);
int type;
RippleType(int type)
{
this.type = type;
}
}
}
In the layout XML file
<FrameLayout
android:background="#drawable/curved_button"
android:layout_width="match_parent"
android:layout_height="50dp">
<com.package.MyRippleView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:rv_color="#color/colorAccent"
rv_centered="true">
</com.package.MyRippleView>
</FrameLayout>
Curved Shape
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item >
<shape android:shape="rectangle" >
<corners android:radius="40dip" />
<stroke android:width="1dp" android:color="#FF9A00" />
</shape>
</item>
It's possible. The easiest way is to use Carbon which does such things just like that. I was able to recreate your button using only xml and run it on Gingerbread.
<carbon.widget.Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rounded with ripple"
android:textColor="#color/carbon_amber_700"
app:carbon_cornerRadius="100dp"
app:carbon_backgroundTint="#color/carbon_white"
app:carbon_rippleColor="#40ff0000"
app:carbon_stroke="#color/carbon_amber_700"
app:carbon_strokeWidth="2dp" />
The downside is that Carbon is huge and you may not want to include it just for that one button.
If you wish to do that by yourself, you should use a path and a PorterDuff mode to clip your button to a rounded rect.
private float cornerRadius;
private Path cornersMask;
private static PorterDuffXfermode pdMode = new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
private void initCorners() {
cornersMask = new Path();
cornersMask.addRoundRect(new RectF(0, 0, getWidth(), getHeight()), cornerRadius, cornerRadius, Path.Direction.CW);
cornersMask.setFillType(Path.FillType.INVERSE_WINDING);
}
#Override
public void draw(#NonNull Canvas canvas) {
int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
super.draw(canvas);
paint.setXfermode(pdMode);
canvas.drawPath(cornersMask, paint);
canvas.restoreToCount(saveCount);
paint.setXfermode(null);
}
And you should probably use ViewOutlineProvider on Lollipop to use native stuff where possible.

How to pause canvas from rotating for 2 secs at specific angles?

I made a rotating knob ,but I want to stop the knob at specific angles for 2 seconds. I want to stop it on 260f and -20f.
Can anyone suggest how to do it ?
This is the code from a blog. I made many changes according to my requirements.
public class RotatoryKnobView extends ImageView {
private float angle = -20f;
private float theta_old=0f;
private RotaryKnobListener listener;
public interface RotaryKnobListener {
public void onKnobChanged(float arg);
}
public void setKnobListener(RotaryKnobListener l )
{
listener = l;
}
public RotatoryKnobView(Context context) {
super(context);
initialize();
}
public RotatoryKnobView(Context context, AttributeSet attrs)
{
super(context, attrs);
initialize();
}
public RotatoryKnobView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
initialize();
}
private float getTheta(float x, float y)
{
float sx = x - (getWidth() / 2.0f);
float sy = y - (getHeight() / 2.0f);
float length = (float)Math.sqrt( sx*sx + sy*sy);
float nx = sx / length;
float ny = sy / length;
float theta = (float)Math.atan2( ny, nx );
final float rad2deg = (float)(180.0/Math.PI);
float thetaDeg = theta*rad2deg;
return (thetaDeg < 0) ? thetaDeg + 360.0f : thetaDeg;
}
public void initialize()
{
this.setImageResource(R.drawable.rotoron);
setOnTouchListener(new OnTouchListener()
{
#Override
public boolean onTouch(View v, MotionEvent event) {
float x = event.getX(0);
float y = event.getY(0);
float theta = getTheta(x,y);
switch(event.getAction() & MotionEvent.ACTION_MASK)
{
case MotionEvent.ACTION_POINTER_DOWN:
theta_old = theta;
break;
case MotionEvent.ACTION_MOVE:
invalidate();
float delta_theta = theta - theta_old;
theta_old = theta;
int direction = (delta_theta > 0) ? 1 : -1;
angle += 5*direction;
notifyListener(angle+20);
break;
}
return true;
}
});
}
private void notifyListener(float arg)
{
if (null!=listener)
listener.onKnobChanged(arg);
}
protected void onDraw(Canvas c)
{if(angle==257f){
try {
synchronized (c) {
c.wait(5000);
angle=260f;
}
} catch (InterruptedException e) {
}
}
else if(angle==-16f)
{
try {
synchronized (c) {
c.wait(5000);
angle=-20f;
}
} catch (InterruptedException e) {
}
}
else
if(angle>260f)
{
angle=-20f;
}
else if(angle<-20f)
{
angle=260f;
}
else{
c.rotate(angle,getWidth()/2,getHeight()/2);
}
super.onDraw(c);
}
}
You may set a fixed angle and use postDelayed to clear it after 2 seconds.
public class RotatoryKnobView extends ImageView {
private float angle = -20f;
private float theta_old=0f;
private RotaryKnobListener listener;
private Float fixedAngle;
private float settleAngle;
private Runnable unsetFixedAngle = new Runnable() {
#Override
public void run() {
angle = settleAngle;
fixedAngle = null;
invalidate();
}
};
public interface RotaryKnobListener {
public void onKnobChanged(float arg);
}
public void setKnobListener(RotaryKnobListener l )
{
listener = l;
}
public RotatoryKnobView(Context context) {
super(context);
initialize();
}
public RotatoryKnobView(Context context, AttributeSet attrs)
{
super(context, attrs);
initialize();
}
public RotatoryKnobView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
initialize();
}
private float getTheta(float x, float y)
{
float sx = x - (getWidth() / 2.0f);
float sy = y - (getHeight() / 2.0f);
float length = (float)Math.sqrt( sx*sx + sy*sy);
float nx = sx / length;
float ny = sy / length;
float theta = (float)Math.atan2( ny, nx );
final float rad2deg = (float)(180.0/Math.PI);
float thetaDeg = theta*rad2deg;
return (thetaDeg < 0) ? thetaDeg + 360.0f : thetaDeg;
}
public void initialize()
{
this.setImageResource(R.drawable.rotoron);
setOnTouchListener(new OnTouchListener()
{
#Override
public boolean onTouch(View v, MotionEvent event) {
float x = event.getX(0);
float y = event.getY(0);
float theta = getTheta(x,y);
switch(event.getAction() & MotionEvent.ACTION_MASK)
{
case MotionEvent.ACTION_POINTER_DOWN:
theta_old = theta;
break;
case MotionEvent.ACTION_MOVE:
invalidate();
float delta_theta = theta - theta_old;
theta_old = theta;
int direction = (delta_theta > 0) ? 1 : -1;
angle += 5*direction;
notifyListener(angle+20);
break;
}
return true;
}
});
}
private void notifyListener(float arg)
{
if (null!=listener)
listener.onKnobChanged(arg);
}
void setFixedAngle(float angle, float settleAngle) {
fixedAngle = angle;
this.settleAngle = settleAngle;
postDelayed(unsetFixedAngle, 2000);
}
protected void onDraw(Canvas c)
{
if(fixedAngle==null) {
if (angle > 270) {
setFixedAngle(270, -15);
} else if (angle < -20f) {
setFixedAngle(-20, 260);
}
}
Log.d("angle", "angle: " + angle + " fixed angle: " + fixedAngle);
c.rotate(fixedAngle == null ? angle : fixedAngle,getWidth()/2,getHeight()/2);
super.onDraw(c);
}
}
`
I think the ultimate answer here is to implement your own class by extending SurfaceView and then overriding onDraw( Canvas canvas )
You can then use the Canvas routines to render your control.
There are a lot of good examples out there if you google.
To get started initialize the surface view:
// So things actually render
setDrawingCacheEnabled(true);
setWillNotDraw(false);
setZOrderOnTop(true);
// Controls the drawing thread.
getHolder().addCallback(new CallbackSurfaceView());
Override onDraw and add your rendering routines. You can layer them
as you go.
public void onDraw(Canvas canvas) {
// Always Draw
super.onDraw(canvas);
drawBackground(canvas);
drawKnobIndentWell(canvas);
drawKnob(canvas);
drawKnobLED( canvas ); //etc....
}
An example of a Callback and an update thread:
/**
* This is the drawing callback.
* It handles the creation and destruction of the drawing thread when the
* surface for drawing is created and destroyed.
*/
class CallbackSurfaceView implements SurfaceHolder.Callback {
Thread threadIndeterminant;
RunnableProgressUpdater runnableUpdater;
boolean done = false;
/**
* Kills the running thread.
*/
public void done() {
done = true;
if (null != runnableUpdater) {
runnableUpdater.done();
}
}
/**
* Causes the UI to render once.
*/
public void needRedraw() {
if (runnableUpdater != null) {
runnableUpdater.needRedraw();
}
}
/**
* When the surface is created start the drawing thread.
* #param holder
*/
#Override
public void surfaceCreated(SurfaceHolder holder) {
if (!done) {
threadIndeterminant = new Thread(runnableUpdater = new RunnableProgressUpdater());
threadIndeterminant.start();
}
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
/**
* When the surface is destroyed stop the drawing thread.
* #param holder
*/
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (null != runnableUpdater) {
runnableUpdater.done();
threadIndeterminant = null;
runnableUpdater = null;
}
}
}
/**
* This is the runnable for the drawing operations. It is started and stopped by the callback class.
*/
class RunnableProgressUpdater implements Runnable {
boolean surfaceExists = true;
boolean needRedraw = false;
public void done() {
surfaceExists = false;
}
public void needRedraw() {
needRedraw = true;
}
#Override
public void run() {
canvasDrawAndPost();
while (surfaceExists) {
// Renders continuously during a download operation.
// Otherwise only renders when requested.
// Necessary so that progress bar and cirlce activity update.
if (syncContext.isRunning()) {
canvasDrawAndPost();
needRedraw = true;
} else if (needRedraw) {
canvasDrawAndPost();
needRedraw = false;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// Don't care
}
}
// One final update
canvasDrawAndPost();
}
/**
* Routine the redraws the controls on each loop.
*/
private synchronized void canvasDrawAndPost() {
Canvas canvas = getHolder().lockCanvas();
if (canvas != null) {
try {
draw(canvas);
} finally {
getHolder().unlockCanvasAndPost(canvas);
}
}
}
}
If you decide to go this route you can customize your control from XML using
custom values.
<com.killerknob.graphics.MultimeterVolumeControl
android:id="#+id/volume_control"
android:layout_below="#id/divider_one"
android:background="#android:color/white"
android:layout_width="match_parent"
android:layout_height="60dp"
android:minHeight="60dp"
custom:ledShadow="#357BBB"
custom:ledColor="#357BBB"
custom:knobBackground="#color/gray_level_13"
custom:knobColor="#android:color/black"
/>
When you create a custom control you reference it by its package name.
You create custom variable in a resource file under /values and then reference
them in your class.
More details here:
http://developer.android.com/training/custom-views/create-view.html
This may be more work then you want to do, but I think you will end up with a more professional looking control and the animations will be smoother.
At any rate, looks like a fun project. Good Luck.

Error inflating class <unknown> - runtime exception

Does someone know what might be the source of an error which occurs randomly, without being able to identify a clear scenario?
java.lang.RuntimeException: Unable to start activity ComponentInfo {com.xxx/com.xxx.activities.SplashActivity}: android.view.InflateException: Binary XML file line #38: Error inflating class <unknown>
A fragment from the layout is posted below:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/dark_gray">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="#drawable/xl" />
<TextView
android:id="#+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:padding="10dp"
android:text="xxx"
android:textColor="#FFF"
android:textSize="24sp" />
</LinearLayout>
<RelativeLayout
android:id="#+id/welcomeContent"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.xxx.utils.ParallaxViewPager // line 38
android:id="#+id/parallaxviewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/bg_parallax">
The class com.xxx.utils.ParallaxViewPager exists and I can catch the error very randomly.
public class ParallaxViewPager extends ViewPager {
public static final int FIT_WIDTH = 0;
public static final int FIT_HEIGHT = 1;
public static final float OVERLAP_FULL = 1f;
public static final float OVERLAP_HALF = 0.5f;
public static final float OVERLAP_QUARTER = 0.25f;
private static final float CORRECTION_PERCENTAGE = 0.01f;
public Bitmap bitmap;
private Rect source, destination;
private int scaleType;
private int chunkWidth;
private int projectedWidth;
private float overlap;
private OnPageChangeListener secondOnPageChangeListener;
private TextView[] dots;
private ScrollerCustomDuration mScroller = null;
float mStartDragX;
OnSwipeOutListener mListener;
public ParallaxViewPager(Context context) {
super(context);
init();
}
public ParallaxViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
init();
postInitViewPager();
}
public void setScrollDurationFactor(double scrollFactor) {
mScroller.setScrollDurationFactor(scrollFactor);
}
public void setOnSwipeOutListener(OnSwipeOutListener listener) {
mListener = listener;
}
private void init() {
source = new Rect();
destination = new Rect();
scaleType = FIT_HEIGHT;
overlap = OVERLAP_HALF;
setOnPageChangeListener(new OnPageChangeListener() {
#Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (position < 3) {
if (bitmap != null) {
source.left = (int) Math.floor((position + positionOffset - CORRECTION_PERCENTAGE) * chunkWidth);
source.right = (int) Math.ceil((position + positionOffset + CORRECTION_PERCENTAGE) * chunkWidth + projectedWidth);
destination.left = (int) Math.floor((position + positionOffset - CORRECTION_PERCENTAGE) * getWidth());
destination.right = (int) Math.ceil((position + positionOffset + 1 + CORRECTION_PERCENTAGE) * getWidth());
invalidate();
}
if (secondOnPageChangeListener != null) {
secondOnPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
}
}
}
#Override public void onPageSelected(int position) {
if (secondOnPageChangeListener != null) {
secondOnPageChangeListener.onPageSelected(position);
}
for (int i = 0; i < 3; i++) {
dots[i].setTextColor(getResources().getColor(android.R.color.darker_gray));
}
if (position < 3) {
dots[position].setTextColor(getResources().getColor(R.color.tw__solid_white));
}
if (position == 3) {
mListener.onSwipeOutAtEnd();
}
}
#Override public void onPageScrollStateChanged(int state) {
if (secondOnPageChangeListener != null) {
secondOnPageChangeListener.onPageScrollStateChanged(state);
}
}
});
}
#Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
destination.top = 0;
destination.bottom = h;
if (getAdapter() != null && bitmap != null)
calculateParallaxParameters();
}
private void calculateParallaxParameters() {
if (bitmap.getWidth() < getWidth() && bitmap.getWidth() < bitmap.getHeight() && scaleType == FIT_HEIGHT) {
Log.w(ParallaxViewPager.class.getName(), "Invalid bitmap bounds for the current device, parallax effect will not work.");
}
final float ratio = (float) getHeight() / bitmap.getHeight();
if (ratio != 1) {
switch (scaleType) {
case FIT_WIDTH:
source.top = (int) ((bitmap.getHeight() - bitmap.getHeight() / ratio) / 2);
source.bottom = bitmap.getHeight() - source.top;
chunkWidth = (int) Math.ceil((float) bitmap.getWidth() / (float) getAdapter().getCount());
projectedWidth = chunkWidth;
break;
case FIT_HEIGHT:
default:
source.top = 0;
source.bottom = bitmap.getHeight();
projectedWidth = (int) Math.ceil(getWidth() / ratio);
chunkWidth = (int) Math.ceil((bitmap.getWidth() - projectedWidth) / (float) getAdapter().getCount() * overlap);
break;
}
}
}
/**
* Sets the background from a resource file.
*
* #param resid
*/
#Override public void setBackgroundResource(int resid) {
bitmap = BitmapFactory.decodeResource(getResources(), resid);
}
/**
* Sets the background from a Drawable.
*
* #param background
*/
#Override public void setBackground(Drawable background) {
bitmap = ((BitmapDrawable) background).getBitmap();
}
/**
* Deprecated.
* Sets the background from a Drawable.
*
* #param background
*/
#Override public void setBackgroundDrawable(Drawable background) {
bitmap = ((BitmapDrawable) background).getBitmap();
}
/**
* Sets the background from a bitmap.
*
* #param bitmap
* #return The ParallaxViewPager object itself.
*/
public ParallaxViewPager setBackground(Bitmap bitmap) {
this.bitmap = bitmap;
return this;
}
/**
* Sets how the view should scale the background. The available choices are:
* <ul>
* <li>FIT_HEIGHT - the height of the image is resized to matched the height of the View, also stretching the width to keep the aspect ratio. The non-visible part of the bitmap is divided into equal parts, each of them sliding in at the proper position.</li>
* <li>FIT_WIDTH - the width of the background image is divided into equal chunks, each taking up the whole width of the screen.</li>
* </ul>
*
* #param scaleType
* #return
*/
public ParallaxViewPager setScaleType(final int scaleType) {
if (scaleType != FIT_WIDTH && scaleType != FIT_HEIGHT)
throw new IllegalArgumentException("Illegal argument: scaleType must be FIT_WIDTH or FIT_HEIGHT");
this.scaleType = scaleType;
return this;
}
/**
* Sets the amount of overlapping with the setOverlapPercentage(final float percentage) method. This is a number between 0 and 1, the smaller it is, the slower is the background scrolling.
*
* #param percentage
* #return The ParallaxViewPager object itself.
*/
public ParallaxViewPager setOverlapPercentage(final float percentage) {
if (percentage <= 0 || percentage >= 1)
throw new IllegalArgumentException("Illegal argument: percentage must be between 0 and 1");
overlap = percentage;
return this;
}
/**
* Recalculates the parameters of the parallax effect, useful after changes in runtime.
*
* #return The ParallaxViewPager object itself.
*/
public ParallaxViewPager invalidateParallaxParameters() {
calculateParallaxParameters();
return this;
}
#Override protected void onDraw(Canvas canvas) {
if (bitmap != null)
canvas.drawBitmap(bitmap, source, destination, null);
}
public void addOnPageChangeListener(OnPageChangeListener listener) {
secondOnPageChangeListener = listener;
}
public TextView[] getDots() {
return dots;
}
public void setDots(TextView[] dots) {
this.dots = dots;
}
private void postInitViewPager() {
try {
Class<?> viewpager = ViewPager.class;
Field scroller = viewpager.getDeclaredField("mScroller");
scroller.setAccessible(true);
Field interpolator = viewpager.getDeclaredField("sInterpolator");
interpolator.setAccessible(true);
mScroller = new ScrollerCustomDuration(getContext(),
(Interpolator) interpolator.get(null));
scroller.set(this, mScroller);
} catch (Exception e) {
Log.e("MyPager", e.getMessage());
}
}
class ScrollerCustomDuration extends Scroller {
private double mScrollFactor = 2;
public ScrollerCustomDuration(Context context) {
super(context);
}
public ScrollerCustomDuration(Context context, Interpolator interpolator) {
super(context, interpolator);
}
public ScrollerCustomDuration(Context context,
Interpolator interpolator, boolean flywheel) {
super(context, interpolator, flywheel);
}
/**
* Set the factor by which the duration will change
*/
public void setScrollDurationFactor(double scrollFactor) {
mScrollFactor = scrollFactor;
}
#Override
public void startScroll(int startX, int startY, int dx, int dy,
int duration) {
super.startScroll(startX, startY, dx, dy,
(int) (duration * mScrollFactor));
}
}
public interface OnSwipeOutListener {
public void onSwipeOutAtEnd();
}}
Thanks for your input.

Android zoom and pan and child drag and drop on long click

I have the following situation:
ZoomAndPanLayout
|
+---> ImageView
|
+---> FrameLayout (DragLayer)
|
+---> One or more controls. A view with a circle drawn on it.
With some minor issues because I don't care for now about screen bound the ZoomAndPanLayout works. I implemented ZoomAndPan like this:
public class ZoomAndPanLayout extends FrameLayout {
//region Constants
public static final float DEFAULT_MIN_SCALE_FACTOR = 1.0f;
public static final float DEFAULT_MAX_SCALE_FACTOR = 5.0f;
// endregion Constants
// region Fields
private float translationX = 0;
private float translationY = 0;
private float pivotX = 0;
private float pivotY = 0;
private float oldX;
private float oldY;
private float scaleFactor = 1.0f;
private float minScaleFactor = ZoomAndPanLayout.DEFAULT_MIN_SCALE_FACTOR;
private float maxScaleFactor = ZoomAndPanLayout.DEFAULT_MAX_SCALE_FACTOR;
private ScaleGestureDetector scaleGestureDetector = null;
// endregion Fields
// region Constructor
public ZoomAndPanLayout(Context context) {
super(context);
this.initialize(context);
}
public ZoomAndPanLayout(Context context, AttributeSet attrs) {
super(context, attrs);
this.initialize(context);
}
public ZoomAndPanLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
this.initialize(context);
}
private void initialize(Context context) {
this.scaleGestureDetector = new ScaleGestureDetector(context, new ScaleGestureListener());
}
// endregion Constructor
#Override
public boolean onTouchEvent(MotionEvent event) {
this.scaleGestureDetector.onTouchEvent(event);
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
{
this.oldX = event.getX();
this.oldY = event.getY();
break;
}
case MotionEvent.ACTION_MOVE:
{
if (!this.scaleGestureDetector.isInProgress())
{
float x = event.getX();
float y = event.getY();
float deltaX = x - this.oldX;
float deltaY = y - this.oldY;
this.translationX += deltaX;
this.translationY += deltaY;
this.applyTransformations();
this.oldX = x;
this.oldY = y;
}
}
}
return true;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return this.scaleGestureDetector.onTouchEvent(event);
}
private void applyTransformations() {
final View child = this.getChildAt(0);
if (child != null)
{
child.setPivotX(this.pivotX);
child.setPivotY(this.pivotY);
child.setScaleX(this.scaleFactor);
child.setScaleY(this.scaleFactor);
// TODO: bound child to screen limits
child.setTranslationX(this.translationX);
child.setTranslationY(this.translationY);
}
}
public Rect getChildRect() {
View child = this.getChildAt(0);
if (child != null)
{
Rect outRect = new Rect();
outRect.right = (int) (child.getWidth() * child.getScaleX());
outRect.bottom = (int) (child.getHeight() * child.getScaleY());
int[] location = new int[2];
child.getLocationOnScreen(location);
outRect.offset(location[0], location[1]);
return outRect;
}
else
{
return new Rect(0, 0, 0, 0);
}
}
// region Private Inner Enums, Interfaces and Classes
private class ScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
private ZoomAndPanLayout upper = ZoomAndPanLayout.this;
#Override
public boolean onScale(ScaleGestureDetector detector) {
float newScaleFactor = detector.getScaleFactor();
float originalScaleFactor = upper.scaleFactor;
upper.scaleFactor *= newScaleFactor;
// Bound the scaleFactor to the min and max limits
if (upper.scaleFactor >= upper.maxScaleFactor)
{
upper.scaleFactor = upper.maxScaleFactor;
newScaleFactor = upper.maxScaleFactor / originalScaleFactor;
}
else if (upper.scaleFactor * newScaleFactor <= upper.minScaleFactor)
{
upper.scaleFactor = upper.minScaleFactor;
newScaleFactor = upper.minScaleFactor / originalScaleFactor;
}
// set pivot
View child = upper.getChildAt(0);
if (child != null)
{
if (newScaleFactor * child.getWidth() * upper.scaleFactor <= originalScaleFactor * child.getWidth()
|| newScaleFactor * child.getHeight() * upper.scaleFactor <= originalScaleFactor * child.getWidth())
{
upper.pivotX = newScaleFactor * child.getWidth() / 2;
upper.pivotY = newScaleFactor * child.getHeight() / 2;
}
else
{
upper.pivotX = detector.getFocusX();
upper.pivotY = detector.getFocusY();
}
}
upper.applyTransformations();
return true;
}
}
// endregion Private Inner Enums, Interfaces and Classes
}
When I create each child of DragLayer I assign to them a OnLongClickListener, but the god damn thing dose not fire. when I long click on any child of DragLayer.
Any idea how can I implement this using both my idea for ZoomAndPanLayout or any idea. If you ask yourself why I need ZoomAndPanLayout, it is because I must be able to zoom and pan any layout not just an ImageView.
Any idea?
Probably because you return true in onTouchEvent method.
When true is returned it consumed an event and children don't receive their own onTouchEvent.
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return this.scaleGestureDetector.onTouchEvent(event);
}
always return true and it also blocks children from receiving an event.

Get a ball to roll around inside another circle

I'm trying to get a ball to bounce and roll around inside another ball, ultimately based on the accelerometer. There are countless tutorials out there to detect circle collisions and such, and they are indeed marginally helpful. Unfortunately, none that I have found deal with circle-inside-of-a-circle collision only circles bouncing around in a rectangle view.
Several helpful URLs are, where I got most of this code:
http://xiangchen.wordpress.com/2011/12/17/an-android-accelerometer-example/
circle-circle collision
..but again, this isn't quite what I'm after.
I want to get a circle to bounce and roll around inside of another circle. Then, after that, I will want the inner ball to roll down the inside of the outer circle at the right time as the velocity lessens, not simply bounce to the bottom. Am I articulating that clearly? And finally, the bounce angle will need to be adjusted I'm sure so I will ultimately need to figure out how to do that as well.
My code is a mess because I've tried so many things, so in particular, the commented block isn't even close to what it needs to be I don't think. It is just my latest attempt.
Anyone know a little something about this and willing to give me a hand? I would appreciate it.
Edit: This guy is very close to what I'm after, but I am having trouble making sense of it and converting the selected answer into Java. Help? https://gamedev.stackexchange.com/questions/29650/circle-inside-circle-collision
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class Main extends Activity implements SensorEventListener {
private SensorManager mSensorManager;
private Sensor mAccelerometer;
private ShapeView mShapeView;
private int mWidthScreen;
private int mHeightScreen;
private final float FACTOR_FRICTION = 0.2f; // imaginary friction on the screen
private final float GRAVITY = 9.8f; // acceleration of gravity
private float mAx; // acceleration along x axis
private float mAy; // acceleration along y axis
private final float mDeltaT = 0.5f; // imaginary time interval between each acceleration updates
private static final float OUTERSTROKE = 5;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
DisplayMetrics displaymetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
mWidthScreen = displaymetrics.widthPixels;
mHeightScreen = displaymetrics.heightPixels;
mShapeView = new ShapeView(this);
mShapeView.initOvalCenter((int) (mWidthScreen * 0.6), (int) (mHeightScreen * 0.6));
setContentView(mShapeView);
}
#Override
public void onSensorChanged(SensorEvent event) {
// obtain the three accelerations from sensors
mAx = event.values[0];
mAy = event.values[1];
float mAz = event.values[2];
// taking into account the frictions
mAx = Math.signum(mAx) * Math.abs(mAx) * (1 - FACTOR_FRICTION * Math.abs(mAz) / GRAVITY);
mAy = Math.signum(mAy) * Math.abs(mAy) * (1 - FACTOR_FRICTION * Math.abs(mAz) / GRAVITY);
}
#Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
#Override
protected void onResume() {
super.onResume();
// start sensor sensing
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);
}
#Override
protected void onPause() {
super.onPause();
// stop sensor sensing
mSensorManager.unregisterListener(this);
}
// the view that renders the ball
private class ShapeView extends SurfaceView implements SurfaceHolder.Callback {
private final int BALLRADIUS = 100;
private final float FACTOR_BOUNCEBACK = 0.15f;
private final int OUTERRADIUS = 300;
private Point ballCenter = new Point();
private RectF mRectF;
private final Paint mPaint;
private ShapeThread mThread;
private float mVx;
private float mVy;
private final Paint outerPaint;
private RectF outerBounds;
private Point outerCenter;
private final double outerDiagonal;
public ShapeView(Context context) {
super(context);
getHolder().addCallback(this);
mThread = new ShapeThread(getHolder(), this);
setFocusable(true);
mPaint = new Paint();
mPaint.setColor(0xFFFFFFFF);
mPaint.setAlpha(192);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
outerPaint= new Paint();
outerPaint.setColor(0xFFFFFFFF);
outerPaint.setAlpha(255);
outerPaint.setStrokeWidth(OUTERSTROKE);
outerPaint.setStyle(Paint.Style.STROKE);
outerPaint.setAntiAlias(true);
mRectF = new RectF();
outerDiagonal= Math.pow(BALLRADIUS - OUTERRADIUS, 2);
}
public void initOvalCenter(int x, int y) {
mShapeView.setOvalCenter(x, y);
outerCenter= new Point(x, y);
outerBounds = new RectF(x - OUTERRADIUS, y - OUTERRADIUS, x + OUTERRADIUS, y + OUTERRADIUS);
}
public boolean setOvalCenter(int x, int y) {
ballCenter.set(x, y);
return true;
}
public boolean updateOvalCenter() {
/*-------
* This is where the trouble is, currently. How do I "snap" the inner circle back into the
* outer circle? Or even better, how do I keep it from crossing the line to bring with?
*/
mVx -= mAx * mDeltaT;
mVy += mAy * mDeltaT;
Point newBallCenter = new Point();
newBallCenter.x = ballCenter.x + (int) (mDeltaT * (mVx + 0.5 * mAx * mDeltaT));
newBallCenter.y = ballCenter.y + (int) (mDeltaT * (mVy + 0.5 * mAy * mDeltaT));
double distance = Math.sqrt(Math.pow(newBallCenter.x - outerCenter.x, 2) + Math.pow(newBallCenter.y - outerCenter.y, 2));
if(distance >= OUTERRADIUS - BALLRADIUS) {
mVx = -mVx * FACTOR_BOUNCEBACK;
mVy = -mVy * FACTOR_BOUNCEBACK;
newBallCenter.x = ballCenter.x + (int) (mDeltaT * (mVx + 0.5 * mAx * mDeltaT));
newBallCenter.y = ballCenter.y + (int) (mDeltaT * (mVy + 0.5 * mAy * mDeltaT));
}
ballCenter.x = newBallCenter.x;
ballCenter.y = newBallCenter.y;
return true;
}
protected void doDraw(Canvas canvas) {
if (mRectF != null && canvas != null) {
mRectF.set(ballCenter.x - BALLRADIUS, ballCenter.y - BALLRADIUS, ballCenter.x + BALLRADIUS, ballCenter.y + BALLRADIUS);
canvas.drawColor(0XFF000000);
canvas.drawOval(mRectF, mPaint);
canvas.drawOval(outerBounds, outerPaint);
}
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
#Override
public void surfaceCreated(SurfaceHolder holder) {
mThread.setRunning(true);
mThread.start();
}
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
boolean retry = true;
mThread.setRunning(false);
while (retry) {
try {
mThread.join();
retry = false;
} catch (InterruptedException ignored) { }
}
}
}
class ShapeThread extends Thread {
private final SurfaceHolder mSurfaceHolder;
private ShapeView mShapeView;
private boolean mRun = false;
public ShapeThread(SurfaceHolder surfaceHolder, ShapeView shapeView) {
mSurfaceHolder = surfaceHolder;
mShapeView = shapeView;
}
public void setRunning(boolean run) {
mRun = run;
}
#Override
public void run() {
Canvas c;
while (mRun) {
mShapeView.updateOvalCenter();
c = null;
try {
c = mSurfaceHolder.lockCanvas(null);
synchronized (mSurfaceHolder) {
mShapeView.doDraw(c);
}
} finally {
if (c != null) {
mSurfaceHolder.unlockCanvasAndPost(c);
}
}
}
}
}
}
I thought i would answer this to at least help you even if it doesnt answer your question completely.
Below is your code with changes to a few things
i fixed your issue with snapping the inner circle back when a collision occurs.
Basically you need to move the inner circle back by one frame once a collision has occured. i think you tried doing this however you were resetting these values just before the collision check. I also added a little check to the velocity to say if it was less than 0.5 then just move the inner circle to the last frame without a bounce to get rid of the judering bouncing effect when it is trying to settle.
package com.test.circleincircle;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class Main extends Activity implements SensorEventListener {
private SensorManager mSensorManager;
private Sensor mAccelerometer;
private ShapeView mShapeView;
private int mWidthScreen;
private int mHeightScreen;
private final float FACTOR_FRICTION = 0.2f; // imaginary friction on the screen
private final float GRAVITY = 9.8f; // acceleration of gravity
private float mAx; // acceleration along x axis
private float mAy; // acceleration along y axis
private final float mDeltaT = 0.5f; // imaginary time interval between each acceleration updates
private int previousInnerX, previousInnerY;
private static final float OUTERSTROKE = 5;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
DisplayMetrics displaymetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
mWidthScreen = displaymetrics.widthPixels;
mHeightScreen = displaymetrics.heightPixels;
mShapeView = new ShapeView(this);
mShapeView.initOvalCenter((int) (mWidthScreen * 0.6), (int) (mHeightScreen * 0.6));
setContentView(mShapeView);
}
#Override
public void onSensorChanged(SensorEvent event) {
// obtain the three accelerations from sensors
mAx = event.values[0];
mAy = event.values[1];
float mAz = event.values[2];
// taking into account the frictions
mAx = Math.signum(mAx) * Math.abs(mAx) * (1 - FACTOR_FRICTION * Math.abs(mAz) / GRAVITY);
mAy = Math.signum(mAy) * Math.abs(mAy) * (1 - FACTOR_FRICTION * Math.abs(mAz) / GRAVITY);
}
#Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
#Override
protected void onResume() {
super.onResume();
// start sensor sensing
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);
}
#Override
protected void onPause() {
super.onPause();
// stop sensor sensing
mSensorManager.unregisterListener(this);
}
// the view that renders the ball
private class ShapeView extends SurfaceView implements SurfaceHolder.Callback {
private final int BALLRADIUS = 100;
private final float FACTOR_BOUNCEBACK = 0.45f;
private final int OUTERRADIUS = 300;
private Point ballCenter = new Point();
private RectF mRectF;
private final Paint mPaint;
private ShapeThread mThread;
private float mVx;
private float mVy;
private final Paint outerPaint;
private RectF outerBounds;
private Point outerCenter;
private final double outerDiagonal;
public ShapeView(Context context) {
super(context);
getHolder().addCallback(this);
mThread = new ShapeThread(getHolder(), this);
setFocusable(true);
mPaint = new Paint();
mPaint.setColor(0xFFFFFFFF);
mPaint.setAlpha(192);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
outerPaint= new Paint();
outerPaint.setColor(0xFFFFFFFF);
outerPaint.setAlpha(255);
outerPaint.setStrokeWidth(OUTERSTROKE);
outerPaint.setStyle(Paint.Style.STROKE);
outerPaint.setAntiAlias(true);
mRectF = new RectF();
outerDiagonal= Math.pow(BALLRADIUS - OUTERRADIUS, 2);
}
public void initOvalCenter(int x, int y) {
mShapeView.setOvalCenter(x, y);
outerCenter= new Point(x, y);
outerBounds = new RectF(x - OUTERRADIUS, y - OUTERRADIUS, x + OUTERRADIUS, y + OUTERRADIUS);
}
public boolean setOvalCenter(int x, int y) {
ballCenter.set(x, y);
return true;
}
public boolean updateOvalCenter() {
Point newBallCenter = new Point();
newBallCenter.x = ballCenter.x + (int) (mDeltaT * (mVx + 0.5 * mAx * mDeltaT));
newBallCenter.y = ballCenter.y + (int) (mDeltaT * (mVy + 0.5 * mAy * mDeltaT));
double distance = Math.sqrt(Math.pow(newBallCenter.x - outerCenter.x, 2) + Math.pow(newBallCenter.y - outerCenter.y, 2));
if(distance >= OUTERRADIUS - BALLRADIUS) {
mVx = -mVx * FACTOR_BOUNCEBACK;
mVy = -mVy * FACTOR_BOUNCEBACK;
if(Math.abs(mVx) > 0.5)
{
newBallCenter.x = previousInnerX + (int) (mDeltaT * (mVx + 0.5 * mAx * mDeltaT));
newBallCenter.y = previousInnerY + (int) (mDeltaT * (mVy + 0.5 * mAy * mDeltaT));
}
else
{
newBallCenter.x = previousInnerX;
newBallCenter.y = previousInnerY;
}
}
else
{
mVx -= mAx * mDeltaT;
mVy += mAy * mDeltaT;
}
previousInnerX = ballCenter.x;
previousInnerY = ballCenter.y;
ballCenter.x = newBallCenter.x;
ballCenter.y = newBallCenter.y;
return true;
}
protected void doDraw(Canvas canvas) {
if (mRectF != null && canvas != null) {
mRectF.set(ballCenter.x - BALLRADIUS, ballCenter.y - BALLRADIUS, ballCenter.x + BALLRADIUS, ballCenter.y + BALLRADIUS);
canvas.drawColor(0XFF000000);
canvas.drawOval(mRectF, mPaint);
canvas.drawOval(outerBounds, outerPaint);
}
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
#Override
public void surfaceCreated(SurfaceHolder holder) {
mThread.setRunning(true);
mThread.start();
}
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
boolean retry = true;
mThread.setRunning(false);
while (retry) {
try {
mThread.join();
retry = false;
} catch (InterruptedException ignored) { }
}
}
}
class ShapeThread extends Thread {
private final SurfaceHolder mSurfaceHolder;
private ShapeView mShapeView;
private boolean mRun = false;
public ShapeThread(SurfaceHolder surfaceHolder, ShapeView shapeView) {
mSurfaceHolder = surfaceHolder;
mShapeView = shapeView;
}
public void setRunning(boolean run) {
mRun = run;
}
#Override
public void run() {
Canvas c;
while (mRun) {
mShapeView.updateOvalCenter();
c = null;
try {
c = mSurfaceHolder.lockCanvas(null);
synchronized (mSurfaceHolder) {
mShapeView.doDraw(c);
}
} finally {
if (c != null) {
mSurfaceHolder.unlockCanvasAndPost(c);
}
}
}
}
}
}
Adding in the smooth movement of the iner circle rolling within the outer one while also bouncing will be a lot more difficult to implement. The correct way would be to have the inner circle rotating and following the instructions from the question you reference.
Maybe you can ask a seperate question for that part after you are happy with the bouncing.
If anything this may just help you along your journey and hopefully you will be able to add to this.

Categories

Resources