package org.odk.collect.android.views; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import org.javarosa.core.model.FormIndex; import org.javarosa.core.model.data.IAnswerData; import org.javarosa.form.api.FormEntryCaption; import org.javarosa.form.api.FormEntryPrompt; import org.odk.collect.android.R; import org.odk.collect.android.application.Collect; import org.odk.collect.android.listeners.WidgetChangedListener; import org.odk.collect.android.preferences.PreferencesActivity; import org.odk.collect.android.preferences.PreferencesActivity.ProgressBarMode; import org.odk.collect.android.widgets.IBinaryWidget; import org.odk.collect.android.widgets.QuestionWidget; import org.odk.collect.android.widgets.WidgetFactory; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Typeface; import android.preference.PreferenceManager; import android.util.Log; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.View.OnLongClickListener; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.ScrollView; import android.widget.TextView; /** * This class is * * @author carlhartung */ public class ODKView extends ScrollView implements OnLongClickListener, WidgetChangedListener { // starter random number for view IDs private final static int VIEW_ID = 12345; private final static String t = "CLASSNAME"; private final static int TEXTSIZE = 21; private Context mContext; private LinearLayout mView; private LinearLayout.LayoutParams mLayout; private ArrayList<QuestionWidget> widgets; private ArrayList<View> dividers; private ProgressBar mProgressBar; private int mQuestionFontsize; public final static String FIELD_LIST = "field-list"; private WidgetChangedListener wcListener; private boolean hasListener = false; private int widgetIdCount = 0; private int mViewBannerCount = 0; private boolean mProgressEnabled; public ODKView(Context context, FormEntryPrompt questionPrompt, FormEntryCaption[] groups, WidgetFactory factory) { this(context, new FormEntryPrompt[] { questionPrompt }, groups, factory); } public ODKView(Context context, FormEntryPrompt questionPrompt, FormEntryCaption[] groups, WidgetFactory factory, WidgetChangedListener wcl) { this(context, new FormEntryPrompt[] { questionPrompt }, groups, factory, wcl, false); } public ODKView(Context context, FormEntryPrompt[] questionPrompts, FormEntryCaption[] groups, WidgetFactory factory) { this(context, questionPrompts, groups, factory, null, false); } public ODKView(Context context, FormEntryPrompt[] questionPrompts, FormEntryCaption[] groups, WidgetFactory factory, WidgetChangedListener wcl, boolean isGroup) { super(context); if(wcl !=null){ hasListener = true; wcListener = wcl; } SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); String question_font = settings.getString(PreferencesActivity.KEY_FONT_SIZE, Collect.DEFAULT_FONTSIZE); mQuestionFontsize = new Integer(question_font).intValue(); mContext = context; widgets = new ArrayList<QuestionWidget>(); dividers = new ArrayList<View>(); mView = new LinearLayout(getContext()); mView.setOrientation(LinearLayout.VERTICAL); mView.setGravity(Gravity.TOP); mView.setPadding(0, 7, 0, 0); if(PreferencesActivity.getProgressBarMode(mContext) == ProgressBarMode.ProgressOnly) { this.mProgressEnabled = true; } // Construct progress bar if (mProgressEnabled) { mProgressBar = new ProgressBar(getContext(), null, android.R.attr.progressBarStyleHorizontal); mProgressBar.setProgressDrawable(getResources().getDrawable(R.drawable.progressbar)); LinearLayout.LayoutParams barLayout = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT); barLayout.setMargins(15, 15, 15, 15); barLayout.gravity = Gravity.BOTTOM; LinearLayout barView = new LinearLayout(getContext()); barView.setOrientation(LinearLayout.VERTICAL); barView.setGravity(Gravity.BOTTOM); barView.addView((View) mProgressBar); mView.addView(barView, barLayout); } mLayout = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); mLayout.setMargins(10, 0, 10, 0); //Figure out if we share hint text between questions String hintText = null; if(questionPrompts.length > 1) { hintText = questionPrompts[0].getHelpText(); for (FormEntryPrompt p : questionPrompts) { //If something doesn't have hint text at all, //bail String curHintText = p.getHelpText(); //Otherwise see if it matches if(curHintText == null || !curHintText.equals(hintText)) { //If not, we can't do this trick hintText = null; break; } } } // display which group you are in as well as the question addGroupText(groups); addHintText(hintText); boolean first = true; for (FormEntryPrompt p : questionPrompts) { if (!first) { View divider = new View(getContext()); divider.setBackgroundResource(android.R.drawable.divider_horizontal_bright); divider.setMinimumHeight(3); dividers.add(divider); mView.addView(divider); } else { first = false; } QuestionWidget qw; // if question or answer type is not supported, use text widget qw = factory.createWidgetFromPrompt(p, getContext()); qw.setLongClickable(true); qw.setOnLongClickListener(this); qw.setId(VIEW_ID + widgetIdCount++); //Suppress the hint text if we bubbled it if(hintText != null) { qw.hideHintText(); } widgets.add(qw); mView.addView((View) qw, mLayout); qw.setChangedListener(this); } addView(mView); } public void removeQuestionFromIndex(int i){ mView.removeView((View) widgets.get(i)); int dividerIndex = Math.max(i - 1, 0); mView.removeView(dividers.get(dividerIndex)); widgets.remove(i); dividers.remove(dividerIndex); } public void removeQuestionsFromIndex(ArrayList<Integer> indexes){ //Always gotta move backwards when removing, ensure that this list //goes backwards Collections.sort(indexes); Collections.reverse(indexes); for(int i=0; i< indexes.size(); i++){ removeQuestionFromIndex(indexes.get(i).intValue()); } } public void addQuestionToIndex(FormEntryPrompt fep, WidgetFactory factory, int i){ View divider = new View(getContext()); divider.setBackgroundResource(android.R.drawable.divider_horizontal_bright); divider.setMinimumHeight(3); int dividerIndex = mViewBannerCount; if(i > 0) { dividerIndex += 2 * i - 1; } mView.addView(divider, getViewIndex(dividerIndex)); dividers.add(Math.max(0, i - 1), divider); QuestionWidget qw = factory.createWidgetFromPrompt(fep, getContext());; qw.setLongClickable(true); qw.setOnLongClickListener(this); qw.setId(VIEW_ID + widgetIdCount++); //Suppress the hint text if we bubbled it // if(hintText != null) { //TODO figure this out // qw.hideHintText(); // } widgets.add(i, qw); mView.addView((View) qw, getViewIndex(2 * i + mViewBannerCount), mLayout); qw.setChangedListener(this); } /** * @return a HashMap of answers entered by the user for this set of widgets */ public HashMap<FormIndex, IAnswerData> getAnswers() { HashMap<FormIndex, IAnswerData> answers = new HashMap<FormIndex, IAnswerData>(); Iterator<QuestionWidget> i = widgets.iterator(); while (i.hasNext()) { /* * The FormEntryPrompt has the FormIndex, which is where the answer gets stored. The * QuestionWidget has the answer the user has entered. */ QuestionWidget q = i.next(); FormEntryPrompt p = q.getPrompt(); answers.put(p.getIndex(), q.getAnswer()); } return answers; } /* (non-Javadoc) * @see android.widget.LinearLayout#onMeasure(int, int) */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int newHeight = MeasureSpec.getSize(heightMeasureSpec); int newWidth = MeasureSpec.getSize(widthMeasureSpec); int oldHeight = this.getMeasuredHeight(); if(oldHeight == 0 || Math.abs(((newHeight * 1.0 - oldHeight) / oldHeight)) > .2) { for(QuestionWidget qw : this.widgets) { qw.updateFrameSize(newWidth, newHeight); } } super.onMeasure(widthMeasureSpec, heightMeasureSpec); // double change = ((this.getMeasuredHeight() * 1.0 - gmh) / this.getMeasuredHeight()); // System.out.println("Old: " + gmh + ". New: "+ this.getMeasuredHeight() + ". CurrentHeight: " + height); // // if(gmh == -1 || gmh == 0 || change > .2) { // //If the view size change has changed by more than 20% // if(mHelpText != null) { // mHelpText.updateMaxHeight((this.getMeasuredHeight() - this.getScrollY())/ 3); // } // } } private void updateConstraintRelevancies(){ if(hasListener){ wcListener.widgetEntryChanged(); } } /** * Update progress bar * @param progress Current value * @param max Progress bar will be given range 0..max */ public void updateProgressBar(int progress, int max) { if (mProgressBar != null) { mProgressBar.setMax(max); mProgressBar.setProgress(progress); } } /** * // * Add a TextView containing the hierarchy of groups to which the question belongs. // */ private void addGroupText(FormEntryCaption[] groups) { StringBuffer s = new StringBuffer(""); String t = ""; int i; // list all groups in one string for (FormEntryCaption g : groups) { i = g.getMultiplicity() + 1; t = g.getLongText(); if (t != null) { s.append(t); if (g.repeats() && i > 0) { s.append(" (" + i + ")"); } s.append(" > "); } } // build view if (s.length() > 0) { TextView tv = new TextView(getContext()); tv.setText(s.substring(0, s.length() - 3)); tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, TEXTSIZE - 4); tv.setPadding(0, 0, 0, 5); mView.addView(tv, mLayout); mViewBannerCount ++; } } private void addHintText(String hintText) { if (hintText != null && !hintText.equals("")) { TextView mHelpText = new TextView(getContext()); mHelpText = new TextView(getContext()); mHelpText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mQuestionFontsize - 3); mHelpText.setPadding(0, -5, 0, 7); // wrap to the widget of view mHelpText.setHorizontallyScrolling(false); mHelpText.setText(hintText); mHelpText.setTypeface(null, Typeface.ITALIC); mViewBannerCount++; mView.addView(mHelpText, mLayout); } } public void setFocus(Context context) { if (widgets.size() > 0) { widgets.get(0).setFocus(context); } } /** * Called when another activity returns information to answer this question. * * @param answer */ public void setBinaryData(Object answer) { boolean set = false; for (QuestionWidget q : widgets) { if (q instanceof IBinaryWidget) { if (((IBinaryWidget) q).isWaitingForBinaryData()) { ((IBinaryWidget) q).setBinaryData(answer); set = true; break; } } } if (!set) { Log.w(t, "Attempting to return data to a widget or set of widgets not looking for data"); for (QuestionWidget q : widgets) { if (q instanceof IBinaryWidget) { ((IBinaryWidget) q).setBinaryData(answer); set = true; break; } } } } /** * @return true if the answer was cleared, false otherwise. */ public boolean clearAnswer() { // If there's only one widget, clear the answer. // If there are more, then force a long-press to clear the answer. if (widgets.size() == 1 && !widgets.get(0).getPrompt().isReadOnly()) { widgets.get(0).clearAnswer(); return true; } else { return false; } } public ArrayList<QuestionWidget> getWidgets() { return widgets; } /* * (non-Javadoc) * @see android.view.View#setOnFocusChangeListener(android.view.View.OnFocusChangeListener) */ @Override public void setOnFocusChangeListener(OnFocusChangeListener l) { for (int i = 0; i < widgets.size(); i++) { QuestionWidget qw = widgets.get(i); qw.setOnFocusChangeListener(l); } } /* * (non-Javadoc) * @see android.view.View.OnLongClickListener#onLongClick(android.view.View) */ @Override public boolean onLongClick(View v) { return false; } /* * (non-Javadoc) * @see android.view.View#cancelLongPress() */ @Override public void cancelLongPress() { super.cancelLongPress(); for (QuestionWidget qw : widgets) { qw.cancelLongPress(); } } /* * (non-Javadoc) * @see org.odk.collect.android.listeners.WidgetChangedListener#widgetEntryChanged() */ @Override public void widgetEntryChanged() { updateConstraintRelevancies(); } /** * Remove question, based on position. * @param questionIndex Index in question list. */ public void removeWidget(int questionIndex){ mView.removeViewAt(getViewIndex(questionIndex)); } /** * Remove question, based on view object. * @param v View to remove */ public void removeWidget(View v){ mView.removeView(v); } /** * Translate question index to view index. * @param questionIndex Index in the list of questions. * @return Index of question's view in mView. */ private int getViewIndex(int questionIndex) { // Account for progress bar if (mProgressEnabled) { return questionIndex + 1; } return questionIndex; } }