/*
* Copyright (c) 2016 Ha Duy Trung
*
* 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 io.github.hidroh.materialistic;
import android.app.Activity;
import android.content.SharedPreferences;
import android.support.annotation.IntDef;
import android.support.design.widget.AppBarLayout;
import android.support.v4.widget.NestedScrollView;
import android.support.v7.preference.PreferenceManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Helper that intercepts key events and interprets them into navigation actions
*/
public class KeyDelegate {
@Retention(RetentionPolicy.SOURCE)
@IntDef({
DIRECTION_NONE,
DIRECTION_UP,
DIRECTION_DOWN
})
@interface Direction {}
private static final int DIRECTION_NONE = 0;
private static final int DIRECTION_UP = 1;
private static final int DIRECTION_DOWN = 2;
private final SharedPreferences.OnSharedPreferenceChangeListener mPreferenceListener;
private String mPreferenceKey;
private boolean mEnabled;
private Scrollable mScrollable;
private AppBarLayout mAppBarLayout;
private boolean mAppBarEnabled = true;
private BackInterceptor mBackInterceptor;
public KeyDelegate() {
mPreferenceListener = (sharedPreferences, key) -> {
if (TextUtils.equals(key, mPreferenceKey)) {
mEnabled = sharedPreferences.getBoolean(key, false);
}
};
}
/**
* Attaches this delegate to given activity lifecycle
* Should call {@link #detach(Activity)} accordingly
* @param activity active activity to receive key events
* @see {@link #detach(Activity)}
*/
public void attach(Activity activity) {
mPreferenceKey = activity.getString(R.string.pref_volume);
mEnabled = PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(mPreferenceKey, false);
PreferenceManager.getDefaultSharedPreferences(activity)
.registerOnSharedPreferenceChangeListener(mPreferenceListener);
}
/**
* Detaches this delegate from given activity lifecycle
* Should already call {@link #attach(Activity)}
* @param activity active activity that has been receiving key events
* @see {@link #attach(Activity)}
*/
public void detach(Activity activity) {
PreferenceManager.getDefaultSharedPreferences(activity)
.unregisterOnSharedPreferenceChangeListener(mPreferenceListener);
mScrollable = null;
mAppBarLayout = null;
}
/**
* Binds navigation objects that would be scrolled by key events
* @param scrollable vertically scrollable instance
* @param appBarLayout optional AppBarLayout that expands/collapses while scrolling
*/
public void setScrollable(Scrollable scrollable, AppBarLayout appBarLayout) {
mScrollable = scrollable;
mAppBarLayout = appBarLayout;
}
/**
* Toggle {@link AppBarLayout} expand/collapse
* @param enabled true to enable, false otherwise
*/
void setAppBarEnabled(boolean enabled) {
mAppBarEnabled = enabled;
}
/**
* Intercepts back pressed
* @param backInterceptor listener to back pressed event
*/
void setBackInterceptor(BackInterceptor backInterceptor) {
mBackInterceptor = backInterceptor;
}
/**
* Calls from {@link Activity#onKeyDown(int, KeyEvent)} to delegate
* @param keyCode event key code
* @param event key event
* @return true if is intercepted as navigation, false otherwise
*/
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
return mBackInterceptor != null && mBackInterceptor.onBackPressed();
}
if (!mEnabled) {
return false;
}
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP ||
keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
event.startTracking();
return true;
}
return false;
}
/**
* Calls from {@link Activity#onKeyUp(int, KeyEvent)} to delegate
* @param keyCode event key code
* @param event key event
* @return true if is intercepted as navigation, false otherwise
*/
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (!mEnabled) {
return false;
}
boolean notLongPress = (event.getFlags() & KeyEvent.FLAG_CANCELED_LONG_PRESS) == 0;
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && notLongPress) {
shortPress(DIRECTION_UP);
return true;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && notLongPress) {
shortPress(DIRECTION_DOWN);
return true;
}
return false;
}
/**
* Calls from {@link Activity#onKeyLongPress(int, KeyEvent)} to delegate
* @param keyCode event key code
* @param event key event
* @return true if is intercepted as navigation, false otherwise
*/
@SuppressWarnings("UnusedParameters")
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
if (!mEnabled) {
return false;
}
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
longPress(DIRECTION_UP);
return true;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
longPress(DIRECTION_DOWN);
return true;
}
return false;
}
private void shortPress(@Direction int direction) {
if (mScrollable == null) {
return;
}
switch (direction) {
case DIRECTION_UP:
if (!mScrollable.scrollToPrevious() && mAppBarEnabled && mAppBarLayout != null) {
mAppBarLayout.setExpanded(true, true);
}
break;
case DIRECTION_DOWN:
if (mAppBarEnabled && mAppBarLayout != null &&
mAppBarLayout.getHeight() == mAppBarLayout.getBottom()) {
mAppBarLayout.setExpanded(false, true);
} else {
mScrollable.scrollToNext();
}
break;
case DIRECTION_NONE:
default:
break;
}
}
private void longPress(@Direction int direction) {
switch (direction) {
case DIRECTION_DOWN:
case DIRECTION_NONE:
default:
break;
case DIRECTION_UP:
if (mAppBarEnabled && mAppBarLayout != null) {
mAppBarLayout.setExpanded(true, true);
}
if (mScrollable != null) {
mScrollable.scrollToTop();
}
break;
}
}
/**
* Helper class to navigate vertical RecyclerView
*/
static class RecyclerViewHelper implements Scrollable {
@Retention(RetentionPolicy.SOURCE)
@IntDef({
SCROLL_ITEM,
SCROLL_PAGE
})
@interface ScrollMode {}
static final int SCROLL_ITEM = 0;
static final int SCROLL_PAGE = 1;
private final RecyclerView mRecyclerView;
private final @ScrollMode int mScrollMode;
private boolean mSmoothScroll = true;
RecyclerViewHelper(RecyclerView recyclerView, @ScrollMode int scrollMode) {
mRecyclerView = recyclerView;
if (!(mRecyclerView.getLayoutManager() instanceof LinearLayoutManager)) {
throw new IllegalArgumentException("Only LinearLayoutManager supported");
}
mScrollMode = scrollMode;
}
@Override
public void scrollToTop() {
mRecyclerView.scrollToPosition(0);
}
@Override
public boolean scrollToNext() {
int pos = mScrollMode == SCROLL_ITEM ?
getLinearLayoutManager().findFirstVisibleItemPosition() :
getLinearLayoutManager().findLastCompletelyVisibleItemPosition(),
next = pos != RecyclerView.NO_POSITION ?
pos + 1 : RecyclerView.NO_POSITION;
if (next != RecyclerView.NO_POSITION &&
next < mRecyclerView.getAdapter().getItemCount()) {
mRecyclerView.smoothScrollToPosition(next);
return true;
} else {
return false;
}
}
@Override
public boolean scrollToPrevious() {
switch (mScrollMode) {
case SCROLL_ITEM:
default:
int pos = getLinearLayoutManager().findFirstVisibleItemPosition(),
previous = pos != RecyclerView.NO_POSITION ?
pos - 1 : RecyclerView.NO_POSITION;
if (previous >= 0) {
mRecyclerView.smoothScrollToPosition(previous);
return true;
} else {
return false;
}
case SCROLL_PAGE:
if (getLinearLayoutManager().findFirstVisibleItemPosition() <= 0) {
return false;
} else {
mRecyclerView.smoothScrollBy(0, -mRecyclerView.getHeight());
return true;
}
}
}
void smoothScrollEnabled(boolean enabled) {
mSmoothScroll = enabled;
}
int getCurrentPosition() {
// TODO handle last page item
return getLinearLayoutManager().findFirstVisibleItemPosition();
}
int[] scrollToPosition(int position) {
if (position >= 0 && position < mRecyclerView.getAdapter().getItemCount()) {
if (!mSmoothScroll) {
getLinearLayoutManager().scrollToPositionWithOffset(position, 0);
return null;
}
int first = getLinearLayoutManager().findFirstVisibleItemPosition();
int[] lock = null;
if (Math.abs(position - first) > 1) { // lock nothing if scroll to adjacent
if (position < first) { // scroll up, lock lower part
lock = new int[]{position, first - 1};
} else if (position > first) { // scroll down, lock upper part
lock = new int[]{first, position - 1};
}
}
mRecyclerView.smoothScrollToPosition(position);
return lock;
} else {
return null;
}
}
private LinearLayoutManager getLinearLayoutManager() {
return (LinearLayoutManager) mRecyclerView.getLayoutManager();
}
}
/**
* Helper class to navigate vertical NestedScrollView
*/
static class NestedScrollViewHelper implements Scrollable {
private final NestedScrollView mScrollView;
NestedScrollViewHelper(NestedScrollView nestedScrollView) {
mScrollView = nestedScrollView;
}
@Override
public void scrollToTop() {
mScrollView.smoothScrollTo(0, 0);
}
@Override
public boolean scrollToNext() {
return mScrollView.pageScroll(View.FOCUS_DOWN);
}
@Override
public boolean scrollToPrevious() {
return mScrollView.pageScroll(View.FOCUS_UP);
}
}
/**
* Callback interface for back pressed events
*/
interface BackInterceptor {
/**
* Fired upon back pressed
* @return true if handled, false otherwise
*/
boolean onBackPressed();
}
}