/* * Copyright (C) 2011 The original author or authors. * * 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.zapta.apps.maniana.menus; import static com.zapta.apps.maniana.util.Assertions.check; import static com.zapta.apps.maniana.util.Assertions.checkNotNull; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.WindowManager; import android.widget.ImageView; import android.widget.PopupWindow; import android.widget.PopupWindow.OnDismissListener; import android.widget.RelativeLayout; import android.widget.TextView; import com.zapta.apps.maniana.R; import com.zapta.apps.maniana.annotations.MainActivityScope; import com.zapta.apps.maniana.main.MainActivityState; import com.zapta.apps.maniana.util.PopupsTracker.TrackablePopup; /** * Item action popup menu. * * @author Tal Dayan (adapted to Maniana) Based on example by Lorensius W. L. T * <lorenz@londatiga.net>. */ @MainActivityScope public class ItemMenu implements OnDismissListener, TrackablePopup { public interface OnActionItemOutcomeListener { /** Action item is null if dismissed with no selection. */ void onOutcome(ItemMenu source, @Nullable ItemMenuEntry actionItem); } private final MainActivityState mMainActivityState; /** The window that contains the menu's top view. */ private final PopupWindow mMenuWindow; private View mTopView; private ImageView mUpArrowView; private ImageView mDownArrowView; private ViewGroup mItemContainerView; private final OnActionItemOutcomeListener mOutcomeListener; private final List<ItemMenuEntry> actionItems = new ArrayList<ItemMenuEntry>(); private boolean mActioWasSelected; /** * Constructor allowing orientation override * * @param mContext Context * @param orientation Layout orientation, can be vartical or horizontal */ public ItemMenu(MainActivityState mainActivityState, OnActionItemOutcomeListener outcomeListener) { mMainActivityState = mainActivityState; mMenuWindow = new PopupWindow(mainActivityState.context()); mMenuWindow.setTouchInterceptor(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { mMenuWindow.dismiss(); return true; } return false; } }); mOutcomeListener = checkNotNull(outcomeListener); mTopView = (ViewGroup) mMainActivityState.services().layoutInflater() .inflate(R.layout.item_menu, null); mItemContainerView = (ViewGroup) mTopView.findViewById(R.id.items_container); mDownArrowView = (ImageView) mTopView.findViewById(R.id.arrow_down); mUpArrowView = (ImageView) mTopView.findViewById(R.id.arrow_up); mTopView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); mMenuWindow.setContentView(mTopView); mMenuWindow.setOnDismissListener(this); } /** * Get action item at given index */ public final ItemMenuEntry getActionItem(int index) { return actionItems.get(index); } /** * Add an action item to the end of the list. */ public final void addActionItem(final ItemMenuEntry actionItem) { actionItems.add(actionItem); // TODO: rename this to action_wrapper here and in the layout. final View wrapperView = mMainActivityState.services().layoutInflater() .inflate(R.layout.item_menu_entry, null); final ImageView imageView = (ImageView) wrapperView .findViewById(R.id.item_menu_entry_icon); imageView.setImageDrawable(actionItem.getIcon()); final TextView textView = (TextView) wrapperView.findViewById(R.id.item_menu_entry_text); textView.setText(actionItem.getLabel()); // Set a listener to track touches and highlight pressed items. wrapperView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { wrapperView.setBackgroundResource(R.drawable.popup_menu_entry_selected); } else if (event.getAction() == MotionEvent.ACTION_CANCEL || event.getAction() == MotionEvent.ACTION_UP || !wrapperView.isPressed()) { wrapperView.setBackgroundColor(Color.TRANSPARENT); } return false; } }); wrapperView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mOutcomeListener.onOutcome(ItemMenu.this, actionItem); mActioWasSelected = true; mMenuWindow.dismiss(); } }); wrapperView.setFocusable(true); wrapperView.setClickable(true); // If not first, add seperator before it. if (mItemContainerView.getChildCount() > 0) { final View separator = mMainActivityState.services().layoutInflater() .inflate(R.layout.item_menu_separator, null); // TODO: move this configuration to the XML RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.FILL_PARENT); separator.setLayoutParams(params); separator.setPadding(5, 0, 5, 0); mItemContainerView.addView(separator); } mItemContainerView.addView(wrapperView); } /** * Show the popup action menu over a given anchor view. * * @param anchorView a view to which the action menu's arrow will point it. */ public final void show(View anchorView) { if (mTopView == null) { throw new IllegalStateException("setContentView was not called with a view to display."); } // Set transparent window background. This will clear the horizontal strips above and below // the menu defined by the two arrows. mMenuWindow.setBackgroundDrawable(new BitmapDrawable()); mMenuWindow.setWidth(WindowManager.LayoutParams.WRAP_CONTENT); mMenuWindow.setHeight(WindowManager.LayoutParams.WRAP_CONTENT); mMenuWindow.setTouchable(true); mMenuWindow.setFocusable(true); mMenuWindow.setOutsideTouchable(true); mMenuWindow.setContentView(mTopView); mActioWasSelected = false; final int[] anchorXYOnsScreen = new int[2]; anchorView.getLocationOnScreen(anchorXYOnsScreen); final Rect anchorRectOnScreen = new Rect(anchorXYOnsScreen[0], anchorXYOnsScreen[1], anchorXYOnsScreen[0] + anchorView.getWidth(), anchorXYOnsScreen[1] + anchorView.getHeight()); mTopView.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); final int rootHeight = mTopView.getMeasuredHeight(); final int screenHeight = mMainActivityState.services().windowManager().getDefaultDisplay().getHeight(); // Arrow position is slightly to the right of the left upper/lower cornet. // TODO: define const. // TODO: scale by density? final int xPosPixels = 50; final int arrowPos = anchorRectOnScreen.left + xPosPixels; int spaceAbove = anchorRectOnScreen.top; int spaceBelow = screenHeight - anchorRectOnScreen.bottom; final boolean showAbove = spaceAbove >= rootHeight; // TODO: make a const or param. // TODO: scale by density? final int ARROW_VERTICAL_OVERLAP = 15; final int yPosPixels; if (showAbove) { check(rootHeight <= spaceAbove); yPosPixels = anchorRectOnScreen.top - rootHeight + ARROW_VERTICAL_OVERLAP; } else { yPosPixels = anchorRectOnScreen.bottom - ARROW_VERTICAL_OVERLAP; if (rootHeight > spaceBelow) { mItemContainerView.getLayoutParams().height = spaceBelow; } } showArrow(((showAbove) ? R.id.arrow_down : R.id.arrow_up), arrowPos); mMenuWindow.setAnimationStyle((showAbove) ? R.style.Animations_ItemMenuAbove : R.style.Animations_ItemMenuBelow); mMenuWindow.showAtLocation(anchorView, Gravity.NO_GRAVITY, xPosPixels, yPosPixels); } /** * Show arrow * * @param whichArrow arrow type resource id * @param requestedX distance from left screen */ private final void showArrow(int whichArrow, int requestedX) { // Decide which of the two up and down arrows will be shown and hidden respectivly. final View showArrow = (whichArrow == R.id.arrow_up) ? mUpArrowView : mDownArrowView; final View hideArrow = (whichArrow == R.id.arrow_up) ? mDownArrowView : mUpArrowView; final int arrowWidth = mUpArrowView.getMeasuredWidth(); showArrow.setVisibility(View.VISIBLE); ViewGroup.MarginLayoutParams param = (ViewGroup.MarginLayoutParams) showArrow .getLayoutParams(); param.leftMargin = requestedX - arrowWidth / 2; hideArrow.setVisibility(View.INVISIBLE); } @Override public final void onDismiss() { if (!mActioWasSelected) { mOutcomeListener.onOutcome(this, null); } } @Override public final void closeLeftOver() { // NOTE: we don't bother here to early report the dismissal, as we do with the item editor, // because the dismissal of this menu does not cause a mutation of the model. if (mMenuWindow.isShowing()) { mMenuWindow.dismiss(); } } }