385 lines
15 KiB
Java
385 lines
15 KiB
Java
|
/*
|
||
|
* 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<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];
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
}
|