/* * Copyright 2013 serso aka se.solovyev * * 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. * * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Contact details * * Email: se.solovyev@gmail.com * Site: http://se.solovyev.org */ package org.solovyev.android.calculator.floating; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.PixelFormat; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.v7.view.ContextThemeWrapper; import android.util.DisplayMetrics; import android.view.Display; import android.view.*; import android.widget.ImageView; import org.solovyev.android.calculator.*; import org.solovyev.android.calculator.buttons.CppButton; import org.solovyev.android.calculator.keyboard.BaseKeyboardUi; import org.solovyev.android.views.Adjuster; import javax.annotation.Nonnull; import javax.inject.Inject; import javax.inject.Named; import static android.view.HapticFeedbackConstants.*; import static android.view.WindowManager.LayoutParams.*; import static org.solovyev.android.calculator.App.cast; public class FloatingCalculatorView { private static class MyTouchListener implements View.OnTouchListener { private static final float DIST_EPS = 0f; private static final float DIST_MAX = 100000f; private static final long TIME_EPS = 0L; @Nonnull private final WindowManager wm; @Nonnull private final View view; private int orientation; private float x0; private float y0; private long lastMoveTime = 0; private final DisplayMetrics dm = new DisplayMetrics(); public MyTouchListener(@Nonnull WindowManager wm, @Nonnull View view) { this.wm = wm; this.view = view; onDisplayChanged(); } private void onDisplayChanged() { final Display dd = wm.getDefaultDisplay(); //noinspection deprecation orientation = dd.getOrientation(); dd.getMetrics(dm); } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View v, MotionEvent event) { //noinspection deprecation if (orientation != wm.getDefaultDisplay().getOrientation()) { // orientation has changed => we need to check display width/height each time window moved onDisplayChanged(); } final float x1 = event.getRawX(); final float y1 = event.getRawY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: x0 = x1; y0 = y1; return true; case MotionEvent.ACTION_MOVE: final long now = System.currentTimeMillis(); if (now - lastMoveTime >= TIME_EPS) { lastMoveTime = now; processMove(x1, y1); } return true; } return false; } private void processMove(float x1, float y1) { final float Δx = x1 - x0; final float Δy = y1 - y0; final WindowManager.LayoutParams params = (WindowManager.LayoutParams) view.getLayoutParams(); boolean xInBounds = isDistanceInBounds(Δx); boolean yInBounds = isDistanceInBounds(Δy); if (xInBounds || yInBounds) { if (xInBounds) { params.x = (int) (params.x + Δx); } if (yInBounds) { params.y = (int) (params.y + Δy); } params.x = Math.min(Math.max(params.x, 0), dm.widthPixels - params.width); params.y = Math.min(Math.max(params.y, 0), dm.heightPixels - params.height); wm.updateViewLayout(view, params); if (xInBounds) { x0 = x1; } if (yInBounds) { y0 = y1; } } } private boolean isDistanceInBounds(float δx) { δx = Math.abs(δx); return δx >= DIST_EPS && δx < DIST_MAX; } } public static class State implements Parcelable { public static final Creator<State> CREATOR = new Creator<State>() { public State createFromParcel(@Nonnull Parcel in) { return new State(in); } public State[] newArray(int size) { return new State[size]; } }; public final int width; public final int height; public final int x; public final int y; public State(int width, int height, int x, int y) { this.width = width; this.height = height; this.x = x; this.y = y; } private State(@NonNull SharedPreferences prefs) { width = prefs.getInt("width", 200); height = prefs.getInt("height", 400); x = prefs.getInt("x", 0); y = prefs.getInt("y", 0); } public State(@Nonnull Parcel in) { width = in.readInt(); height = in.readInt(); x = in.readInt(); y = in.readInt(); } @android.support.annotation.Nullable public static State fromPrefs(@NonNull SharedPreferences prefs) { if(!prefs.contains("width")) { return null; } return new State(prefs); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@Nonnull Parcel out, int flags) { out.writeInt(width); out.writeInt(height); out.writeInt(x); out.writeInt(y); } @Override public String toString() { return "State{" + "y=" + y + ", x=" + x + ", height=" + height + ", width=" + width + '}'; } public void save(@NonNull SharedPreferences.Editor editor) { editor.putInt("width", width); editor.putInt("height", height); editor.putInt("x", x); editor.putInt("y", y); } } @NonNull private final Context context; @NonNull private final FloatingViewListener listener; @Inject Keyboard keyboard; @Inject Editor editor; @Inject SharedPreferences preferences; @Inject Typeface typeface; @Named(AppModule.PREFS_FLOATING) @Inject SharedPreferences myPreferences; private View root; private View content; private View header; private ImageView headerTitle; private Drawable headerTitleDrawable; private EditorView editorView; private DisplayView displayView; @Nonnull private final State state; private boolean minimized; private boolean attached; private boolean folded; private boolean initialized; private boolean shown; public FloatingCalculatorView(@Nonnull Context context, @Nonnull State state, @NonNull FloatingViewListener listener) { cast(context).getComponent().inject(this); this.listener = listener; final Preferences.SimpleTheme theme = Preferences.Onscreen.theme.getPreferenceNoError(preferences); final Preferences.Gui.Theme appTheme = Preferences.Gui.theme.getPreferenceNoError(preferences); final Preferences.SimpleTheme resolvedTheme = theme.resolveThemeFor(appTheme); this.context = new ContextThemeWrapper(context, resolvedTheme.light ? R.style.Cpp_Theme_Light : R.style.Cpp_Theme); this.root = View.inflate(this.context, theme.getOnscreenLayout(appTheme), null); BaseActivity.fixFonts(this.root, typeface); final State persistedState = State.fromPrefs(myPreferences); if (persistedState != null) { this.state = persistedState; } else { this.state = state; } } static boolean isOverlayPermissionGranted(@NonNull Context context) { try { final Context application = context.getApplicationContext(); final WindowManager wm = (WindowManager) application.getSystemService(Context.WINDOW_SERVICE); if (wm == null) { return false; } final View view = new View(application); wm.addView(view, makeLayoutParams()); wm.removeView(view); return true; } catch (Exception e) { return false; } } public void updateDisplayState(@Nonnull DisplayState displayState) { checkInit(); displayView.setState(displayState); } private void checkInit() { if (!initialized) { throw new IllegalStateException("init() must be called!"); } } public void updateEditorState(@Nonnull EditorState editorState) { checkInit(); editorView.setState(editorState); } private void setHeight(int height) { checkInit(); final WindowManager.LayoutParams params = (WindowManager.LayoutParams) root.getLayoutParams(); params.height = height; getWindowManager().updateViewLayout(root, params); } private void init() { if (initialized) { return; } for (final CppButton widgetButton : CppButton.values()) { final View button = root.findViewById(widgetButton.id); if (button == null) { continue; } button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (keyboard.buttonPressed(widgetButton.action)) { if (keyboard.isVibrateOnKeypress()) { v.performHapticFeedback(KEYBOARD_TAP, FLAG_IGNORE_GLOBAL_SETTING | FLAG_IGNORE_VIEW_SETTING); } } if (widgetButton == CppButton.app) { minimize(); } } }); button.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { if (keyboard.buttonPressed(widgetButton.actionLong)) { if (keyboard.isVibrateOnKeypress()) { v.performHapticFeedback(LONG_PRESS, FLAG_IGNORE_GLOBAL_SETTING | FLAG_IGNORE_VIEW_SETTING); } } return true; } }); if (widgetButton == CppButton.erase && button instanceof ImageView) { Adjuster.adjustImage((ImageView) button, BaseKeyboardUi.IMAGE_SCALE_ERASE); } else { BaseKeyboardUi.adjustButton(button); } } final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); header = root.findViewById(R.id.onscreen_header); headerTitle = (ImageView) header.findViewById(R.id.onscreen_title); headerTitleDrawable = headerTitle.getDrawable(); headerTitle.setImageDrawable(null); content = root.findViewById(R.id.onscreen_content); displayView = (DisplayView) root.findViewById(R.id.calculator_display); editorView = (EditorView) root.findViewById(R.id.calculator_editor); editorView.setEditor(editor); final View onscreenFoldButton = root.findViewById(R.id.onscreen_fold_button); onscreenFoldButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (folded) { unfold(); } else { fold(); } } }); final View onscreenHideButton = root.findViewById(R.id.onscreen_minimize_button); onscreenHideButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { minimize(); } }); root.findViewById(R.id.onscreen_close_button) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { hide(); } }); headerTitle.setOnTouchListener(new MyTouchListener(wm, root)); initialized = true; } public void show() { if (shown) { return; } init(); attach(); shown = true; } public void attach() { checkInit(); final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); if (!attached) { final WindowManager.LayoutParams params = makeLayoutParams(); params.width = state.width; params.height = state.height; params.x = state.x; params.y = state.y; params.gravity = Gravity.TOP | Gravity.LEFT; wm.addView(root, params); attached = true; } } @Nonnull private static WindowManager.LayoutParams makeLayoutParams() { return new WindowManager.LayoutParams( TYPE_SYSTEM_ALERT, FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_WATCH_OUTSIDE_TOUCH, PixelFormat.TRANSLUCENT); } private void fold() { if (!folded) { headerTitle.setImageDrawable(headerTitleDrawable); final Resources r = header.getResources(); final int newHeight = header.getHeight() + 2 * r .getDimensionPixelSize(R.dimen.cpp_onscreen_main_padding); content.setVisibility(View.GONE); setHeight(newHeight); folded = true; } } private void unfold() { if (folded) { headerTitle.setImageDrawable(null); content.setVisibility(View.VISIBLE); setHeight(state.height); folded = false; } } public void detach() { checkInit(); if (attached) { getWindowManager().removeView(root); attached = false; } } public void minimize() { checkInit(); if (!minimized) { saveState(); detach(); listener.onViewMinimized(); minimized = true; } } public void hide() { checkInit(); if (!shown) { return; } saveState(); detach(); listener.onViewHidden(); shown = false; } private void saveState() { final SharedPreferences.Editor editor = myPreferences.edit(); getState().save(editor); editor.apply(); } @Nonnull private WindowManager getWindowManager() { return ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)); } @Nonnull public State getState() { final WindowManager.LayoutParams params = (WindowManager.LayoutParams) root.getLayoutParams(); if (!folded) { return new State(params.width, params.height, params.x, params.y); } else { return new State(state.width, state.height, params.x, params.y); } } }