/* * 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.widget; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.os.Vibrator; import android.support.annotation.RequiresApi; import android.support.design.widget.FloatingActionButton; import android.support.v4.view.GestureDetectorCompat; import android.support.v7.app.AlertDialog; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.widget.Toast; import java.util.Locale; import io.github.hidroh.materialistic.AppUtils; import io.github.hidroh.materialistic.Navigable; import io.github.hidroh.materialistic.Preferences; import io.github.hidroh.materialistic.R; import io.github.hidroh.materialistic.annotation.Synthetic; public class NavFloatingActionButton extends FloatingActionButton implements ViewTreeObserver.OnGlobalLayoutListener { private static final String PREFERENCES_FAB = "_fab"; private static final String PREFERENCES_FAB_X = "%1$s_%2$d_%3$d_x"; private static final String PREFERENCES_FAB_Y = "%1$s_%2$d_%3$d_y"; private static final long VIBRATE_DURATION_MS = 15; private static final int DOUBLE_TAP = -1; private static final int[] KONAMI_CODE = { Navigable.DIRECTION_UP, Navigable.DIRECTION_UP, Navigable.DIRECTION_DOWN, Navigable.DIRECTION_DOWN, Navigable.DIRECTION_LEFT, Navigable.DIRECTION_RIGHT, Navigable.DIRECTION_LEFT, Navigable.DIRECTION_RIGHT, DOUBLE_TAP }; @Synthetic final Vibrator mVibrator; private final Preferences.Observable mPreferenceObservable = new Preferences.Observable(); @Synthetic Navigable mNavigable; @Synthetic boolean mMoved; private int mNextKonamiCode = 0; private SharedPreferences mPreferences; private String mPreferenceX, mPreferenceY; @Synthetic boolean mVibrationEnabled; public static void resetPosition(Context context) { getSharedPreferences(context).edit().clear().apply(); } public NavFloatingActionButton(Context context) { this(context, null); } public NavFloatingActionButton(Context context, AttributeSet attrs) { this(context, attrs, 0); } public NavFloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); bindNavigationPad(); mVibrationEnabled = Preferences.navigationEnabled(context); if (!isInEditMode()) { mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); } else { mVibrator = null; } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); getViewTreeObserver().addOnGlobalLayoutListener(this); mPreferenceObservable.subscribe(getContext(), (key, contextChanged) -> mVibrationEnabled = Preferences.navigationVibrationEnabled(getContext()), R.string.pref_navigation_vibrate); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); stopObservingViewTree(); mPreferenceObservable.unsubscribe(getContext()); } @Override public void setOnTouchListener(OnTouchListener l) { throw new UnsupportedOperationException(); } public void setNavigable(Navigable navigable) { mNavigable = navigable; } @Synthetic void bindNavigationPad() { GestureDetectorCompat detectorCompat = new GestureDetectorCompat(getContext(), new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { return mNavigable != null; } @Override public boolean onSingleTapConfirmed(MotionEvent e) { Toast.makeText(getContext(), R.string.hint_nav_short, Toast.LENGTH_LONG).show(); return true; } @Override public boolean onDoubleTap(MotionEvent e) { trackKonami(DOUBLE_TAP); return super.onDoubleTap(e); } @Override public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float velocityX, float velocityY) { int direction; if (Math.abs(velocityX) > Math.abs(velocityY)) { direction = velocityX < 0 ? Navigable.DIRECTION_LEFT : Navigable.DIRECTION_RIGHT; } else { direction = velocityY < 0 ? Navigable.DIRECTION_UP : Navigable.DIRECTION_DOWN; } mNavigable.onNavigate(direction); if (mVibrationEnabled) { mVibrator.vibrate(VIBRATE_DURATION_MS); } trackKonami(direction); return false; } @Override public void onLongPress(MotionEvent e) { if (mNavigable == null) { return; } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { Toast.makeText(getContext(), R.string.not_supported, Toast.LENGTH_SHORT).show(); } else { startDrag(e.getX(), e.getY()); } } }); //noinspection Convert2Lambda super.setOnTouchListener(new OnTouchListener() { @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View view, MotionEvent motionEvent) { return detectorCompat.onTouchEvent(motionEvent); } }); } @Synthetic void startDrag(float startX, float startY) { if (mVibrationEnabled) { mVibrator.vibrate(VIBRATE_DURATION_MS * 2); } Toast.makeText(getContext(), R.string.hint_drag, Toast.LENGTH_SHORT).show(); //noinspection Convert2Lambda super.setOnTouchListener(new OnTouchListener() { @SuppressLint("ClickableViewAccessibility") @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override public boolean onTouch(View view, MotionEvent motionEvent) { switch (motionEvent.getAction()) { case MotionEvent.ACTION_MOVE: mMoved = true; view.setX(motionEvent.getRawX() - startX); // TODO compensate shift view.setY(motionEvent.getRawY() - startY); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: bindNavigationPad(); if (mMoved) { persistPosition(); } break; default: return false; } return true; } }); } @Synthetic boolean trackKonami(int direction) { if (KONAMI_CODE[mNextKonamiCode] != direction) { mNextKonamiCode = direction == KONAMI_CODE[0] ? 1 : 0; return false; } else if (mNextKonamiCode == KONAMI_CODE.length - 1) { mNextKonamiCode = 0; if (mVibrationEnabled) { mVibrator.vibrate(new long[]{0, VIBRATE_DURATION_MS * 2, 100, VIBRATE_DURATION_MS * 2}, -1); } new AlertDialog.Builder(getContext()) .setView(R.layout.dialog_konami) .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> AppUtils.openPlayStore(getContext())) .create() .show(); return true; } else { mNextKonamiCode++; return true; } } @Override public void onGlobalLayout() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { restorePosition(); } stopObservingViewTree(); } private void stopObservingViewTree() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { getViewTreeObserver().removeOnGlobalLayoutListener(this); } else { //noinspection deprecation getViewTreeObserver().removeGlobalOnLayoutListener(this); } } @SuppressLint("CommitPrefEdits") @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB) @Synthetic void persistPosition() { getPreferences() .edit() .putFloat(mPreferenceX, getX()) .putFloat(mPreferenceY, getY()) .apply(); } @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB) private void restorePosition() { setX(getPreferences().getFloat(mPreferenceX, getX())); setY(getPreferences().getFloat(mPreferenceY, getY())); } private DisplayMetrics getDisplayMetrics() { DisplayMetrics metrics = new DisplayMetrics(); ((WindowManager) getContext().getSystemService(Activity.WINDOW_SERVICE)) .getDefaultDisplay().getMetrics(metrics); return metrics; } private SharedPreferences getPreferences() { if (mPreferences == null) { mPreferences = getSharedPreferences(getContext()); DisplayMetrics metrics = getDisplayMetrics(); mPreferenceX = String.format(Locale.US, PREFERENCES_FAB_X, getContext().getClass().getName(), metrics.widthPixels, metrics.heightPixels); mPreferenceY = String.format(Locale.US, PREFERENCES_FAB_Y, getContext().getClass().getName(), metrics.widthPixels, metrics.heightPixels); } return mPreferences; } private static SharedPreferences getSharedPreferences(Context context) { return context.getSharedPreferences(context.getPackageName() + PREFERENCES_FAB, Context.MODE_PRIVATE); } }