package fr.neamar.kiss.ui;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
/**
* Utility class for automatically hiding the keyboard when scrolling down a {@see ListView},
* keeping the position of the finger on the list stable
*/
public class KeyboardScrollHider implements View.OnTouchListener {
private final static int THRESHOLD = 24;
private KeyboardHandler handler;
private BlockableListView list;
private View listParent;
private BottomPullEffectView pullEffect;
private int listHeightInitial = 0;
private float offsetYStart = 0;
private float offsetYCurrent = 0;
private int offsetYDiff = 0;
private MotionEvent lastMotionEvent;
private int initialWindowPadding = 0;
private boolean resizeDone = false;
private boolean scrollBarEnabled = true;
public interface KeyboardHandler {
void showKeyboard();
void hideKeyboard();
}
public KeyboardScrollHider(KeyboardHandler handler, BlockableListView list, BottomPullEffectView pullEffect) {
this.handler = handler;
this.list = list;
this.listParent = (View) list.getParent();
this.pullEffect = pullEffect;
}
/**
* Start monitoring and intercepting touch events of the target list view and providing our
* transformations
*/
public void start() {
this.list.setOnTouchListener(this);
}
/**
*
*/
@SuppressWarnings("unused")
public void stop() {
this.list.setOnTouchListener(null);
this.handleResizeDone();
}
private int getWindowPadding() {
ViewGroup rootView = (ViewGroup) this.list.getRootView();
return rootView.getChildAt(0).getPaddingBottom();
}
private int getWindowWidth() {
ViewGroup rootView = (ViewGroup) this.list.getRootView();
return rootView.getChildAt(0).getWidth();
}
private void setListLayoutHeight(int height) {
final ViewGroup.LayoutParams params = this.list.getLayoutParams();
params.height = height;
this.list.setLayoutParams(params);
this.list.forceLayout();
}
private void handleResizeDone() {
if (this.resizeDone) {
return;
}
// Give the list view the control over it's input back
this.list.unblockTouchEvents();
// Quickly fade out edge pull effect
this.pullEffect.releasePull();
// Make sure list uses the height of it's parent
this.list.setVerticalScrollBarEnabled(this.scrollBarEnabled);
this.setListLayoutHeight(ViewGroup.LayoutParams.MATCH_PARENT);
this.resizeDone = true;
}
private void updateListViewHeight() {
// Don't do anything if the window hasn't resized yet or if we're already done
if (this.getWindowPadding() >= this.initialWindowPadding || this.resizeDone) {
return;
}
// Resize in progress - prevent the view from responding to touch events directly
this.list.blockTouchEvents();
this.list.setVerticalScrollBarEnabled(false);
int heightContainer = this.listParent.getHeight();
int offsetYDiff = (int) (this.offsetYCurrent - this.offsetYStart);
if (offsetYDiff < (this.offsetYDiff - THRESHOLD)) {
double pullFeedback = Math.sqrt((double) (this.offsetYDiff - offsetYDiff) / THRESHOLD);
offsetYDiff = this.offsetYDiff - (int) (THRESHOLD * pullFeedback);
}
// Determine new size of list view widget within its container
int listLayoutHeight = ViewGroup.LayoutParams.MATCH_PARENT;
if ((this.listHeightInitial + offsetYDiff) < heightContainer) {
listLayoutHeight = this.listHeightInitial + offsetYDiff;
}
this.setListLayoutHeight(listLayoutHeight);
if (offsetYDiff > this.offsetYDiff) {
this.offsetYDiff = offsetYDiff;
}
if (this.getWindowPadding() < this.initialWindowPadding
&& listLayoutHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
// Window size has increased and view has reached it's new maximum size - we're done
this.handleResizeDone();
return;
}
// Display edge pulling effect while list view is detached from the bottom of its
// container
float distance = ((float) (heightContainer - listLayoutHeight)) / heightContainer;
float displacement = 1 - this.lastMotionEvent.getX() / getWindowWidth();
this.pullEffect.setPull(distance, displacement, false);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
this.scrollBarEnabled = this.list.isVerticalScrollBarEnabled();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
this.offsetYStart = event.getY();
this.offsetYCurrent = event.getY();
this.offsetYDiff = 0;
this.lastMotionEvent = event;
this.resizeDone = false;
this.initialWindowPadding = this.getWindowPadding();
// Lock list view height to its current value
this.listHeightInitial = this.list.getHeight();
this.setListLayoutHeight(this.listHeightInitial);
break;
case MotionEvent.ACTION_MOVE:
this.offsetYCurrent = event.getY();
this.lastMotionEvent = event;
this.updateListViewHeight();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
this.lastMotionEvent = null;
if (!this.resizeDone) {
ValueAnimator animator = ValueAnimator.ofInt(
this.list.getHeight(),
this.listParent.getHeight()
);
animator.setDuration(250);
animator.setInterpolator(new AccelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
int height = (int) (animator.getAnimatedValue());
KeyboardScrollHider.this.setListLayoutHeight(height);
}
});
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
// Give the list view the control over it's input back
KeyboardScrollHider.this.list.unblockTouchEvents();
// Quickly fade out edge pull effect
KeyboardScrollHider.this.pullEffect.releasePull();
}
@Override
public void onAnimationEnd(Animator animation) {
KeyboardScrollHider.this.handleResizeDone();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animator.start();
} else {
this.handleResizeDone();
}
break;
}
// Hide the keyboard if the user has scrolled down by about half a result item
if ((this.offsetYCurrent - this.offsetYStart) > THRESHOLD) {
this.handler.hideKeyboard();
}
return false;
}
}