diff --git a/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/CacheFragmentStatePagerAdapter.java b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/CacheFragmentStatePagerAdapter.java new file mode 100755 index 0000000000..f0057410cb --- /dev/null +++ b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/CacheFragmentStatePagerAdapter.java @@ -0,0 +1,150 @@ +/* + * 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 com.github.ksoichiro.android.observablescrollview; + +import android.os.Bundle; +import android.os.Parcelable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.util.SparseArray; +import android.view.ViewGroup; + +/** + * FragmentStatePagerAdapter that caches each pages. + * FragmentStatePagerAdapter is also originally caches pages, + * but its keys are not public nor documented, so depending + * on how it create cache key is dangerous. + * This adapter caches pages by itself and provide getter method to the cache. + */ +public abstract class CacheFragmentStatePagerAdapter extends FragmentStatePagerAdapter { + + private static final String STATE_SUPER_STATE = "superState"; + private static final String STATE_PAGES = "pages"; + private static final String STATE_PAGE_INDEX_PREFIX = "pageIndex:"; + private static final String STATE_PAGE_KEY_PREFIX = "page:"; + + private FragmentManager mFm; + private SparseArray mPages; + + public CacheFragmentStatePagerAdapter(FragmentManager fm) { + super(fm); + mPages = new SparseArray(); + mFm = fm; + } + + @Override + public Parcelable saveState() { + Parcelable p = super.saveState(); + Bundle bundle = new Bundle(); + bundle.putParcelable(STATE_SUPER_STATE, p); + + bundle.putInt(STATE_PAGES, mPages.size()); + if (0 < mPages.size()) { + for (int i = 0; i < mPages.size(); i++) { + int position = mPages.keyAt(i); + bundle.putInt(createCacheIndex(i), position); + Fragment f = mPages.get(position); + mFm.putFragment(bundle, createCacheKey(position), f); + } + } + return bundle; + } + + @Override + public void restoreState(Parcelable state, ClassLoader loader) { + Bundle bundle = (Bundle) state; + int pages = bundle.getInt(STATE_PAGES); + if (0 < pages) { + for (int i = 0; i < pages; i++) { + int position = bundle.getInt(createCacheIndex(i)); + Fragment f = mFm.getFragment(bundle, createCacheKey(position)); + mPages.put(position, f); + } + } + + Parcelable p = bundle.getParcelable(STATE_SUPER_STATE); + super.restoreState(p, loader); + } + + /** + * Get a new Fragment instance. + * Each fragments are automatically cached in this method, + * so you don't have to do it by yourself. + * If you want to implement instantiation of Fragments, + * you should override {@link #createItem(int)} instead. + * + * {@inheritDoc} + * + * @param position position of the item in the adapter + * @return fragment instance + */ + @Override + public Fragment getItem(int position) { + Fragment f = createItem(position); + // We should cache fragments manually to access to them later + mPages.put(position, f); + return f; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + if (0 <= mPages.indexOfKey(position)) { + mPages.remove(position); + } + super.destroyItem(container, position, object); + } + + /** + * Get the item at the specified position in the adapter. + * + * @param position position of the item in the adapter + * @return fragment instance + */ + public Fragment getItemAt(int position) { + return mPages.get(position); + } + + /** + * Create a new Fragment instance. + * This is called inside {@link #getItem(int)}. + * + * @param position position of the item in the adapter + * @return fragment instance + */ + protected abstract Fragment createItem(int position); + + /** + * Create an index string for caching Fragment pages. + * + * @param index index of the item in the adapter + * @return key string for caching Fragment pages + */ + protected String createCacheIndex(int index) { + return STATE_PAGE_INDEX_PREFIX + index; + } + + /** + * Create a key string for caching Fragment pages. + * + * @param position position of the item in the adapter + * @return key string for caching Fragment pages + */ + protected String createCacheKey(int position) { + return STATE_PAGE_KEY_PREFIX + position; + } +} diff --git a/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ObservableGridView.java b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ObservableGridView.java new file mode 100755 index 0000000000..7b759726d4 --- /dev/null +++ b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ObservableGridView.java @@ -0,0 +1,384 @@ +/* + * 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 com.github.ksoichiro.android.observablescrollview; + +import android.content.Context; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.SparseIntArray; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.GridView; + +/** + * GridView that its scroll position can be observed. + */ +public class ObservableGridView extends GridView implements Scrollable { + + // Fields that should be saved onSaveInstanceState + private int mPrevFirstVisiblePosition; + private int mPrevFirstVisibleChildHeight = -1; + private int mPrevScrolledChildrenHeight; + private int mPrevScrollY; + private int mScrollY; + private SparseIntArray mChildrenHeights; + + // 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; + + private OnScrollListener mOriginalScrollListener; + private OnScrollListener mScrollListener = new OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (mOriginalScrollListener != null) { + mOriginalScrollListener.onScrollStateChanged(view, scrollState); + } + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + if (mOriginalScrollListener != null) { + mOriginalScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); + } + // AbsListView#invokeOnItemScrollListener calls onScrollChanged(0, 0, 0, 0) + // on Android 4.0+, but Android 2.3 is not. (Android 3.0 is unknown) + // So call it with onScrollListener. + onScrollChanged(); + } + }; + + public ObservableGridView(Context context) { + super(context); + init(); + } + + public ObservableGridView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ObservableGridView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + mPrevFirstVisiblePosition = ss.prevFirstVisiblePosition; + mPrevFirstVisibleChildHeight = ss.prevFirstVisibleChildHeight; + mPrevScrolledChildrenHeight = ss.prevScrolledChildrenHeight; + mPrevScrollY = ss.prevScrollY; + mScrollY = ss.scrollY; + mChildrenHeights = ss.childrenHeights; + super.onRestoreInstanceState(ss.getSuperState()); + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.prevFirstVisiblePosition = mPrevFirstVisiblePosition; + ss.prevFirstVisibleChildHeight = mPrevFirstVisibleChildHeight; + ss.prevScrolledChildrenHeight = mPrevScrolledChildrenHeight; + ss.prevScrollY = mPrevScrollY; + ss.scrollY = mScrollY; + ss.childrenHeights = mChildrenHeights; + 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 setOnScrollListener(OnScrollListener l) { + // Don't set l to super.setOnScrollListener(). + // l receives all events through mScrollListener. + mOriginalScrollListener = l; + } + + @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; + } + + private void init() { + mChildrenHeights = new SparseIntArray(); + super.setOnScrollListener(mScrollListener); + } + + private int getNumColumnsCompat() { + if (Build.VERSION.SDK_INT >= 11) { + return getNumColumns(); + } else { + int columns = 0; + if (getChildCount() > 0) { + int width = getChildAt(0).getMeasuredWidth(); + if (width > 0) { + columns = getWidth() / width; + } + } + return columns > 0 ? columns : AUTO_FIT; + } + } + + private void onScrollChanged() { + if (mCallbacks != null) { + if (getChildCount() > 0) { + int firstVisiblePosition = getFirstVisiblePosition(); + for (int i = getFirstVisiblePosition(), j = 0; i <= getLastVisiblePosition(); i++, j++) { + if (mChildrenHeights.indexOfKey(i) < 0 || getChildAt(j).getHeight() != mChildrenHeights.get(i)) { + if (i % getNumColumnsCompat() == 0) { + mChildrenHeights.put(i, getChildAt(j).getHeight()); + } + } + } + + View firstVisibleChild = getChildAt(0); + if (firstVisibleChild != null) { + if (mPrevFirstVisiblePosition < firstVisiblePosition) { + // scroll down + int skippedChildrenHeight = 0; + if (firstVisiblePosition - mPrevFirstVisiblePosition != 1) { + for (int i = firstVisiblePosition - 1; i > mPrevFirstVisiblePosition; i--) { + if (0 < mChildrenHeights.indexOfKey(i)) { + skippedChildrenHeight += mChildrenHeights.get(i); + } + } + } + mPrevScrolledChildrenHeight += mPrevFirstVisibleChildHeight + skippedChildrenHeight; + mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight(); + } else if (firstVisiblePosition < mPrevFirstVisiblePosition) { + // scroll up + int skippedChildrenHeight = 0; + if (mPrevFirstVisiblePosition - firstVisiblePosition != 1) { + for (int i = mPrevFirstVisiblePosition - 1; i > firstVisiblePosition; i--) { + if (0 < mChildrenHeights.indexOfKey(i)) { + skippedChildrenHeight += mChildrenHeights.get(i); + } + } + } + mPrevScrolledChildrenHeight -= firstVisibleChild.getHeight() + skippedChildrenHeight; + mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight(); + } else if (firstVisiblePosition == 0) { + mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight(); + } + if (mPrevFirstVisibleChildHeight < 0) { + mPrevFirstVisibleChildHeight = 0; + } + mScrollY = mPrevScrolledChildrenHeight - firstVisibleChild.getTop(); + mPrevFirstVisiblePosition = firstVisiblePosition; + + mCallbacks.onScrollChanged(mScrollY, mFirstScroll, mDragging); + if (mFirstScroll) { + mFirstScroll = false; + } + + if (mPrevScrollY < mScrollY) { + mScrollState = ScrollState.UP; + } else if (mScrollY < mPrevScrollY) { + mScrollState = ScrollState.DOWN; + } else { + mScrollState = ScrollState.STOP; + } + mPrevScrollY = mScrollY; + } + } + } + } + + static class SavedState extends BaseSavedState { + int prevFirstVisiblePosition; + int prevFirstVisibleChildHeight = -1; + int prevScrolledChildrenHeight; + int prevScrollY; + int scrollY; + SparseIntArray childrenHeights; + + /** + * Called by onSaveInstanceState. + */ + SavedState(Parcelable superState) { + super(superState); + } + + /** + * Called by CREATOR. + */ + private SavedState(Parcel in) { + super(in); + prevFirstVisiblePosition = in.readInt(); + prevFirstVisibleChildHeight = in.readInt(); + prevScrolledChildrenHeight = in.readInt(); + prevScrollY = in.readInt(); + scrollY = in.readInt(); + childrenHeights = new SparseIntArray(); + final int numOfChildren = in.readInt(); + if (0 < numOfChildren) { + for (int i = 0; i < numOfChildren; i++) { + final int key = in.readInt(); + final int value = in.readInt(); + childrenHeights.put(key, value); + } + } + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(prevFirstVisiblePosition); + out.writeInt(prevFirstVisibleChildHeight); + out.writeInt(prevScrolledChildrenHeight); + out.writeInt(prevScrollY); + out.writeInt(scrollY); + final int numOfChildren = childrenHeights == null ? 0 : childrenHeights.size(); + out.writeInt(numOfChildren); + if (0 < numOfChildren) { + for (int i = 0; i < numOfChildren; i++) { + out.writeInt(childrenHeights.keyAt(i)); + out.writeInt(childrenHeights.valueAt(i)); + } + } + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ObservableListView.java b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ObservableListView.java new file mode 100755 index 0000000000..1d20c5784e --- /dev/null +++ b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ObservableListView.java @@ -0,0 +1,381 @@ +/* + * 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 com.github.ksoichiro.android.observablescrollview; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.SparseIntArray; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.ListView; + +/** + * ListView that its scroll position can be observed. + */ +public class ObservableListView extends ListView implements Scrollable { + + // Fields that should be saved onSaveInstanceState + private int mPrevFirstVisiblePosition; + private int mPrevFirstVisibleChildHeight = -1; + private int mPrevScrolledChildrenHeight; + private int mPrevScrollY; + private int mScrollY; + private SparseIntArray mChildrenHeights; + + // 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; + + private OnScrollListener mOriginalScrollListener; + private OnScrollListener mScrollListener = new OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (mOriginalScrollListener != null) { + mOriginalScrollListener.onScrollStateChanged(view, scrollState); + } + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + if (mOriginalScrollListener != null) { + mOriginalScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); + } + // AbsListView#invokeOnItemScrollListener calls onScrollChanged(0, 0, 0, 0) + // on Android 4.0+, but Android 2.3 is not. (Android 3.0 is unknown) + // So call it with onScrollListener. + onScrollChanged(); + } + }; + + public ObservableListView(Context context) { + super(context); + init(); + } + + public ObservableListView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ObservableListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + mPrevFirstVisiblePosition = ss.prevFirstVisiblePosition; + mPrevFirstVisibleChildHeight = ss.prevFirstVisibleChildHeight; + mPrevScrolledChildrenHeight = ss.prevScrolledChildrenHeight; + mPrevScrollY = ss.prevScrollY; + mScrollY = ss.scrollY; + mChildrenHeights = ss.childrenHeights; + super.onRestoreInstanceState(ss.getSuperState()); + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.prevFirstVisiblePosition = mPrevFirstVisiblePosition; + ss.prevFirstVisibleChildHeight = mPrevFirstVisibleChildHeight; + ss.prevScrolledChildrenHeight = mPrevScrolledChildrenHeight; + ss.prevScrollY = mPrevScrollY; + ss.scrollY = mScrollY; + ss.childrenHeights = mChildrenHeights; + 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 setOnScrollListener(OnScrollListener l) { + // Don't set l to super.setOnScrollListener(). + // l receives all events through mScrollListener. + mOriginalScrollListener = l; + } + + @Override + public void setScrollViewCallbacks(ObservableScrollViewCallbacks listener) { + mCallbacks = listener; + } + + @Override + public void setTouchInterceptionViewGroup(ViewGroup viewGroup) { + mTouchInterceptionViewGroup = viewGroup; + } + + @Override + public void scrollVerticallyTo(int y) { + View firstVisibleChild = getChildAt(0); + if (firstVisibleChild != null) { + int baseHeight = firstVisibleChild.getHeight(); + int position = y / baseHeight; + setSelection(position); + } + } + + @Override + public int getCurrentScrollY() { + return mScrollY; + } + + private void init() { + mChildrenHeights = new SparseIntArray(); + super.setOnScrollListener(mScrollListener); + } + + private void onScrollChanged() { + if (mCallbacks != null) { + if (getChildCount() > 0) { + int firstVisiblePosition = getFirstVisiblePosition(); + for (int i = getFirstVisiblePosition(), j = 0; i <= getLastVisiblePosition(); i++, j++) { + if (mChildrenHeights.indexOfKey(i) < 0 || getChildAt(j).getHeight() != mChildrenHeights.get(i)) { + mChildrenHeights.put(i, getChildAt(j).getHeight()); + } + } + + View firstVisibleChild = getChildAt(0); + if (firstVisibleChild != null) { + if (mPrevFirstVisiblePosition < firstVisiblePosition) { + // scroll down + int skippedChildrenHeight = 0; + if (firstVisiblePosition - mPrevFirstVisiblePosition != 1) { + for (int i = firstVisiblePosition - 1; i > mPrevFirstVisiblePosition; i--) { + if (0 < mChildrenHeights.indexOfKey(i)) { + skippedChildrenHeight += mChildrenHeights.get(i); + } else { + // Approximate each item's height to the first visible child. + // It may be incorrect, but without this, scrollY will be broken + // when scrolling from the bottom. + skippedChildrenHeight += firstVisibleChild.getHeight(); + } + } + } + mPrevScrolledChildrenHeight += mPrevFirstVisibleChildHeight + skippedChildrenHeight; + mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight(); + } else if (firstVisiblePosition < mPrevFirstVisiblePosition) { + // scroll up + int skippedChildrenHeight = 0; + if (mPrevFirstVisiblePosition - firstVisiblePosition != 1) { + for (int i = mPrevFirstVisiblePosition - 1; i > firstVisiblePosition; i--) { + if (0 < mChildrenHeights.indexOfKey(i)) { + skippedChildrenHeight += mChildrenHeights.get(i); + } else { + // Approximate each item's height to the first visible child. + // It may be incorrect, but without this, scrollY will be broken + // when scrolling from the bottom. + skippedChildrenHeight += firstVisibleChild.getHeight(); + } + } + } + mPrevScrolledChildrenHeight -= firstVisibleChild.getHeight() + skippedChildrenHeight; + mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight(); + } else if (firstVisiblePosition == 0) { + mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight(); + } + if (mPrevFirstVisibleChildHeight < 0) { + mPrevFirstVisibleChildHeight = 0; + } + mScrollY = mPrevScrolledChildrenHeight - firstVisibleChild.getTop(); + mPrevFirstVisiblePosition = firstVisiblePosition; + + mCallbacks.onScrollChanged(mScrollY, mFirstScroll, mDragging); + if (mFirstScroll) { + mFirstScroll = false; + } + + if (mPrevScrollY < mScrollY) { + mScrollState = ScrollState.UP; + } else if (mScrollY < mPrevScrollY) { + mScrollState = ScrollState.DOWN; + } else { + mScrollState = ScrollState.STOP; + } + mPrevScrollY = mScrollY; + } + } + } + } + + static class SavedState extends BaseSavedState { + int prevFirstVisiblePosition; + int prevFirstVisibleChildHeight = -1; + int prevScrolledChildrenHeight; + int prevScrollY; + int scrollY; + SparseIntArray childrenHeights; + + /** + * Called by onSaveInstanceState. + */ + SavedState(Parcelable superState) { + super(superState); + } + + /** + * Called by CREATOR. + */ + private SavedState(Parcel in) { + super(in); + prevFirstVisiblePosition = in.readInt(); + prevFirstVisibleChildHeight = in.readInt(); + prevScrolledChildrenHeight = in.readInt(); + prevScrollY = in.readInt(); + scrollY = in.readInt(); + childrenHeights = new SparseIntArray(); + final int numOfChildren = in.readInt(); + if (0 < numOfChildren) { + for (int i = 0; i < numOfChildren; i++) { + final int key = in.readInt(); + final int value = in.readInt(); + childrenHeights.put(key, value); + } + } + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(prevFirstVisiblePosition); + out.writeInt(prevFirstVisibleChildHeight); + out.writeInt(prevScrolledChildrenHeight); + out.writeInt(prevScrollY); + out.writeInt(scrollY); + final int numOfChildren = childrenHeights == null ? 0 : childrenHeights.size(); + out.writeInt(numOfChildren); + if (0 < numOfChildren) { + for (int i = 0; i < numOfChildren; i++) { + out.writeInt(childrenHeights.keyAt(i)); + out.writeInt(childrenHeights.valueAt(i)); + } + } + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ObservableScrollView.java b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ObservableScrollView.java new file mode 100755 index 0000000000..d38c0e2d5b --- /dev/null +++ b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ObservableScrollView.java @@ -0,0 +1,252 @@ +/* + * 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 com.github.ksoichiro.android.observablescrollview; + +import android.content.Context; +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; + +/** + * ScrollView that its scroll position can be observed. + */ +public class ObservableScrollView extends ScrollView implements Scrollable { + + // 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 ObservableScrollView(Context context) { + super(context); + } + + public ObservableScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ObservableScrollView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @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 + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + 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; + } + } + + @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 CREATOR + = new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ObservableScrollViewCallbacks.java b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ObservableScrollViewCallbacks.java new file mode 100755 index 0000000000..4f7e9c621d --- /dev/null +++ b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ObservableScrollViewCallbacks.java @@ -0,0 +1,46 @@ +/* + * 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 com.github.ksoichiro.android.observablescrollview; + +/** + * Callbacks for Scrollable widgets. + */ +public interface ObservableScrollViewCallbacks { + /** + * Called when the scroll change events occurred. + * This won't be called just after the view is laid out, so if you'd like to + * initialize the position of your views with this method, you should call this manually + * or invoke scroll as appropriate. + * + * @param scrollY scroll position in Y axis + * @param firstScroll true when this is called for the first time in the consecutive motion events + * @param dragging true when the view is dragged and false when the view is scrolled in the inertia + */ + public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging); + + /** + * Called when the down motion event occurred. + */ + public void onDownMotionEvent(); + + /** + * Called when the dragging ended or canceled. + * + * @param scrollState state to indicate the scroll direction + */ + public void onUpOrCancelMotionEvent(ScrollState scrollState); +} diff --git a/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ObservableWebView.java b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ObservableWebView.java new file mode 100755 index 0000000000..3e023f74a5 --- /dev/null +++ b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ObservableWebView.java @@ -0,0 +1,250 @@ +/* + * 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 com.github.ksoichiro.android.observablescrollview; + +import android.content.Context; +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.webkit.WebView; + +/** + * WebView that its scroll position can be observed. + */ +public class ObservableWebView extends WebView implements Scrollable { + + // 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 ObservableWebView(Context context) { + super(context); + } + + public ObservableWebView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ObservableWebView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @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 + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + 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 { + mScrollState = ScrollState.STOP; + } + mPrevScrollY = t; + } + } + + @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_DOWN: + break; + 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 CREATOR + = new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ScrollState.java b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ScrollState.java new file mode 100755 index 0000000000..ae2277160f --- /dev/null +++ b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ScrollState.java @@ -0,0 +1,38 @@ +/* + * 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 com.github.ksoichiro.android.observablescrollview; + +/** + * Constants that indicates the scroll state of the Scrollable widgets. + */ +public enum ScrollState { + /** + * Widget is stopped. + * This state does not always mean that this widget have never been scrolled. + */ + STOP, + + /** + * Widget is scrolled up by swiping it down. + */ + UP, + + /** + * Widget is scrolled down by swiping it up. + */ + DOWN, +} diff --git a/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ScrollUtils.java b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ScrollUtils.java new file mode 100755 index 0000000000..daec7556c0 --- /dev/null +++ b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/ScrollUtils.java @@ -0,0 +1,143 @@ +/* + * 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 com.github.ksoichiro.android.observablescrollview; + +import android.os.Build; +import android.view.View; +import android.view.ViewTreeObserver; + +/** + * Utilities for creating scrolling effects. + */ +public final class ScrollUtils { + + private ScrollUtils() { + } + + /** + * Return a float value within the range. + * This is just a wrapper for Math.min() and Math.max(). + * This may be useful if you feel it confusing ("Which is min and which is max?"). + * + * @param value the target value + * @param minValue minimum value. If value is less than this, minValue will be returned + * @param maxValue maximum value. If value is greater than this, maxValue will be returned + * @return float value limited to the range + */ + public static float getFloat(final float value, final float minValue, final float maxValue) { + return Math.min(maxValue, Math.max(minValue, value)); + } + + /** + * Create a color integer value with specified alpha. + * This may be useful to change alpha value of background color. + * + * @param alpha alpha value from 0.0f to 1.0f. + * @param baseColor base color. alpha value will be ignored. + * @return a color with alpha made from base color + */ + public static int getColorWithAlpha(float alpha, int baseColor) { + int a = Math.min(255, Math.max(0, (int) (alpha * 255))) << 24; + int rgb = 0x00ffffff & baseColor; + return a + rgb; + } + + /** + * Add an OnGlobalLayoutListener for the view. + * This is just a convenience method for using {@code ViewTreeObserver.OnGlobalLayoutListener()}. + * This also handles removing listener when onGlobalLayout is called. + * + * @param view the target view to add global layout listener + * @param runnable runnable to be executed after the view is laid out + */ + public static void addOnGlobalLayoutListener(final View view, final Runnable runnable) { + ViewTreeObserver vto = view.getViewTreeObserver(); + vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + view.getViewTreeObserver().removeGlobalOnLayoutListener(this); + } else { + view.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + runnable.run(); + } + }); + } + + /** + * Mix two colors. + * {@code toColor} will be {@code toAlpha/1} percent, + * and {@code fromColor} will be {@code (1-toAlpha)/1} percent. + * + * @param fromColor first color to be mixed + * @param toColor second color to be mixed + * @param toAlpha alpha value of toColor, 0.0f to 1.0f. + * @return mixed color value in ARGB. Alpha is fixed value (255). + */ + public static int mixColors(int fromColor, int toColor, float toAlpha) { + float[] fromCmyk = ScrollUtils.cmykFromRgb(fromColor); + float[] toCmyk = ScrollUtils.cmykFromRgb(toColor); + float[] result = new float[4]; + for (int i = 0; i < 4; i++) { + result[i] = Math.min(1, fromCmyk[i] * (1 - toAlpha) + toCmyk[i] * toAlpha); + } + return 0xff000000 + (0x00ffffff & ScrollUtils.rgbFromCmyk(result)); + } + + /** + * Convert RGB color to CMYK color. + * + * @param rgbColor target color + * @return CMYK array + */ + public static float[] cmykFromRgb(int rgbColor) { + int red = (0xff0000 & rgbColor) >> 16; + int green = (0xff00 & rgbColor) >> 8; + int blue = (0xff & rgbColor); + float black = Math.min(1.0f - red / 255.0f, Math.min(1.0f - green / 255.0f, 1.0f - blue / 255.0f)); + float cyan = 1.0f; + float magenta = 1.0f; + float yellow = 1.0f; + if (black != 1.0f) { + // black 1.0 causes zero divide + cyan = (1.0f - (red / 255.0f) - black) / (1.0f - black); + magenta = (1.0f - (green / 255.0f) - black) / (1.0f - black); + yellow = (1.0f - (blue / 255.0f) - black) / (1.0f - black); + } + return new float[]{cyan, magenta, yellow, black}; + } + + /** + * Convert CYMK color to RGB color. + * This method doesn't check f cmyk is not null or have 4 elements in array. + * + * @param cmyk target CYMK color. Each value should be between 0.0f to 1.0f, + * and should be set in this order: cyan, magenta, yellow, black. + * @return ARGB color. Alpha is fixed value (255). + */ + public static int rgbFromCmyk(float[] cmyk) { + float cyan = cmyk[0]; + float magenta = cmyk[1]; + float yellow = cmyk[2]; + float black = cmyk[3]; + int red = (int) ((1.0f - Math.min(1.0f, cyan * (1.0f - black) + black)) * 255); + int green = (int) ((1.0f - Math.min(1.0f, magenta * (1.0f - black) + black)) * 255); + int blue = (int) ((1.0f - Math.min(1.0f, yellow * (1.0f - black) + black)) * 255); + return ((0xff & red) << 16) + ((0xff & green) << 8) + (0xff & blue); + } +} diff --git a/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/Scrollable.java b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/Scrollable.java new file mode 100755 index 0000000000..1043b4bcaf --- /dev/null +++ b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/Scrollable.java @@ -0,0 +1,56 @@ +/* + * 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 com.github.ksoichiro.android.observablescrollview; + +import android.view.ViewGroup; + +/** + * Provides common API for observable and scrollable widgets. + */ +public interface Scrollable { + /** + * Sets a callback listener. + * + * @param listener listener to set + */ + void setScrollViewCallbacks(ObservableScrollViewCallbacks listener); + + /** + * Scrolls vertically to the absolute Y. + * Implemented classes are expected to scroll to the exact Y pixels from the top, + * but it depends on the type of the widget. + * + * @param y vertical position to scroll to + */ + void scrollVerticallyTo(int y); + + /** + * Returns the current Y of the scrollable view. + * + * @return current Y pixel + */ + int getCurrentScrollY(); + + /** + * Sets a touch motion event delegation ViewGroup. + * This is used to pass motion events back to parent view. + * It's up to the implementation classes whether or not it works. + * + * @param viewGroup ViewGroup object to dispatch motion events + */ + void setTouchInterceptionViewGroup(ViewGroup viewGroup); +} diff --git a/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/TouchInterceptionFrameLayout.java b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/TouchInterceptionFrameLayout.java new file mode 100755 index 0000000000..6a5ed557d7 --- /dev/null +++ b/OsmAnd/src/com/github/ksoichiro/android/observablescrollview/TouchInterceptionFrameLayout.java @@ -0,0 +1,286 @@ +/* + * 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 com.github.ksoichiro.android.observablescrollview; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Build; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; + +/** + * A layout that delegates interception of touch motion events. + * This layout is provided to move the container of Scrollable views using scroll position. + * Please note that this class overrides or uses touch events API such as onTouchEvent, + * onInterceptTouchEvent and dispatchTouchEvent, + * so be careful when you handle touches with this layout. + */ +public class TouchInterceptionFrameLayout extends FrameLayout { + + /** + * Callbacks for TouchInterceptionFrameLayout. + */ + public interface TouchInterceptionListener { + /** + * Determines whether the layout should intercept this event. + * + * @param ev motion event + * @param moving true if this event is ACTION_MOVE type + * @param diffX difference between previous X and current X, if moving is true + * @param diffY difference between previous Y and current Y, if moving is true + * @return true if the layout should intercept + */ + boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffX, float diffY); + + /** + * Called if the down motion event is intercepted by this layout. + * + * @param ev motion event + */ + void onDownMotionEvent(MotionEvent ev); + + /** + * Called if the move motion event is intercepted by this layout. + * + * @param ev motion event + * @param diffX difference between previous X and current X + * @param diffY difference between previous Y and current Y + */ + void onMoveMotionEvent(MotionEvent ev, float diffX, float diffY); + + /** + * Called if the up (or cancel) motion event is intercepted by this layout. + * + * @param ev motion event + */ + void onUpOrCancelMotionEvent(MotionEvent ev); + } + + private boolean mIntercepting; + private boolean mDownMotionEventPended; + private boolean mBeganFromDownMotionEvent; + private boolean mChildrenEventsCanceled; + private PointF mInitialPoint; + private MotionEvent mPendingDownMotionEvent; + private TouchInterceptionListener mTouchInterceptionListener; + + public TouchInterceptionFrameLayout(Context context) { + super(context); + } + + public TouchInterceptionFrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public TouchInterceptionFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public TouchInterceptionFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public void setScrollInterceptionListener(TouchInterceptionListener listener) { + mTouchInterceptionListener = listener; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (mTouchInterceptionListener == null) { + return false; + } + + // In here, we must initialize touch state variables + // and ask if we should intercept this event. + // Whether we should intercept or not is kept for the later event handling. + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + mInitialPoint = new PointF(ev.getX(), ev.getY()); + mPendingDownMotionEvent = MotionEvent.obtainNoHistory(ev); + mDownMotionEventPended = true; + mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, false, 0, 0); + mBeganFromDownMotionEvent = mIntercepting; + mChildrenEventsCanceled = false; + return mIntercepting; + case MotionEvent.ACTION_MOVE: + // ACTION_MOVE will be passed suddenly, so initialize to avoid exception. + if (mInitialPoint == null) { + mInitialPoint = new PointF(ev.getX(), ev.getY()); + } + + // diffX and diffY are the origin of the motion, and should be difference + // from the position of the ACTION_DOWN event occurred. + float diffX = ev.getX() - mInitialPoint.x; + float diffY = ev.getY() - mInitialPoint.y; + mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, true, diffX, diffY); + return mIntercepting; + } + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mTouchInterceptionListener != null) { + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + if (mIntercepting) { + mTouchInterceptionListener.onDownMotionEvent(ev); + duplicateTouchEventForChildren(ev); + return true; + } + break; + case MotionEvent.ACTION_MOVE: + // ACTION_MOVE will be passed suddenly, so initialize to avoid exception. + if (mInitialPoint == null) { + mInitialPoint = new PointF(ev.getX(), ev.getY()); + } + + // diffX and diffY are the origin of the motion, and should be difference + // from the position of the ACTION_DOWN event occurred. + float diffX = ev.getX() - mInitialPoint.x; + float diffY = ev.getY() - mInitialPoint.y; + mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, true, diffX, diffY); + if (mIntercepting) { + // If this layout didn't receive ACTION_DOWN motion event, + // we should generate ACTION_DOWN event with current position. + if (!mBeganFromDownMotionEvent) { + mBeganFromDownMotionEvent = true; + + MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent); + event.setLocation(ev.getX(), ev.getY()); + mTouchInterceptionListener.onDownMotionEvent(event); + + mInitialPoint = new PointF(ev.getX(), ev.getY()); + diffX = diffY = 0; + } + + // Children's touches should be canceled + if (!mChildrenEventsCanceled) { + mChildrenEventsCanceled = true; + duplicateTouchEventForChildren(obtainMotionEvent(ev, MotionEvent.ACTION_CANCEL)); + } + + mTouchInterceptionListener.onMoveMotionEvent(ev, diffX, diffY); + + // If next mIntercepting become false, + // then we should generate fake ACTION_DOWN event. + // Therefore we set pending flag to true as if this is a down motion event. + mDownMotionEventPended = true; + + // Whether or not this event is consumed by the listener, + // assume it consumed because we declared to intercept the event. + return true; + } else { + if (mDownMotionEventPended) { + mDownMotionEventPended = false; + MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent); + event.setLocation(ev.getX(), ev.getY()); + duplicateTouchEventForChildren(ev, event); + } else { + duplicateTouchEventForChildren(ev); + } + + // If next mIntercepting become true, + // then we should generate fake ACTION_DOWN event. + // Therefore we set beganFromDownMotionEvent flag to false + // as if we haven't received a down motion event. + mBeganFromDownMotionEvent = false; + + // Reserve children's click cancellation here if they've already canceled + mChildrenEventsCanceled = false; + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mBeganFromDownMotionEvent = false; + if (mIntercepting) { + mTouchInterceptionListener.onUpOrCancelMotionEvent(ev); + } + + // Children's touches should be canceled regardless of + // whether or not this layout intercepted the consecutive motion events. + if (!mChildrenEventsCanceled) { + mChildrenEventsCanceled = true; + if (mDownMotionEventPended) { + mDownMotionEventPended = false; + MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent); + event.setLocation(ev.getX(), ev.getY()); + duplicateTouchEventForChildren(ev, event); + } else { + duplicateTouchEventForChildren(ev); + } + } + return true; + } + } + return super.onTouchEvent(ev); + } + + private MotionEvent obtainMotionEvent(MotionEvent base, int action) { + MotionEvent ev = MotionEvent.obtainNoHistory(base); + ev.setAction(action); + return ev; + } + + /** + * Duplicate touch events to child views. + * We want to dispatch a down motion event and the move events to + * child views, but calling dispatchTouchEvent() causes StackOverflowError. + * Therefore we do it manually. + * + * @param ev motion event to be passed to children + * @param pendingEvents pending events like ACTION_DOWN. This will be passed to the children before ev + */ + private void duplicateTouchEventForChildren(MotionEvent ev, MotionEvent... pendingEvents) { + if (ev == null) { + return; + } + for (int i = getChildCount() - 1; 0 <= i; i--) { + View childView = getChildAt(i); + if (childView != null) { + Rect childRect = new Rect(); + childView.getHitRect(childRect); + MotionEvent event = MotionEvent.obtainNoHistory(ev); + if (!childRect.contains((int) event.getX(), (int) event.getY())) { + continue; + } + float offsetX = -childView.getLeft(); + float offsetY = -childView.getTop(); + boolean consumed = false; + if (pendingEvents != null) { + for (MotionEvent pe : pendingEvents) { + if (pe != null) { + MotionEvent peAdjusted = MotionEvent.obtainNoHistory(pe); + peAdjusted.offsetLocation(offsetX, offsetY); + consumed |= childView.dispatchTouchEvent(peAdjusted); + } + } + } + event.offsetLocation(offsetX, offsetY); + consumed |= childView.dispatchTouchEvent(event); + if (consumed) { + break; + } + } + } + } +}