/* * Copyright 2015. Appsi Mobile * * 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 com.appsimobile.appsii; import android.content.Context; import android.content.SharedPreferences; import android.database.ContentObserver; import android.database.Cursor; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; import android.os.Message; import android.os.Vibrator; import android.support.v4.util.CircularArray; import android.util.AttributeSet; import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import com.appsimobile.appsii.dagger.AppInjector; import com.appsimobile.appsii.module.home.provider.HomeContract; import com.appsimobile.util.ArrayUtils; import java.lang.ref.WeakReference; import javax.inject.Inject; public class SidebarHotspot extends View { public static final int VIBRATE_DURATION = 20; static final int STATE_AWAITING_RELEASE = 3; private static final int STATE_WAITING = 0; private static final int STATE_GESTURE_IN_PROGRESS = 2; private static final int SIDEBAR_MINIMUM_MOVE = 0; final CircularArray<HotspotPageEntry> mHotspotPageEntries = new CircularArray<>(8); final Handler mHandler = new Handler(); int mState = STATE_WAITING; boolean mSwipeInProgress; SidebarGestureCallback mCallback; boolean mIsDragOpening; float mStartX; float mStartY; float mRawStartY; SwipeListener mSwipeListener; ContentObserver mHotspotsPagesObserver; AsyncTask<Void, Void, CircularArray<HotspotPageEntry>> mLoadDataTask; /** * A shared preferences listener */ SharedPreferencesListener mSharedPreferencesListener; @Inject Vibrator mVibrator; @Inject SharedPreferences mPreferences; private float mMinimumMove; private boolean mVibrate; private boolean mLeft; private int mTop; private int mLeftPos; private boolean mVisibleHotspots; /** * The hotspot-item this hotspot is bound to. * This contains the properties of the hotspot. */ private HotspotItem mHotspotItem; /** * A velocity tracked given to the listeners so they can determine the speed if needed */ private VelocityTracker mVelocityTracker; /** * The current background drawable */ private Drawable mBackground; public SidebarHotspot(Context context) { super(context); } public SidebarHotspot(Context context, AttributeSet attrs) { super(context, attrs); } public SidebarHotspot(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public static Gesture detectAction(float deltaX, float deltaY, float minDistance) { Gesture swipeAction = null; if (deltaX >= minDistance) { swipeAction = Gesture.TO_CENTER; } else if (deltaY > minDistance) { swipeAction = Gesture.DOWN; } else if (deltaY < -minDistance) { swipeAction = Gesture.UP; } return swipeAction; } @Override public boolean onTouchEvent(MotionEvent e) { switch (e.getAction()) { case MotionEvent.ACTION_DOWN: { mVelocityTracker = VelocityTracker.obtain(); mVelocityTracker.addMovement(e); // remove the background to make sure it does not overlap // the sidebar mIsDragOpening = true; setBackgroundResource(0); float x = e.getX(); float y = e.getY(); if (mCallback != null) { mSwipeListener = mCallback.open(this, Gesture.TO_CENTER, (int) x, (int) y); mSwipeInProgress = mSwipeListener != null; mState = STATE_AWAITING_RELEASE; if (mVibrate) { vibrate(); } return true; } return false; } case MotionEvent.ACTION_MOVE: mVelocityTracker.addMovement(e); float x = e.getX(); float y = e.getY(); return detectSwipe(x, y, e); case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: cancelMotionHandling(e, false); return false; } return super.onTouchEvent(e); } private void vibrate() { mVibrator.vibrate(VIBRATE_DURATION); } private boolean detectSwipe(float x, float y, MotionEvent e) { if (mState == STATE_AWAITING_RELEASE && mSwipeInProgress) { if (mCallback != null) { mSwipeListener.setSwipeLocation(this, (int) e.getX(), (int) e.getY(), (int) e.getRawX(), (int) e.getRawY()); } return true; } if (mState != STATE_GESTURE_IN_PROGRESS) return false; float deltaX = Math.abs(x - mStartX); float deltaY = y - mStartY; Gesture action = detectAction(deltaX, deltaY, mMinimumMove); if (action != null) { mSwipeListener = mCallback.open(this, action, (int) x, (int) y); mSwipeInProgress = mSwipeListener != null; mState = STATE_AWAITING_RELEASE; return true; } return true; } private void cancelMotionHandling(MotionEvent e, boolean cancelled) { mState = STATE_WAITING; if (mSwipeListener != null) { if (e == null) { mSwipeListener.onSwipeEnd(this, 0, 0, cancelled, mVelocityTracker); } else { mSwipeListener.onSwipeEnd(this, (int) e.getRawX(), (int) e.getRawY(), cancelled, mVelocityTracker); } } if (mCallback != null) { mCallback.cancelVisualHints(this); } mSwipeInProgress = false; mSwipeListener = null; // restore the background setBackground(mBackground); mIsDragOpening = false; if (mCallback != null) { mCallback.removeIfNeeded(this); } invalidate(); mVelocityTracker.addMovement(e); mVelocityTracker.recycle(); mVelocityTracker = null; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mSwipeInProgress = false; if (mHotspotsPagesObserver == null) { mHotspotsPagesObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { onChange(selfChange, null); } @Override public void onChange(boolean selfChange, Uri uri) { reloadHotspotData(); } }; getContext().getContentResolver().registerContentObserver( HomeContract.HotspotPages.CONTENT_URI, true, mHotspotsPagesObserver); } setupBackground(); } void reloadHotspotData() { Log.d("SidebarHotspot", "reloading hotspots"); if (mHotspotItem == null) return; final long hotspotId = mHotspotItem.mId; final Context context = getContext(); if (mLoadDataTask != null) mLoadDataTask.cancel(true); mLoadDataTask = new AsyncTask<Void, Void, CircularArray<HotspotPageEntry>>() { @Override protected CircularArray<HotspotPageEntry> doInBackground(Void... params) { Cursor c = context.getContentResolver(). query(HotspotPagesQuery.createUri(hotspotId), HotspotPagesQuery.PROJECTION, null, null, HomeContract.HotspotDetails.POSITION + " ASC" ); CircularArray<HotspotPageEntry> result = new CircularArray<>(c.getCount()); while (c.moveToNext()) { HotspotPageEntry entry = new HotspotPageEntry(); entry.mEnabled = c.getInt(HotspotPagesQuery.ENABLED) == 1; entry.mPageId = c.getLong(HotspotPagesQuery.PAGE_ID); entry.mHotspotId = c.getLong(HotspotPagesQuery.HOTSPOT_ID); entry.mPageName = c.getString(HotspotPagesQuery.PAGE_NAME); entry.mHotspotName = c.getString(HotspotPagesQuery.HOTSPOT_NAME); entry.mPosition = c.getInt(HotspotPagesQuery.POSITION); entry.mPageType = c.getInt(HotspotPagesQuery.PAGE_TYPE); result.addLast(entry); } c.close(); return result; } @Override protected void onPostExecute(CircularArray<HotspotPageEntry> hotspotPageEntries) { onHotspotEntriesLoaded(hotspotPageEntries); } }; mLoadDataTask.execute(); } private void setupBackground() { Drawable bg; if (mHotspotItem == null || !mVisibleHotspots) { mBackground = null; setBackground(null); return; } if (mHotspotItem.mLeft) { bg = getResources().getDrawable(R.drawable.floating_navigation_drawer_handle).mutate(); } else { bg = getResources().getDrawable(R.drawable.floating_navigation_drawer_handle_right) .mutate(); } mBackground = bg; setBackground(bg); } void onHotspotEntriesLoaded(CircularArray<HotspotPageEntry> hotspotPageEntries) { mHotspotPageEntries.clear(); ArrayUtils.addAll(mHotspotPageEntries, hotspotPageEntries); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mHotspotsPagesObserver != null) { getContext().getContentResolver().unregisterContentObserver( mHotspotsPagesObserver); mHotspotsPagesObserver = null; } if (mLoadDataTask != null) { mLoadDataTask.cancel(true); } } @Override protected void onFinishInflate() { super.onFinishInflate(); init(getContext()); } void init(Context context) { AppInjector.inject(this); float scale = AppsiApplication.getDensity(context); mMinimumMove = scale * SIDEBAR_MINIMUM_MOVE; SharedPreferences sharedPreferences = mPreferences; mVisibleHotspots = !sharedPreferences.getBoolean("pref_hide_hotspots", false); mSharedPreferencesListener = new SharedPreferencesListener(this); sharedPreferences.registerOnSharedPreferenceChangeListener(mSharedPreferencesListener); setClickable(true); setupBackground(); } public CircularArray<HotspotPageEntry> getHotspotPageEntries() { return mHotspotPageEntries; } public void setVibrateOnTouch(boolean vibrate) { mVibrate = vibrate; } public void setCallback(SidebarGestureCallback callback) { mCallback = callback; } public void setPosition(boolean left, int x, int y) { mLeft = left; mTop = y; mLeftPos = x; } public boolean isLeft() { return mLeft; } public int getHPos() { return mLeftPos; } public int getVPos() { return mTop; } void setVisibleHotspotsEnabled(boolean visibleHotspotsEnabled) { mVisibleHotspots = visibleHotspotsEnabled; setupBackground(); invalidate(); } void bind(HotspotItem hotspotItem) { mHotspotItem = hotspotItem; setupBackground(); reloadHotspotData(); } long getHotspotId() { return mHotspotItem.mId; } public HotspotItem getConfiguration() { return mHotspotItem; } public enum Gesture { UP, DOWN, TO_CENTER, TAP, DOUBLE_TAP, LONG_PRESS, } public interface SidebarGestureCallback { /** * Show the swyper * * @return true if the swyper was added and the gesture must be tracked to it's end */ SwipeListener open(SidebarHotspot hotspot, Gesture action, int x, int y); void cancelVisualHints(SidebarHotspot sidebarHotspot); SwipeListener longPressGesturePerformed(SidebarHotspot hotspot, int localX, int localY, int rawStartY); void removeIfNeeded(SidebarHotspot hotspot); } public interface SwipeListener { /** * Update the location of the swype with the raw x and y coordinates */ void setSwipeLocation(SidebarHotspot hotspot, int localX, int localY, int screenX, int screenY); /** * @param hotspot * @param screenX * @param screenY */ void onSwipeEnd(SidebarHotspot hotspot, int screenX, int screenY, boolean cancelled, VelocityTracker velocityTracker); } static class TapGestureDetector implements Handler.Callback { static final int mMinMovePx = 40; static final int WHAT_TAP_EVENT = 1; final int mMinMoveDip; final GestureDetector.OnDoubleTapListener mOnDoubleTapListener; final Handler mHandler; boolean mCouldBeTap; int mStartX; int mStartY; long mLastTapMillis = -1; int mLastTapX = -1; int mLastTapY = -1; TapGestureDetector(Context context, GestureDetector.OnDoubleTapListener doubleTapListener) { mMinMoveDip = (int) (mMinMovePx * context.getResources().getDisplayMetrics().density); mOnDoubleTapListener = doubleTapListener; mHandler = new Handler(this); } public boolean onTouchEvent(MotionEvent e) { switch (e.getAction()) { case MotionEvent.ACTION_DOWN: mCouldBeTap = true; mStartX = (int) e.getX(); mStartY = (int) e.getY(); break; case MotionEvent.ACTION_MOVE: if (!mCouldBeTap) break; int x = (int) e.getX(); int y = (int) e.getY(); int deltaX = Math.abs(x - mStartX); int deltaY = Math.abs(y - mStartY); if (deltaX > mMinMoveDip || deltaY > mMinMoveDip) { mCouldBeTap = false; } break; case MotionEvent.ACTION_CANCEL: mCouldBeTap = false; mLastTapMillis = -1; break; case MotionEvent.ACTION_UP: if (mCouldBeTap) { int tapX = (int) e.getX(); int tapY = (int) e.getY(); return onTapEvent(tapX, tapY, e); } break; } return false; } private boolean onTapEvent(int x, int y, MotionEvent event) { cancelWatchForSigleTap(); long time = System.currentTimeMillis(); long tapDelta = Math.abs(mLastTapMillis - time); if (tapDelta > 50 && tapDelta < 300) { int deltaX = Math.abs(x - mLastTapX); int deltaY = Math.abs(y - mLastTapY); if (deltaX <= mMinMoveDip && deltaY <= mMinMoveDip) { mOnDoubleTapListener.onDoubleTap(event); mLastTapMillis = -1; return true; } } startWatchForSingleTap(x, y, event); mLastTapMillis = time; mLastTapX = x; mLastTapY = y; return false; } private void cancelWatchForSigleTap() { mHandler.removeMessages(WHAT_TAP_EVENT); } private void startWatchForSingleTap(int x, int y, MotionEvent event) { Message message = mHandler.obtainMessage(WHAT_TAP_EVENT, x, y, event); mHandler.sendMessageDelayed(message, 100); } @Override public boolean handleMessage(Message msg) { switch (msg.what) { case WHAT_TAP_EVENT: int x = msg.arg1; int y = msg.arg2; MotionEvent event = (MotionEvent) msg.obj; handleSigleTap(x, y, event); break; } return false; } private void handleSigleTap(int x, int y, MotionEvent event) { mOnDoubleTapListener.onSingleTapConfirmed(event); } } static class SharedPreferencesListener implements SharedPreferences.OnSharedPreferenceChangeListener { private final WeakReference<SidebarHotspot> mSidebarHotspot; SharedPreferencesListener(SidebarHotspot sidebarHotspot) { mSidebarHotspot = new WeakReference<>(sidebarHotspot); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { SidebarHotspot hotspot = mSidebarHotspot.get(); if (hotspot == null) { sharedPreferences.unregisterOnSharedPreferenceChangeListener(this); return; } if (key.equals("pref_hide_hotspots")) { boolean prefHideHotspots = sharedPreferences.getBoolean("pref_hide_hotspots", false); hotspot.setVisibleHotspotsEnabled(!prefHideHotspots); } } } }