Created
May 27, 2019 13:05
-
-
Save caneryilmaz/e430efc7cfca300adcaa9141048ef5cd to your computer and use it in GitHub Desktop.
for auto complete when user swipe
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.content.Context; | |
import android.os.Build; | |
import android.os.Handler; | |
import android.support.v4.view.ViewCompat; | |
import android.support.v7.widget.LinearLayoutManager; | |
import android.support.v7.widget.RecyclerView; | |
import android.util.AttributeSet; | |
import android.view.MotionEvent; | |
import android.view.View; | |
import android.view.ViewTreeObserver; | |
import android.widget.LinearLayout; | |
import static android.widget.AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL; | |
import static android.widget.NumberPicker.OnScrollListener.SCROLL_STATE_FLING; | |
public class SnappingRecyclerView extends RecyclerView { | |
private boolean _userScrolling = false; | |
private boolean _scrolling = false; | |
private int _scrollState = SCROLL_STATE_IDLE; | |
private long _lastScrollTime = 0; | |
private Handler mHandler = new Handler(); | |
private boolean _scaleViews = false; | |
private Orientation _orientation = Orientation.HORIZONTAL; | |
private ChildViewMetrics _childViewMetrics; | |
private OnViewSelectedListener _listener; | |
private int _selectedPosition; | |
private final static int MINIMUM_SCROLL_EVENT_OFFSET_MS = 20; | |
public SnappingRecyclerView(Context context) { | |
this(context, null); | |
} | |
public SnappingRecyclerView(Context context, AttributeSet attrs) { | |
this(context, attrs, 0); | |
} | |
public SnappingRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { | |
super(context, attrs, defStyleAttr); | |
init(); | |
} | |
private void init() { | |
setHasFixedSize(true); | |
setOrientation(_orientation); | |
enableSnapping(); | |
} | |
private boolean scrolling; | |
@Override | |
public void onChildAttachedToWindow(View child) { | |
super.onChildAttachedToWindow(child); | |
if (!scrolling && _scrollState == SCROLL_STATE_IDLE) { | |
scrolling = true; | |
scrollToView(getCenterView()); | |
updateViews(); | |
} | |
} | |
private void enableSnapping() { | |
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { | |
@Override | |
public void onGlobalLayout() { | |
getViewTreeObserver().removeGlobalOnLayoutListener(this); | |
} | |
}); | |
addOnScrollListener(new OnScrollListener() { | |
@Override | |
public void onScrolled(RecyclerView recyclerView, int dx, int dy) { | |
updateViews(); | |
super.onScrolled(recyclerView, dx, dy); | |
} | |
@Override | |
public void onScrollStateChanged(RecyclerView recyclerView, int newState) { | |
super.onScrollStateChanged(recyclerView, newState); | |
/** if scroll is caused by a touch (scroll touch, not any touch) **/ | |
if (newState == SCROLL_STATE_TOUCH_SCROLL) { | |
/** if scroll was initiated already, it would probably be a tap **/ | |
/** if scroll was not initiated before, this is probably a user scrolling **/ | |
if (!_scrolling) { | |
_userScrolling = true; | |
} | |
} else if (newState == SCROLL_STATE_IDLE) { | |
/** if user is the one scrolling, snap to the view closest to center **/ | |
if (_userScrolling) { | |
scrollToView(getCenterView()); | |
} | |
_userScrolling = false; | |
_scrolling = false; | |
/** if idle, always check location and correct it if necessary, this is just an extra check **/ | |
if (getCenterView() != null && getPercentageFromCenter(getCenterView()) > 0) { | |
scrollToView(getCenterView()); | |
} | |
/** if idle, notify listeners of new selected view **/ | |
notifyListener(); | |
} else if (newState == SCROLL_STATE_FLING) { | |
_scrolling = true; | |
} | |
_scrollState = newState; | |
} | |
}); | |
} | |
private void notifyListener() { | |
View view = getCenterView(); | |
int position = getChildAdapterPosition(view); | |
/** if there is a listener and the index is not the same as the currently selected position, notify listener **/ | |
if (_listener != null && position != _selectedPosition) { | |
_listener.onSelected(view, position); | |
} | |
_selectedPosition = position; | |
} | |
/** | |
* Set the orientation for this SnappingRecyclerView | |
* | |
* @param orientation LinearLayoutManager.HORIZONTAL or LinearLayoutManager.VERTICAL | |
*/ | |
public void setOrientation(Orientation orientation) { | |
this._orientation = orientation; | |
_childViewMetrics = new ChildViewMetrics(_orientation); | |
setLayoutManager(new LinearLayoutManager(getContext(), _orientation.intValue(), false)); | |
} | |
/** | |
* Set the OnViewSelectedListener | |
* | |
* @param listener the OnViewSelectedListener | |
*/ | |
public void setOnViewSelectedListener(OnViewSelectedListener listener) { | |
this._listener = listener; | |
} | |
/** | |
* Enable downscaling of views which are not focused, based on how far away they are from the center | |
* | |
* @param enabled enable or disable the scaling behaviour | |
*/ | |
public void enableViewScaling(boolean enabled) { | |
this._scaleViews = enabled; | |
} | |
private void updateViews() { | |
for (int i = 0; i < getChildCount(); i++) { | |
View child = getChildAt(i); | |
setMarginsForChild(child); | |
if (_scaleViews) { | |
float percentage = getPercentageFromCenter(child); | |
float scale = 1f - (0.7f * percentage); | |
child.setScaleX(scale); | |
child.setScaleY(scale); | |
} | |
} | |
} | |
/** | |
* Adds the margins to a childView so a view will still center even if it's only a single child | |
* | |
* @param child childView to set margins for | |
*/ | |
private void setMarginsForChild(View child) { | |
int lastItemIndex = getLayoutManager().getItemCount() - 1; | |
int childIndex = getChildAdapterPosition(child); | |
int startMargin = 0; | |
int endMargin = 0; | |
int topMargin = 0; | |
int bottomMargin = 0; | |
if (_orientation == Orientation.VERTICAL) { | |
topMargin = childIndex == 0 ? getCenterLocation() : 0; | |
bottomMargin = childIndex == lastItemIndex ? getCenterLocation() : 0; | |
} else { | |
startMargin = childIndex == 0 ? getCenterLocation() : 0; | |
endMargin = childIndex == lastItemIndex ? getCenterLocation() : 0; | |
} | |
/** if sdk minimum level is 17, set RTL margins **/ | |
if (_orientation == Orientation.HORIZONTAL && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { | |
((MarginLayoutParams) child.getLayoutParams()).setMarginStart(startMargin); | |
((MarginLayoutParams) child.getLayoutParams()).setMarginEnd(endMargin); | |
} | |
/** If layout direction is RTL, swap the margins **/ | |
if (ViewCompat.getLayoutDirection(child) == ViewCompat.LAYOUT_DIRECTION_RTL) { | |
((MarginLayoutParams) child.getLayoutParams()).setMargins(endMargin, topMargin, startMargin, bottomMargin); | |
} else { | |
((MarginLayoutParams) child.getLayoutParams()).setMargins(startMargin, topMargin, endMargin, bottomMargin); | |
} | |
/** if sdk minimum level is 18, check if view isn't undergoing a layout pass (this improves the feel of the view by a lot) **/ | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { | |
if (!child.isInLayout()) | |
child.requestLayout(); | |
} | |
} | |
@Override | |
public boolean dispatchTouchEvent(MotionEvent event) { | |
long currentTime = System.currentTimeMillis(); | |
/** if touch events are being spammed, this is due to user scrolling right after a tap, | |
* so set userScrolling to true **/ | |
if (_scrolling && _scrollState == SCROLL_STATE_TOUCH_SCROLL) { | |
if ((currentTime - _lastScrollTime) < MINIMUM_SCROLL_EVENT_OFFSET_MS) { | |
_userScrolling = true; | |
} | |
} | |
_lastScrollTime = currentTime; | |
int location = _orientation == Orientation.VERTICAL ? (int) event.getY() : (int) event.getX(); | |
View targetView = getChildClosestToLocation(location); | |
if (!_userScrolling) { | |
if (event.getAction() == MotionEvent.ACTION_UP) { | |
if (targetView != getCenterView()) { | |
scrollToView(targetView); | |
return true; | |
} | |
} | |
} | |
return super.dispatchTouchEvent(event); | |
} | |
@Override | |
public boolean onInterceptTouchEvent(MotionEvent event) { | |
int location = _orientation == Orientation.VERTICAL ? (int) event.getY() : (int) event.getX(); | |
View targetView = getChildClosestToLocation(location); | |
if (targetView != getCenterView()) { | |
return true; | |
} | |
return super.onInterceptTouchEvent(event); | |
} | |
@Override | |
public void scrollToPosition(int position) { | |
_childViewMetrics.size(getChildAt(0)); | |
smoothScrollBy(_childViewMetrics.size(getChildAt(0)) * position); | |
} | |
private View getChildClosestToLocation(int location) { | |
if (getChildCount() <= 0) | |
return null; | |
int closestPos = 9999; | |
View closestChild = null; | |
for (int i = 0; i < getChildCount(); i++) { | |
View child = getChildAt(i); | |
int childCenterLocation = (int) _childViewMetrics.center(child); | |
int distance = childCenterLocation - location; | |
/** if child center is closer than previous closest, set it as closest child **/ | |
if (Math.abs(distance) < Math.abs(closestPos)) { | |
closestPos = distance; | |
closestChild = child; | |
} | |
} | |
return closestChild; | |
} | |
/** | |
* Check if the view is correctly centered (allow for 10px offset) | |
* | |
* @param child the child view | |
* @return true if correctly centered | |
*/ | |
private boolean isChildCorrectlyCentered(View child) { | |
int childPosition = (int) _childViewMetrics.center(child); | |
return childPosition > (getCenterLocation() - 10) && childPosition < (getCenterLocation() + 10); | |
} | |
private View getCenterView() { | |
return getChildClosestToLocation(getCenterLocation()); | |
} | |
private void scrollToView(View child) { | |
if (child == null) | |
return; | |
stopScroll(); | |
int scrollDistance = getScrollDistance(child); | |
if (scrollDistance != 0) | |
smoothScrollBy(scrollDistance); | |
} | |
private int getScrollDistance(View child) { | |
int childCenterLocation = (int) _childViewMetrics.center(child); | |
return childCenterLocation - getCenterLocation(); | |
} | |
private float getPercentageFromCenter(View child) { | |
float center = getCenterLocation(); | |
float childCenter = _childViewMetrics.center(child); | |
float offSet = Math.max(center, childCenter) - Math.min(center, childCenter); | |
float maxOffset = (center + _childViewMetrics.size(child)); | |
return (offSet / maxOffset); | |
} | |
private int getCenterLocation() { | |
if (_orientation == Orientation.VERTICAL) | |
return getMeasuredHeight() / 2; | |
return getMeasuredWidth() / 2; | |
} | |
public void smoothScrollBy(int distance) { | |
if (_orientation == Orientation.VERTICAL) { | |
super.smoothScrollBy(0, distance); | |
return; | |
} | |
super.smoothScrollBy(distance, 0); | |
} | |
public void scrollBy(int distance) { | |
if (_orientation == Orientation.VERTICAL) { | |
super.scrollBy(0, distance); | |
return; | |
} | |
super.scrollBy(distance, 0); | |
} | |
private void scrollTo(int position) { | |
int currentScroll = getScrollOffset(); | |
scrollBy(position - currentScroll); | |
} | |
public int getScrollOffset() { | |
if (_orientation == Orientation.VERTICAL) | |
return computeVerticalScrollOffset(); | |
return computeHorizontalScrollOffset(); | |
} | |
/** | |
* Returns the currently centered item aka the selected item | |
*/ | |
public int getSelectedPosition() { | |
return _selectedPosition; | |
} | |
@Override | |
protected void onDetachedFromWindow() { | |
super.onDetachedFromWindow(); | |
mHandler.removeCallbacksAndMessages(null); | |
} | |
private static class ChildViewMetrics { | |
private Orientation _orientation; | |
public ChildViewMetrics(Orientation orientation) { | |
this._orientation = orientation; | |
} | |
public int size(View view) { | |
if (_orientation == Orientation.VERTICAL) | |
return view.getHeight(); | |
return view.getWidth(); | |
} | |
public float location(View view) { | |
if (_orientation == Orientation.VERTICAL) | |
return view.getY(); | |
return view.getX(); | |
} | |
public float center(View view) { | |
return location(view) + (size(view) / 2); | |
} | |
} | |
public enum Orientation { | |
HORIZONTAL(LinearLayout.HORIZONTAL), | |
VERTICAL(LinearLayout.VERTICAL); | |
int value; | |
Orientation(int value) { | |
this.value = value; | |
} | |
public int intValue() { | |
return value; | |
} | |
} | |
public interface OnViewSelectedListener { | |
void onSelected(View view, int position); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment