517 lines
16 KiB
Java
517 lines
16 KiB
Java
/*
|
|
* 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.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);
|
|
}
|
|
|
|
@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);
|
|
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);
|
|
}
|
|
BaseActivity.setFont(button, typeface);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|