package net.bible.android.view.util.buttongrid; import android.content.Context; import android.graphics.Color; import android.graphics.Typeface; import android.os.Build; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.ViewGroup; import android.widget.Button; import android.widget.PopupWindow; import android.widget.TableLayout; import android.widget.TableRow; import android.widget.TextView; import net.bible.android.activity.R; import net.bible.android.view.util.buttongrid.LayoutDesigner.RowColLayout; import java.util.List; /** Show a grid of buttons to allow selection for navigation * * @author Martin Denham [mjdenham at gmail dot com] * @see gnu.lgpl.License for license details.<br> * The copyright to this program is held by it's author. */ public class ButtonGrid extends TableLayout { public static class ButtonInfo { public int id; public String name; public int textColor = Color.WHITE; public boolean highlight = false; // used internally private Button button; private int top; private int bottom; private int left; private int right; private int rowNo; private int colNo; } private ButtonInfo mCurrentPreview; private TextView mPreviewText; private PopupWindow mPreviewPopup; private int mPreviewOffset; private int mPreviewHeight; private static final int PREVIEW_HEIGHT_DIP = 70; private OnButtonGridActionListener onButtonGridActionListener; private List<ButtonInfo> buttonInfoList; private RowColLayout mRowColLayout; private ButtonInfo mPressed; private Context mContext; private boolean isInitialised = false; private static final String TAG = "ButtonGrid"; public ButtonGrid(Context context) { this(context, null, 0); } public ButtonGrid(Context context, AttributeSet attrs, int defStyle) { super(context, attrs); this.mContext = context; // use generic ViewGroup LayoutParams for Table because we don't know what the parent is setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); setStretchAllColumns(true); } public void clear() { removeAllViews(); buttonInfoList = null; mRowColLayout = null; mPressed = null; isInitialised = false; } /** Called during initialisation to add the list of buttons to be laid out on the screen * * @param buttonInfoList */ public void addButtons(List<ButtonInfo> buttonInfoList) { this.buttonInfoList = buttonInfoList; int numButtons = buttonInfoList.size(); int textSize = getResources().getInteger(R.integer.grid_cell_text_size_sp); // calculate the number of rows and columns so that the grid looks nice mRowColLayout = new LayoutDesigner().calculateLayout(buttonInfoList); LayoutParams rowInTableLp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 1.0f); TableRow.LayoutParams cellInRowLp = new TableRow.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 1.0f); for (int iRow=0; iRow<mRowColLayout.rows; iRow++) { TableRow row = new TableRow(mContext); addView(row, rowInTableLp); for (int iCol=0; iCol<mRowColLayout.cols; iCol++) { int buttonInfoIndex = getButtonInfoIndex(iRow, iCol); if (buttonInfoIndex<numButtons) { // create a graphical Button View object to show on the screen and link it to the ButtonInfo object ButtonInfo buttonInfo = buttonInfoList.get(buttonInfoIndex); Button button = new Button(mContext); button.setText(buttonInfo.name); if (buttonInfo.highlight) { button.setTypeface(Typeface.DEFAULT_BOLD); button.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize+1); } else { button.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize); } button.setBackgroundResource(R.drawable.buttongrid_button_background); button.setTextColor(buttonInfo.textColor); // set pad to 0 prevents text being pushed off the bottom of buttons on small screens button.setPadding(0, 0, 0, 0); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { button.setAllCaps(false); } buttonInfo.button = button; buttonInfo.rowNo = iRow; buttonInfo.colNo = iCol; row.addView(button, cellInRowLp); } else { TextView spacer = new TextView(mContext); row.addView(spacer, cellInRowLp); } } } LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mPreviewText = (TextView)inflater.inflate(R.layout.buttongrid_button_preview, null); mPreviewPopup = new PopupWindow(mPreviewText); mPreviewPopup.setContentView(mPreviewText); mPreviewPopup.setBackgroundDrawable(null); mPreviewPopup.setTouchable(false); mPreviewText.setCompoundDrawables(null, null, null, null); float scale = mContext.getResources().getDisplayMetrics().density; mPreviewHeight = (int)(PREVIEW_HEIGHT_DIP*scale); } /** Ensure longer runs by populating in longest direction ie columns if portrait and rows if landscape * * @param row * @param col * @return */ private int getButtonInfoIndex(int row, int col) { if (mRowColLayout.columnOrder) { return col*mRowColLayout.rows + row; } else { return row*mRowColLayout.cols + col; } } /* (non-Javadoc) * @see android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent) */ @Override public boolean onInterceptTouchEvent(MotionEvent event) { // wait until the columns have been layed out and adjusted before recording button positions if (!isInitialised) { synchronized (buttonInfoList) { if (!isInitialised) { recordButtonPositions(); isInitialised = true; } } } /* * This method JUST determines whether we want to intercept the motion. * If we return true, onMotionEvent will be called and we do the actual * scrolling there. */ return true; } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN : case MotionEvent.ACTION_MOVE : ButtonInfo but = findButton((int)event.getX(), (int)event.getY()); if (but!=null) { // show the button being pressed if (!but.equals(mPressed)) { if (mPressed!=null) { mPressed.button.setPressed(false); } but.button.setPressed(true); mPressed = but; showPreview(but); } } break; case MotionEvent.ACTION_UP : if (mPressed!=null) { buttonSelected(mPressed); } break; } return true; //super.onInterceptTouchEvent(ev); } private ButtonInfo findButton(int x, int y) { for (ButtonInfo but : buttonInfoList) { if (isInside(but, x, y)) { return but; } } return null; } private void buttonSelected(ButtonInfo selectedButton) { Log.i(TAG, "Selected:"+selectedButton.name); if (onButtonGridActionListener!=null) { onButtonGridActionListener.buttonPressed(selectedButton); } close(); } private boolean isInside(ButtonInfo but, float x, float y) { return (but.top<y && but.bottom>y && but.left<x && but.right>x); } private void showPreview(ButtonInfo buttonInfo) { try { if (!buttonInfo.equals(mCurrentPreview)) { Log.d(TAG, "Previewing "+buttonInfo.name); mCurrentPreview = buttonInfo; mPreviewText.setText(buttonInfo.name); int popupHeight = mPreviewHeight; mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), buttonInfo.button.getWidth() + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight()); ViewGroup.LayoutParams lp = mPreviewText.getLayoutParams(); if (lp != null) { lp.width = popupWidth; lp.height = popupHeight; } // where to place the popup int popupPreviewX; int popupPreviewY; if (buttonInfo.rowNo<2) { int horizontalOffset = (2*buttonInfo.button.getWidth()); // if in top 2 rows then show off to right/left to avoid popup going off the screen if (buttonInfo.colNo<mRowColLayout.cols/2.0) { // key is on left so show to right of key popupPreviewX = buttonInfo.left - mPreviewText.getPaddingLeft() + horizontalOffset; } else { // key is on right so show to right of key popupPreviewX = buttonInfo.left - mPreviewText.getPaddingLeft() - horizontalOffset; } popupPreviewY = buttonInfo.bottom; } else { // show above the key above the one currently pressed popupPreviewX = buttonInfo.left - mPreviewText.getPaddingLeft(); popupPreviewY = buttonInfo.top /*- popupHeight*/+ mPreviewOffset; } if (mPreviewPopup.isShowing()) { mPreviewPopup.update(popupPreviewX, popupPreviewY, popupWidth, popupHeight); } else { mPreviewPopup.setWidth(popupWidth); mPreviewPopup.setHeight(popupHeight); mPreviewPopup.showAtLocation(this, Gravity.NO_GRAVITY, popupPreviewX, popupPreviewY); } mPreviewText.setVisibility(VISIBLE); } else { // could be returning to this view via Back or Finish and the user represses same button if (mPreviewText.getVisibility()!=VISIBLE) { mPreviewText.setVisibility(VISIBLE); } } } catch (Exception e) { // avoid very occasional NPE deep in Android code by catching and ignoring because showing preview is optional Log.w(TAG, "Error showing button grid preview", e); } } @Override protected void onDetachedFromWindow() { close(); super.onDetachedFromWindow(); } private void close() { // avoid errors on Lenovo tablet in dismiss try { if (mPreviewPopup.isShowing()) { mPreviewPopup.dismiss(); } } catch (Exception e) { Log.w(TAG, "Error closing ButtonGrid preview"); } } /** calculate button position relative to this table because MotionEvents are relative to this table */ private void recordButtonPositions() { for (ButtonInfo buttonInfo : buttonInfoList) { // get position of button within row Button button = buttonInfo.button; TableRow tableRow = (TableRow)button.getParent(); buttonInfo.left += button.getLeft()+tableRow.getLeft(); buttonInfo.top += button.getTop()+tableRow.getTop(); buttonInfo.right += button.getRight()+tableRow.getLeft(); buttonInfo.bottom += button.getBottom()+tableRow.getTop(); } // calculate offset of 2 button heights so users can see the buttons surrounding the current button pressed if (buttonInfoList.size()>0) { ButtonInfo but1 = buttonInfoList.get(0); mPreviewOffset = but1.top - but1.bottom; } } /** * @param onButtonGridActionListener the onButtonGridActionListener to set */ public void setOnButtonGridActionListener( OnButtonGridActionListener onButtonGridActionListener) { this.onButtonGridActionListener = onButtonGridActionListener; } }