/* * 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]; } }; } }