/** * */ package org.commcare.android.view; import org.commcare.android.models.Entity; import org.commcare.android.util.CachingAsyncImageLoader; import org.commcare.android.util.MarkupUtil; import org.commcare.dalvik.R; import org.commcare.suite.model.Detail; import org.commcare.util.GridCoordinate; import org.commcare.util.GridStyle; import org.javarosa.core.services.Logger; import org.javarosa.xpath.XPathUnhandledException; import org.odk.collect.android.views.media.AudioButton; import org.odk.collect.android.views.media.AudioController; import org.odk.collect.android.views.media.ViewId; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Point; import android.text.Spannable; import android.util.DisplayMetrics; import android.view.Gravity; import android.view.View; import android.widget.GridLayout; import android.widget.ImageView; import android.widget.Space; import android.widget.TextView; /** * @author wspride * This class defines an individual panel within an advanced case list. * Each panel is defined by a Detail and an Entity * Significant axis of configuration are NUMBER_ROWS, NUMBER_COLUMNS, AND CELL_HEIGHT_DIVISOR defined below * */ @SuppressLint("NewApi") public class GridEntityView extends GridLayout { private String[] forms; private String[] searchTerms; private GridCoordinate[] coords; private GridStyle[] styles; Object[] mRowData; public final float SMALL_FONT = getResources().getDimension(R.dimen.font_size_small); // load the screen-size dependent font sizes public final float MEDIUM_FONT = getResources().getDimension(R.dimen.font_size_medium); public final float LARGE_FONT = getResources().getDimension(R.dimen.font_size_large); public final float XLARGE_FONT = getResources().getDimension(R.dimen.font_size_xlarge); public final float DENSITY = getResources().getDisplayMetrics().density; public final int CELL_PADDING_HORIZONTAL = (int)getResources().getDimension(R.dimen.cell_padding_horizontal); public final int CELL_PADDING_VERTICAL = (int)getResources().getDimension(R.dimen.cell_padding_vertical); public final int ROW_PADDING_HORIZONTAL = (int)getResources().getDimension(R.dimen.row_padding_horizontal); public final int ROW_PADDING_VERTICAL = (int)getResources().getDimension(R.dimen.row_padding_vertical); public final int DEFAULT_NUMBER_ROWS_PER_GRID = 6; public final double LANDSCAPE_TO_PORTRAIT_RATIO = .75; public int NUMBER_ROWS_PER_GRID = 6; // number of rows per GridView public int NUMBER_COLUMNS_PER_GRID = 12; // number of columns per GridView public double NUMBER_ROWS_PER_SCREEN_TALL = 5; // number of rows the screen is divided into in portrait mode public double NUMBER_CROWS_PER_SCREEN_WIDE = 3; // number of rows the screen is divided into in landscape mode public double densityRowMultiplier = 1; public String backgroundColor; public double cellWidth; public double cellHeight; public double screenWidth; public double screenHeight; public double rowHeight; public double rowWidth; private CachingAsyncImageLoader mImageLoader; // image loader used for all asyncronous imageView loading private AudioController controller; public GridEntityView(Context context, Detail detail, Entity entity, String[] searchTerms, AudioController controller) { this(context, detail, entity, searchTerms, null, controller); } public GridEntityView(Context context, Detail detail, Entity entity, String[] searchTerms, CachingAsyncImageLoader mLoader, AudioController controller) { super(context); this.searchTerms = searchTerms; this.controller = controller; int maximumRows = this.getMaxRows(detail); this.NUMBER_ROWS_PER_GRID = maximumRows; // calibrate the size of each gridview relative to the screen size based on how many rows will be in each grid // this.NUMBER_ROWS_PER_SCREEN_TALL = this.NUMBER_ROWS_PER_SCREEN_TALL * (this.NUMBER_ROWS_PER_GRID/DEFAULT_NUMBER_ROWS_PER_GRID); this.NUMBER_CROWS_PER_SCREEN_WIDE = this.NUMBER_ROWS_PER_SCREEN_TALL * LANDSCAPE_TO_PORTRAIT_RATIO; this.setColumnCount(NUMBER_COLUMNS_PER_GRID); this.setRowCount(NUMBER_ROWS_PER_GRID); this.setPadding(ROW_PADDING_HORIZONTAL,ROW_PADDING_VERTICAL,ROW_PADDING_HORIZONTAL,ROW_PADDING_VERTICAL); // get density metrics DisplayMetrics metrics = getResources().getDisplayMetrics(); int densityDpi = metrics.densityDpi; if(densityDpi == DisplayMetrics.DENSITY_XHIGH){ densityRowMultiplier = 2.0; } else if(densityDpi == DisplayMetrics.DENSITY_HIGH){ densityRowMultiplier = 1.5; } else if(densityDpi == DisplayMetrics.DENSITY_MEDIUM){ } //setup all the various dimensions we need Point size = new Point(); ((Activity)context).getWindowManager().getDefaultDisplay().getSize(size); screenWidth = size.x; screenHeight = size.y; // If screen is rotated, use width for cell height measurement if(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE){ //TODO: call to inAwesomeMode was not working for me. What's the best method to determine this? if(context.getString(R.string.panes).equals("two")){ // if in awesome mode, split available width in half screenWidth = screenWidth/2; } // calibrate row width and height based on screen density and divisor constant rowHeight = screenHeight/(NUMBER_CROWS_PER_SCREEN_WIDE*densityRowMultiplier); rowWidth = screenWidth; } else{ rowHeight = screenHeight/(NUMBER_ROWS_PER_SCREEN_TALL*densityRowMultiplier); rowWidth = screenWidth; } mImageLoader = mLoader; cellWidth = rowWidth/NUMBER_COLUMNS_PER_GRID; cellHeight = rowHeight / NUMBER_ROWS_PER_GRID; // now ready to setup all these views setViews(context, detail, entity); } /** * Add Spaces to this GridLayout to strictly enforce that Grid columns and rows stay the width/height we want * Android GridLayout tries to be very "smart" about moving entries placed arbitrarily within the grid so that * they look "ordered" even though this often ends up looking not how we want it. It will only make these adjustments * to rows/columns that end up empty or near empty, so we solve this by adding spaces to every row and column. * We just add a space of width cellWidth and height 1 to every column of the first row, and likewise a sapce of height * cellHeight and width 1 to every row of the first column. These are then written on top of if need be. * @param context */ public void addBuffers(Context context){ for(int i=0; i<NUMBER_ROWS_PER_GRID;i++){ Spec rowSpec = GridLayout.spec(i); Spec colSpec = GridLayout.spec(0); GridLayout.LayoutParams mGridParams = new GridLayout.LayoutParams(rowSpec, colSpec); mGridParams.width = 1; mGridParams.height = (int)cellHeight; Space mSpace = new Space(context); mSpace.setLayoutParams(mGridParams); this.addView(mSpace, mGridParams); } for(int i=0; i<NUMBER_COLUMNS_PER_GRID;i++){ Spec rowSpec = GridLayout.spec(0); Spec colSpec = GridLayout.spec(i); GridLayout.LayoutParams mGridParams = new GridLayout.LayoutParams(rowSpec, colSpec); mGridParams.width = (int)cellWidth; mGridParams.height = 1; Space mSpace = new Space(context); mSpace.setLayoutParams(mGridParams); this.addView(mSpace, mGridParams); } } // get the maximum height of this grid public int getMaxRows(Detail detail){ GridCoordinate[] coordinates = detail.getGridCoordinates(); int currentMaxHeight = 0; for(int i=0; i< coordinates.length; i++){ int yCoordinate = coordinates[i].getY(); int height = coordinates[i].getHeight(); int maxHeight = yCoordinate + height; if(maxHeight > currentMaxHeight){ currentMaxHeight = maxHeight; } } return currentMaxHeight; } /** * Set all the views to be displayed in this pane * @param context * @param detail - the Detail describing how to display each entry * @param entity - the Entity describing the actual data of each entry */ public void setViews(Context context, Detail detail, Entity entity){ // clear all previous entries in this grid this.removeAllViews(); // add spacers to enforce regularized column and row size addBuffers(context); // extract UI information from detail and entity forms = detail.getTemplateForms(); coords = detail.getGridCoordinates(); styles = detail.getGridStyles(); mRowData = entity.getData(); String[] bgData = entity.getBackgroundData(); this.setBackgroundDrawable(null); // see if any entities have background data set for(int i=0; i<bgData.length; i++){ if(!bgData[i].equals("")){ if(bgData[i].equals(("red-border"))){ this.setBackgroundDrawable(getResources().getDrawable(R.drawable.border_red)); } else if(bgData[i].equals(("yellow-border"))){ this.setBackgroundDrawable(getResources().getDrawable(R.drawable.border_yellow)); } else if(bgData[i].equals(("red-background"))){ this.setBackgroundDrawable(getResources().getDrawable(R.drawable.background_red)); } else if(bgData[i].equals(("yellow-background"))){ this.setBackgroundDrawable(getResources().getDrawable(R.drawable.background_yellow)); } } } this.setPadding(ROW_PADDING_HORIZONTAL,ROW_PADDING_VERTICAL,ROW_PADDING_HORIZONTAL,ROW_PADDING_VERTICAL); // iterate through every entity to be inserted in this view for(int i=0; i<mRowData.length; i++){ String multimediaType = forms[i]; GridStyle mStyle = styles[i]; GridCoordinate currentCoordinate = coords[i]; // if X and Y coordinates haven't been set, skip this row // if span exceeds allotted dimensions, skip this row and log if(currentCoordinate.getX()<0 || currentCoordinate.getY() <0){ if(currentCoordinate.getX() + currentCoordinate.getWidth() > NUMBER_COLUMNS_PER_GRID || currentCoordinate.getY() + currentCoordinate.getHeight() > NUMBER_ROWS_PER_GRID){ Logger.log("e", "Grid entry dimensions exceed allotted sizes"); throw new XPathUnhandledException("grid coordinates: " + currentCoordinate.getX() + currentCoordinate.getWidth() + ", " + currentCoordinate.getY() + currentCoordinate.getHeight() + " out of bounds"); } continue; } View mView; // these Specs set our span across rows and columns; first arg is root, second is span Spec columnSpec = GridLayout.spec(currentCoordinate.getX(), currentCoordinate.getWidth()); Spec rowSpec = GridLayout.spec(currentCoordinate.getY(), currentCoordinate.getHeight()); ViewId uniqueId = new ViewId(currentCoordinate.getX(), currentCoordinate.getY(), false); // setup our layout parameters GridLayout.LayoutParams mGridParams = new GridLayout.LayoutParams(rowSpec, columnSpec); mGridParams.width = (int)cellWidth * currentCoordinate.getWidth(); mGridParams.height = (int)cellHeight * currentCoordinate.getHeight(); // get style attributes String horzAlign = mStyle.getHorzAlign(); String vertAlign = mStyle.getVertAlign(); String textsize = mStyle.getFontSize(); String CssID = mStyle.getCssID(); mView = getView(context, multimediaType, mGridParams, horzAlign, vertAlign, textsize, entity.getFieldString(i), uniqueId, CssID); mView.setLayoutParams(mGridParams); this.addView(mView, mGridParams); } } /** * Get the correct View for this particular activity. * @param context * @param multimediaType either "image", "audio", or default text. Describes how this XPath result should be displayed. * @param width the width, in number of cells, this entity should occupy in the grid * @param height the height, in number of cells, this entity should occupy in the grid * @param horzAlign How the text should be aligned horizontally - left, center, or right ONE OF horzAlign or vertAlign * @param vertAlign How the text should be aligned vertically - top, center, or bottom ONE OF horzAlign or vertAlign * @param textsize The font size, scaled for screen size. small, medium, large, xlarge accepted. * @param rowData The actual data to display, either an XPath to media or a String to display * @return */ private View getView(Context context, String multimediaType, GridLayout.LayoutParams mGridParams, String horzAlign, String vertAlign, String textsize, String rowData, ViewId uniqueId, String cssid) { View retVal; if(multimediaType.equals(EntityView.FORM_IMAGE)){ retVal = new ImageView(context); retVal.setPadding(CELL_PADDING_HORIZONTAL,CELL_PADDING_VERTICAL,CELL_PADDING_HORIZONTAL,CELL_PADDING_VERTICAL); // image loading is handled asyncronously by the TCImageLoader class to allow smooth scrolling if(rowData != null && !rowData.equals("")){ if(mImageLoader != null){ mImageLoader.display(rowData, ((ImageView)retVal), R.drawable.info_bubble); } else{ Bitmap b = ViewUtil.inflateDisplayImage(getContext(), rowData); ((ImageView)retVal).setImageBitmap(b); } } } else if(multimediaType.equals(EntityView.FORM_AUDIO)){ if (rowData != null & rowData.length() > 0) { retVal = new AudioButton(context, rowData, uniqueId, controller, true); } else { retVal = new AudioButton(context, rowData, uniqueId, controller, false); } } else{ retVal = new TextView(context); if(cssid !=null && !cssid.equals("none")){ // user defined a style we want to use Spannable mSpannable = MarkupUtil.getCustomSpannable(cssid, rowData); ((TextView)retVal).setText(mSpannable); } else{ // just process inline markup Spannable mSpannable = MarkupUtil.getSpannable(rowData); ((TextView)retVal).setText(mSpannable); } // handle horizontal alignments if(horzAlign.equals("center")){ ((TextView)retVal).setGravity(Gravity.CENTER_HORIZONTAL); } else if(horzAlign.equals("left")) { ((TextView)retVal).setGravity(Gravity.TOP); } else if(horzAlign.equals("right")) { ((TextView)retVal).setGravity(Gravity.RIGHT); } // handle vertical alignment if(vertAlign.equals("center")){ ((TextView)retVal).setGravity(Gravity.CENTER_VERTICAL); } else if(vertAlign.equals("top")) { ((TextView)retVal).setGravity(Gravity.TOP); } else if(vertAlign.equals("bottom")) { ((TextView)retVal).setGravity(Gravity.BOTTOM); } // handle text resizing if(textsize.equals("large")){ ((TextView)retVal).setTextSize(LARGE_FONT/DENSITY); } else if(textsize.equals("small")){ ((TextView)retVal).setTextSize(SMALL_FONT/DENSITY); } else if(textsize.equals("medium")){ ((TextView)retVal).setTextSize(MEDIUM_FONT/DENSITY); } else if(textsize.equals("xlarge")){ ((TextView)retVal).setTextSize(XLARGE_FONT/DENSITY); } } return retVal; } }