package wei.mark.standout.ui;
import java.util.LinkedList;
import java.util.Queue;
import com.actionbarsherlock.R;
import wei.mark.standout.StandOutWindow;
import wei.mark.standout.StandOutWindow.StandOutLayoutParams;
import wei.mark.standout.Utils;
import wei.mark.standout.constants.StandOutFlags;
import android.content.Context;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.TextView;
/**
* Special view that represents a floating window.
*
* @author Mark Wei <markwei@gmail.com>
*
*/
public class Window extends FrameLayout {
public static final int VISIBILITY_GONE = 0;
public static final int VISIBILITY_VISIBLE = 1;
public static final int VISIBILITY_TRANSITION = 2;
static final String TAG = "Window";
/**
* Class of the window, indicating which application the window belongs to.
*/
public Class<? extends StandOutWindow> cls;
/**
* Id of the window.
*/
public int id;
/**
* Whether the window is shown, hidden/closed, or in transition.
*/
public int visibility;
/**
* Whether the window is focused.
*/
public boolean focused;
/**
* Original params from {@link StandOutWindow#getParams(int, Window)}.
*/
public StandOutLayoutParams originalParams;
/**
* Original flags from {@link StandOutWindow#getFlags(int)}.
*/
public int flags;
/**
* Touch information of the window.
*/
public TouchInfo touchInfo;
/**
* Data attached to the window.
*/
public Bundle data;
/**
* Width and height of the screen.
*/
int displayWidth, displayHeight;
/**
* Context of the window.
*/
private final StandOutWindow mContext;
private LayoutInflater mLayoutInflater;
public Window(Context context) {
super(context);
mContext = null;
}
public Window(final StandOutWindow context, final int id) {
super(context);
context.setTheme(context.getThemeStyle());
mContext = context;
mLayoutInflater = LayoutInflater.from(context);
this.cls = context.getClass();
this.id = id;
this.originalParams = context.getParams(id, this);
this.flags = context.getFlags(id);
this.touchInfo = new TouchInfo();
touchInfo.ratio = (float) originalParams.width / originalParams.height;
this.data = new Bundle();
DisplayMetrics metrics = mContext.getResources()
.getDisplayMetrics();
displayWidth = metrics.widthPixels;
displayHeight = (int) (metrics.heightPixels - 25 * metrics.density);
// create the window contents
View content;
FrameLayout body;
if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_SYSTEM)) {
// requested system window decorations
content = getSystemDecorations();
body = (FrameLayout) content.findViewById(R.id.body);
} else {
// did not request decorations. will provide own implementation
content = new FrameLayout(context);
content.setId(R.id.content);
body = (FrameLayout) content;
}
addView(content);
body.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// pass all touch events to the implementation
boolean consumed = false;
// handle move and bring to front
consumed = context.onTouchHandleMove(id, Window.this, v, event)
|| consumed;
// alert implementation
consumed = context.onTouchBody(id, Window.this, v, event)
|| consumed;
return consumed;
}
});
// attach the view corresponding to the id from the
// implementation
context.createAndAttachView(id, body);
// make sure the implementation attached the view
if (body.getChildCount() == 0) {
throw new RuntimeException(
"You must attach your view to the given frame in createAndAttachView()");
}
// implement StandOut specific workarounds
if (!Utils.isSet(flags,
StandOutFlags.FLAG_FIX_COMPATIBILITY_ALL_DISABLE)) {
fixCompatibility(body);
}
// implement StandOut specific additional functionality
if (!Utils.isSet(flags,
StandOutFlags.FLAG_ADD_FUNCTIONALITY_ALL_DISABLE)) {
addFunctionality(body);
}
// attach the existing tag from the frame to the window
setTag(body.getTag());
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
StandOutLayoutParams params = getLayoutParams();
// focus window
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (mContext.getFocusedWindow() != this) {
mContext.focus(id);
}
}
// multitouch
if (event.getPointerCount() >= 2
&& Utils.isSet(flags,
StandOutFlags.FLAG_WINDOW_PINCH_RESIZE_ENABLE)
&& (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) {
touchInfo.scale = 1;
touchInfo.dist = -1;
touchInfo.firstWidth = params.width;
touchInfo.firstHeight = params.height;
return true;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// handle touching outside
switch (event.getAction()) {
case MotionEvent.ACTION_OUTSIDE:
// unfocus window
if (mContext.getFocusedWindow() == this) {
mContext.unfocus(this);
}
// notify implementation that ACTION_OUTSIDE occurred
mContext.onTouchBody(id, this, this, event);
break;
}
// handle multitouch
if (event.getPointerCount() >= 2
&& Utils.isSet(flags,
StandOutFlags.FLAG_WINDOW_PINCH_RESIZE_ENABLE)) {
// 2 fingers or more
float x0 = event.getX(0);
float y0 = event.getY(0);
float x1 = event.getX(1);
float y1 = event.getY(1);
double dist = Math
.sqrt(Math.pow(x0 - x1, 2) + Math.pow(y0 - y1, 2));
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE:
if (touchInfo.dist == -1) {
touchInfo.dist = dist;
}
touchInfo.scale *= dist / touchInfo.dist;
touchInfo.dist = dist;
// scale the window with anchor point set to middle
edit().setAnchorPoint(.5f, .5f)
.setSize(
(int) (touchInfo.firstWidth * touchInfo.scale),
(int) (touchInfo.firstHeight * touchInfo.scale))
.commit();
break;
}
mContext.onResize(id, this, this, event);
}
return true;
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (mContext.onKeyEvent(id, this, event)) {
Log.d(TAG, "Window " + id + " key event " + event
+ " cancelled by implementation.");
return false;
}
if (event.getAction() == KeyEvent.ACTION_UP) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_BACK:
mContext.unfocus(this);
return true;
}
}
return super.dispatchKeyEvent(event);
}
/**
* Request or remove the focus from this window.
*
* @param focus
* Whether we want to gain or lose focus.
* @return True if focus changed successfully, false if it failed.
*/
public boolean onFocus(boolean focus) {
if (!Utils.isSet(flags, StandOutFlags.FLAG_WINDOW_FOCUSABLE_DISABLE)) {
// window is focusable
if (focus == focused) {
// window already focused/unfocused
return false;
}
focused = focus;
// alert callbacks and cancel if instructed
if (mContext.onFocusChange(id, this, focus)) {
Log.d(TAG, "Window " + id + " focus change "
+ (focus ? "(true)" : "(false)")
+ " cancelled by implementation.");
focused = !focus;
return false;
}
if (!Utils.isSet(flags,
StandOutFlags.FLAG_WINDOW_FOCUS_INDICATOR_DISABLE)) {
// change visual state
View content = findViewById(R.id.content);
if (focus) {
// gaining focus
content.setBackgroundResource(R.drawable.so_border_focused);
} else {
// losing focus
if (Utils
.isSet(flags, StandOutFlags.FLAG_DECORATION_SYSTEM)) {
// system decorations
content.setBackgroundResource(R.drawable.so_border);
} else {
// no decorations
content.setBackgroundResource(0);
}
}
}
// set window manager params
StandOutLayoutParams params = getLayoutParams();
params.setFocusFlag(focus);
mContext.updateViewLayout(id, params);
if (focus) {
mContext.setFocusedWindow(this);
} else {
if (mContext.getFocusedWindow() == this) {
mContext.setFocusedWindow(null);
}
}
return true;
}
return false;
}
@Override
public void setLayoutParams(ViewGroup.LayoutParams params) {
if (params instanceof StandOutLayoutParams) {
super.setLayoutParams(params);
} else {
throw new IllegalArgumentException(
"Window"
+ id
+ ": LayoutParams must be an instance of StandOutLayoutParams.");
}
}
/**
* Convenience method to start editting the size and position of this
* window. Make sure you call {@link Editor#commit()} when you are done to
* update the window.
*
* @return The Editor associated with this window.
*/
public Editor edit() {
return new Editor();
}
@Override
public StandOutLayoutParams getLayoutParams() {
StandOutLayoutParams params = (StandOutLayoutParams) super
.getLayoutParams();
if (params == null) {
params = originalParams;
}
return params;
}
/**
* Returns the system window decorations if the implementation sets
* {@link #FLAG_DECORATION_SYSTEM}.
*
* <p>
* The system window decorations support hiding, closing, moving, and
* resizing.
*
* @return The frame view containing the system window decorations.
*/
private View getSystemDecorations() {
final View decorations = mLayoutInflater.inflate(
R.layout.so_system_window_decorators, null);
// icon
final ImageView icon = (ImageView) decorations
.findViewById(R.id.window_icon);
icon.setImageResource(mContext.getAppIcon());
icon.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
PopupWindow dropDown = mContext.getDropDown(id);
if (dropDown != null) {
dropDown.showAsDropDown(icon);
}
}
});
// title
TextView title = (TextView) decorations.findViewById(R.id.title);
title.setText(mContext.getTitle(id));
// hide
View hide = decorations.findViewById(R.id.hide);
hide.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mContext.hide(id);
}
});
hide.setVisibility(View.GONE);
// maximize
View maximize = decorations.findViewById(R.id.maximize);
maximize.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
StandOutLayoutParams params = getLayoutParams();
boolean isMaximized = data
.getBoolean(WindowDataKeys.IS_MAXIMIZED);
if (isMaximized && params.width == displayWidth
&& params.height == displayHeight && params.x == 0
&& params.y == 0) {
data.putBoolean(WindowDataKeys.IS_MAXIMIZED, false);
int oldWidth = data.getInt(
WindowDataKeys.WIDTH_BEFORE_MAXIMIZE, -1);
int oldHeight = data.getInt(
WindowDataKeys.HEIGHT_BEFORE_MAXIMIZE, -1);
int oldX = data
.getInt(WindowDataKeys.X_BEFORE_MAXIMIZE, -1);
int oldY = data
.getInt(WindowDataKeys.Y_BEFORE_MAXIMIZE, -1);
edit().setSize(oldWidth, oldHeight).setPosition(oldX, oldY)
.commit();
} else {
data.putBoolean(WindowDataKeys.IS_MAXIMIZED, true);
data.putInt(WindowDataKeys.WIDTH_BEFORE_MAXIMIZE,
params.width);
data.putInt(WindowDataKeys.HEIGHT_BEFORE_MAXIMIZE,
params.height);
data.putInt(WindowDataKeys.X_BEFORE_MAXIMIZE, params.x);
data.putInt(WindowDataKeys.Y_BEFORE_MAXIMIZE, params.y);
edit().setSize(1f, 1f).setPosition(0, 0).commit();
}
}
});
// close
View close = decorations.findViewById(R.id.close);
close.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mContext.close(id);
}
});
// move
View titlebar = decorations.findViewById(R.id.titlebar);
titlebar.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// handle dragging to move
boolean consumed = mContext.onTouchHandleMove(id, Window.this,
v, event);
return consumed;
}
});
// resize
View corner = decorations.findViewById(R.id.corner);
corner.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// handle dragging to move
boolean consumed = mContext.onTouchHandleResize(id,
Window.this, v, event);
return consumed;
}
});
// set window appearance and behavior based on flags
if (Utils.isSet(flags, StandOutFlags.FLAG_WINDOW_HIDE_ENABLE)) {
hide.setVisibility(View.VISIBLE);
}
if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_MAXIMIZE_DISABLE)) {
maximize.setVisibility(View.GONE);
}
if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_CLOSE_DISABLE)) {
close.setVisibility(View.GONE);
}
if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_MOVE_DISABLE)) {
titlebar.setOnTouchListener(null);
}
if (Utils.isSet(flags, StandOutFlags.FLAG_DECORATION_RESIZE_DISABLE)) {
corner.setVisibility(View.GONE);
}
return decorations;
}
/**
* Implement StandOut specific additional functionalities.
*
* <p>
* Currently, this method does the following:
*
* <p>
* Attach resize handles: For every View found to have id R.id.corner,
* attach an OnTouchListener that implements resizing the window.
*
* @param root
* The view hierarchy that is part of the window.
*/
void addFunctionality(View root) {
// corner for resize
if (!Utils.isSet(flags,
StandOutFlags.FLAG_ADD_FUNCTIONALITY_RESIZE_DISABLE)) {
View corner = root.findViewById(R.id.corner);
if (corner != null) {
corner.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// handle dragging to move
boolean consumed = mContext.onTouchHandleResize(id,
Window.this, v, event);
return consumed;
}
});
}
}
// window_icon for drop down
if (!Utils.isSet(flags,
StandOutFlags.FLAG_ADD_FUNCTIONALITY_DROP_DOWN_DISABLE)) {
final View icon = root.findViewById(R.id.window_icon);
if (icon != null) {
icon.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
PopupWindow dropDown = mContext.getDropDown(id);
if (dropDown != null) {
dropDown.showAsDropDown(icon);
}
}
});
}
}
}
/**
* Iterate through each View in the view hiearchy and implement StandOut
* specific compatibility workarounds.
*
* <p>
* Currently, this method does the following:
*
* <p>
* Nothing yet.
*
* @param root
* The root view hierarchy to iterate through and check.
*/
void fixCompatibility(View root) {
Queue<View> queue = new LinkedList<View>();
queue.add(root);
View view = null;
while ((view = queue.poll()) != null) {
// do nothing yet
// iterate through children
if (view instanceof ViewGroup) {
ViewGroup group = (ViewGroup) view;
for (int i = 0; i < group.getChildCount(); i++) {
queue.add(group.getChildAt(i));
}
}
}
}
/**
* Convenient way to resize or reposition a Window. The Editor allows you to
* easily resize and reposition the window around anchor points.
*
* @author Mark Wei <markwei@gmail.com>
*
*/
public class Editor {
/**
* Special value for width, height, x, or y positions that represents
* that the value should not be changed.
*/
public static final int UNCHANGED = Integer.MIN_VALUE;
/**
* Layout params of the window associated with this Editor.
*/
StandOutLayoutParams mParams;
/**
* The position of the anchor point as a percentage of the window's
* width/height. The anchor point is only used by the {@link Editor}.
*
* <p>
* The anchor point effects the following methods:
*
* <p>
* {@link #setSize(float, float)}, {@link #setSize(int, int)},
* {@link #setPosition(int, int)}, {@link #setPosition(int, int)}.
*
* The window will move, expand, or shrink around the anchor point.
*
* <p>
* Values must be between 0 and 1, inclusive. 0 means the left/top, 0.5
* is the center, 1 is the right/bottom.
*/
float anchorX, anchorY;
public Editor() {
mParams = getLayoutParams();
anchorX = anchorY = 0;
}
public Editor setAnchorPoint(float x, float y) {
if (x < 0 || x > 1 || y < 0 || y > 1) {
throw new IllegalArgumentException(
"Anchor point must be between 0 and 1, inclusive.");
}
anchorX = x;
anchorY = y;
return this;
}
/**
* Set the size of this window as percentages of max screen size. The
* window will expand and shrink around the top-left corner, unless
* you've set a different anchor point with
* {@link #setAnchorPoint(float, float)}.
*
* Changes will not applied until you {@link #commit()}.
*
* @param percentWidth
* @param percentHeight
* @return The same Editor, useful for method chaining.
*/
public Editor setSize(float percentWidth, float percentHeight) {
return setSize((int) (displayWidth * percentWidth),
(int) (displayHeight * percentHeight));
}
/**
* Set the size of this window in absolute pixels. The window will
* expand and shrink around the top-left corner, unless you've set a
* different anchor point with {@link #setAnchorPoint(float, float)}.
*
* Changes will not applied until you {@link #commit()}.
*
* @param width
* @param height
* @return The same Editor, useful for method chaining.
*/
public Editor setSize(int width, int height) {
return setSize(width, height, false);
}
/**
* Set the size of this window in absolute pixels. The window will
* expand and shrink around the top-left corner, unless you've set a
* different anchor point with {@link #setAnchorPoint(float, float)}.
*
* Changes will not applied until you {@link #commit()}.
*
* @param width
* @param height
* @param skip
* Don't call {@link #setPosition(int, int)} to avoid stack
* overflow.
* @return The same Editor, useful for method chaining.
*/
private Editor setSize(int width, int height, boolean skip) {
if (mParams != null) {
if (anchorX < 0 || anchorX > 1 || anchorY < 0 || anchorY > 1) {
throw new IllegalStateException(
"Anchor point must be between 0 and 1, inclusive.");
}
int lastWidth = mParams.width;
int lastHeight = mParams.height;
if (width != UNCHANGED) {
mParams.width = width;
}
if (height != UNCHANGED) {
mParams.height = height;
}
// set max width/height
int maxWidth = mParams.maxWidth;
int maxHeight = mParams.maxHeight;
if (Utils.isSet(flags,
StandOutFlags.FLAG_WINDOW_EDGE_LIMITS_ENABLE)) {
maxWidth = (int) Math.min(maxWidth, displayWidth);
maxHeight = (int) Math.min(maxHeight, displayHeight);
}
// keep window between min and max
mParams.width = Math.min(
Math.max(mParams.width, mParams.minWidth), maxWidth);
mParams.height = Math.min(
Math.max(mParams.height, mParams.minHeight), maxHeight);
// keep window in aspect ratio
if (Utils.isSet(flags,
StandOutFlags.FLAG_WINDOW_ASPECT_RATIO_ENABLE)) {
int ratioWidth = (int) (mParams.height * touchInfo.ratio);
int ratioHeight = (int) (mParams.width / touchInfo.ratio);
if (ratioHeight >= mParams.minHeight
&& ratioHeight <= mParams.maxHeight) {
// width good adjust height
mParams.height = ratioHeight;
} else {
// height good adjust width
mParams.width = ratioWidth;
}
}
if (!skip) {
// set position based on anchor point
setPosition((int) (mParams.x + lastWidth * anchorX),
(int) (mParams.y + lastHeight * anchorY));
}
}
return this;
}
/**
* Set the position of this window as percentages of max screen size.
* The window's top-left corner will be positioned at the given x and y,
* unless you've set a different anchor point with
* {@link #setAnchorPoint(float, float)}.
*
* Changes will not applied until you {@link #commit()}.
*
* @param percentWidth
* @param percentHeight
* @return The same Editor, useful for method chaining.
*/
public Editor setPosition(float percentWidth, float percentHeight) {
return setPosition((int) (displayWidth * percentWidth),
(int) (displayHeight * percentHeight));
}
/**
* Set the position of this window in absolute pixels. The window's
* top-left corner will be positioned at the given x and y, unless
* you've set a different anchor point with
* {@link #setAnchorPoint(float, float)}.
*
* Changes will not applied until you {@link #commit()}.
*
* @param x
* @param y
* @return The same Editor, useful for method chaining.
*/
public Editor setPosition(int x, int y) {
return setPosition(x, y, false);
}
/**
* Set the position of this window in absolute pixels. The window's
* top-left corner will be positioned at the given x and y, unless
* you've set a different anchor point with
* {@link #setAnchorPoint(float, float)}.
*
* Changes will not applied until you {@link #commit()}.
*
* @param x
* @param y
* @param skip
* Don't call {@link #setPosition(int, int)} and
* {@link #setSize(int, int)} to avoid stack overflow.
* @return The same Editor, useful for method chaining.
*/
private Editor setPosition(int x, int y, boolean skip) {
if (mParams != null) {
if (anchorX < 0 || anchorX > 1 || anchorY < 0 || anchorY > 1) {
throw new IllegalStateException(
"Anchor point must be between 0 and 1, inclusive.");
}
// sets the x and y correctly according to anchorX and
// anchorY
if (x != UNCHANGED) {
mParams.x = (int) (x - mParams.width * anchorX);
}
if (y != UNCHANGED) {
mParams.y = (int) (y - mParams.height * anchorY);
}
if (Utils.isSet(flags,
StandOutFlags.FLAG_WINDOW_EDGE_LIMITS_ENABLE)) {
// if gravity is not TOP|LEFT throw exception
if (mParams.gravity != (Gravity.TOP | Gravity.LEFT)) {
throw new IllegalStateException(
"The window "
+ id
+ " gravity must be TOP|LEFT if FLAG_WINDOW_EDGE_LIMITS_ENABLE or FLAG_WINDOW_EDGE_TILE_ENABLE is set.");
}
// keep window inside edges
mParams.x = Math.min(Math.max(mParams.x, 0), displayWidth
- mParams.width);
mParams.y = Math.min(Math.max(mParams.y, 0), displayHeight
- mParams.height);
}
}
return this;
}
/**
* Commit the changes to this window. Updates the layout. This Editor
* cannot be used after you commit.
*/
public void commit() {
if (mParams != null) {
mContext.updateViewLayout(id, mParams);
mParams = null;
}
}
}
public static class WindowDataKeys {
public static final String IS_MAXIMIZED = "isMaximized";
public static final String WIDTH_BEFORE_MAXIMIZE = "widthBeforeMaximize";
public static final String HEIGHT_BEFORE_MAXIMIZE = "heightBeforeMaximize";
public static final String X_BEFORE_MAXIMIZE = "xBeforeMaximize";
public static final String Y_BEFORE_MAXIMIZE = "yBeforeMaximize";
}
}