io.github.hidroh.materialistic.KeyDelegate.java Source code

Java tutorial

Introduction

Here is the source code for io.github.hidroh.materialistic.KeyDelegate.java

Source

/*
 * 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 LinearLayoutManager mLayoutManager;
        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");
            }
            mLayoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
            mScrollMode = scrollMode;
        }

        @Override
        public void scrollToTop() {
            mRecyclerView.scrollToPosition(0);
        }

        @Override
        public boolean scrollToNext() {
            int pos = mScrollMode == SCROLL_ITEM ? mLayoutManager.findFirstVisibleItemPosition()
                    : mLayoutManager.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 = mLayoutManager.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 (mLayoutManager.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 mLayoutManager.findFirstVisibleItemPosition();
        }

        int[] scrollToPosition(int position) {
            if (position >= 0 && position < mRecyclerView.getAdapter().getItemCount()) {
                if (!mSmoothScroll) {
                    mLayoutManager.scrollToPositionWithOffset(position, 0);
                    return null;
                }
                int first = mLayoutManager.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;
            }
        }
    }

    /**
     * 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();
    }
}