/*
* Copyright 2014 Soichiro Kashima
*
* 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.
*/
package de.sourcestream.movieDB.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ScrollView;
import java.util.ArrayList;
import de.sourcestream.movieDB.R;
import de.sourcestream.movieDB.helper.ObservableScrollViewCallbacks;
import de.sourcestream.movieDB.helper.ScrollState;
import de.sourcestream.movieDB.helper.Scrollable;
public class ObservableParallaxScrollView extends ScrollView implements Scrollable {
private static final int DEFAULT_PARALLAX_VIEWS = 1;
private static final float DEFAULT_INNER_PARALLAX_FACTOR = 1.9F;
private static final float DEFAULT_PARALLAX_FACTOR = 1.9F;
private static final float DEFAULT_ALPHA_FACTOR = -1F;
private int numOfParallaxViews = DEFAULT_PARALLAX_VIEWS;
private float innerParallaxFactor = DEFAULT_PARALLAX_FACTOR;
private float parallaxFactor = DEFAULT_PARALLAX_FACTOR;
private float alphaFactor = DEFAULT_ALPHA_FACTOR;
private ArrayList<ParallaxedView> parallaxedViews = new ArrayList<ParallaxedView>();
// Fields that should be saved onSaveInstanceState
private int mPrevScrollY;
private int mScrollY;
// Fields that don't need to be saved onSaveInstanceState
private ObservableScrollViewCallbacks mCallbacks;
private ScrollState mScrollState;
private boolean mFirstScroll;
private boolean mDragging;
private boolean mIntercepted;
private MotionEvent mPrevMoveEvent;
private ViewGroup mTouchInterceptionViewGroup;
public ObservableParallaxScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
public ObservableParallaxScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public ObservableParallaxScrollView(Context context) {
super(context);
}
protected void init(Context context, AttributeSet attrs) {
TypedArray typeArray = context.obtainStyledAttributes(attrs, R.styleable.ParallaxScroll);
this.parallaxFactor = typeArray.getFloat(R.styleable.ParallaxScroll_parallax_factor, DEFAULT_PARALLAX_FACTOR);
this.alphaFactor = typeArray.getFloat(R.styleable.ParallaxScroll_alpha_factor, DEFAULT_ALPHA_FACTOR);
this.innerParallaxFactor = typeArray.getFloat(R.styleable.ParallaxScroll_inner_parallax_factor, DEFAULT_INNER_PARALLAX_FACTOR);
this.numOfParallaxViews = typeArray.getInt(R.styleable.ParallaxScroll_parallax_views_num, DEFAULT_PARALLAX_VIEWS);
typeArray.recycle();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
makeViewsParallax();
}
private void makeViewsParallax() {
if (getChildCount() > 0 && getChildAt(0) instanceof ViewGroup) {
ViewGroup viewsHolder = (ViewGroup) getChildAt(0);
int numOfParallaxViews = Math.min(this.numOfParallaxViews, viewsHolder.getChildCount());
for (int i = 0; i < numOfParallaxViews; i++) {
ParallaxedView parallaxedView = new ScrollViewParallaxedItem(viewsHolder.getChildAt(i));
parallaxedViews.add(parallaxedView);
}
}
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
float parallax = parallaxFactor;
float alpha = alphaFactor;
for (ParallaxedView parallaxedView : parallaxedViews) {
parallaxedView.setOffset((float) t / parallax);
parallax *= innerParallaxFactor;
if (alpha != DEFAULT_ALPHA_FACTOR) {
float fixedAlpha = (t <= 0) ? 1 : (100 / ((float) t * alpha));
parallaxedView.setAlpha(fixedAlpha);
alpha /= alphaFactor;
}
parallaxedView.animateNow();
}
if (mCallbacks != null) {
mScrollY = t;
mCallbacks.onScrollChanged(t, mFirstScroll, mDragging);
if (mFirstScroll) {
mFirstScroll = false;
}
if (mPrevScrollY < t) {
mScrollState = ScrollState.UP;
} else if (t < mPrevScrollY) {
mScrollState = ScrollState.DOWN;
//} else {
// Keep previous state while dragging.
// Never makes it STOP even if scrollY not changed.
// Before Android 4.4, onTouchEvent calls onScrollChanged directly for ACTION_MOVE,
// which makes mScrollState always STOP when onUpOrCancelMotionEvent is called.
// STOP state is now meaningless for ScrollView.
}
mPrevScrollY = t;
}
}
protected class ScrollViewParallaxedItem extends ParallaxedView {
public ScrollViewParallaxedItem(View view) {
super(view);
}
@Override
protected void translatePreICS(View view, float offset) {
view.offsetTopAndBottom((int) offset - lastOffset);
lastOffset = (int) offset;
}
}
@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
mPrevScrollY = ss.prevScrollY;
mScrollY = ss.scrollY;
super.onRestoreInstanceState(ss.getSuperState());
}
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.prevScrollY = mPrevScrollY;
ss.scrollY = mScrollY;
return ss;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mCallbacks != null) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// Whether or not motion events are consumed by children,
// flag initializations which are related to ACTION_DOWN events should be executed.
// Because if the ACTION_DOWN is consumed by children and only ACTION_MOVEs are
// passed to parent (this view), the flags will be invalid.
// Also, applications might implement initialization codes to onDownMotionEvent,
// so call it here.
mFirstScroll = mDragging = true;
mCallbacks.onDownMotionEvent();
break;
}
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mCallbacks != null) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIntercepted = false;
mDragging = false;
mCallbacks.onUpOrCancelMotionEvent(mScrollState);
break;
case MotionEvent.ACTION_MOVE:
if (mPrevMoveEvent == null) {
mPrevMoveEvent = ev;
}
float diffY = ev.getY() - mPrevMoveEvent.getY();
mPrevMoveEvent = MotionEvent.obtainNoHistory(ev);
if (getCurrentScrollY() - diffY <= 0) {
// Can't scroll anymore.
if (mIntercepted) {
// Already dispatched ACTION_DOWN event to parents, so stop here.
return false;
}
// Apps can set the interception target other than the direct parent.
final ViewGroup parent;
if (mTouchInterceptionViewGroup == null) {
parent = (ViewGroup) getParent();
} else {
parent = mTouchInterceptionViewGroup;
}
// Get offset to parents. If the parent is not the direct parent,
// we should aggregate offsets from all of the parents.
float offsetX = 0;
float offsetY = 0;
for (View v = this; v != null && v != parent; v = (View) v.getParent()) {
offsetX += v.getLeft() - v.getScrollX();
offsetY += v.getTop() - v.getScrollY();
}
final MotionEvent event = MotionEvent.obtainNoHistory(ev);
event.offsetLocation(offsetX, offsetY);
if (parent.onInterceptTouchEvent(event)) {
mIntercepted = true;
// If the parent wants to intercept ACTION_MOVE events,
// we pass ACTION_DOWN event to the parent
// as if these touch events just have began now.
event.setAction(MotionEvent.ACTION_DOWN);
// Return this onTouchEvent() first and set ACTION_DOWN event for parent
// to the queue, to keep events sequence.
post(new Runnable() {
@Override
public void run() {
parent.dispatchTouchEvent(event);
}
});
return false;
}
// Even when this can't be scrolled anymore,
// simply returning false here may cause subView's click,
// so delegate it to super.
return super.onTouchEvent(ev);
}
break;
}
}
return super.onTouchEvent(ev);
}
@Override
public void setScrollViewCallbacks(ObservableScrollViewCallbacks listener) {
mCallbacks = listener;
}
@Override
public void setTouchInterceptionViewGroup(ViewGroup viewGroup) {
mTouchInterceptionViewGroup = viewGroup;
}
@Override
public void scrollVerticallyTo(int y) {
scrollTo(0, y);
}
@Override
public int getCurrentScrollY() {
return mScrollY;
}
static class SavedState extends BaseSavedState {
int prevScrollY;
int scrollY;
/**
* Called by onSaveInstanceState.
*/
SavedState(Parcelable superState) {
super(superState);
}
/**
* Called by CREATOR.
*/
private SavedState(Parcel in) {
super(in);
prevScrollY = in.readInt();
scrollY = in.readInt();
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(prevScrollY);
out.writeInt(scrollY);
}
public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}