/******************************************************************************* * Gaggle is Copyright 2010 by Geeksville Industries LLC, a California limited liability corporation. * * Gaggle is distributed under a dual license. We've chosen this approach because within Gaggle we've used a number * of components that Geeksville Industries LLC might reuse for commercial products. Gaggle can be distributed under * either of the two licenses listed below. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Commercial Distribution License * If you would like to distribute Gaggle (or portions thereof) under a license other than * the "GNU General Public License, version 2", contact Geeksville Industries. Geeksville Industries reserves * the right to release Gaggle source code under a commercial license of its choice. * * GNU Public License, version 2 * All other distribution of Gaggle must conform to the terms of the GNU Public License, version 2. The full * text of this license is included in the Gaggle source, see assets/manual/gpl-2.0.txt. ******************************************************************************/ package com.geeksville.info; import java.lang.reflect.Constructor; import java.util.HashMap; import java.util.Map; import android.app.Activity; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.Handler; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import com.geeksville.android.LifeCycleHandler; import com.geeksville.android.LifeCyclePublisher; import com.geeksville.gaggle.R; /** * A view containing a single InfoField * * @author kevinh * * We support different layouts by passing in a id_layout attribute. We * look for the following subviews: infodock_label: a full label * infodock_shortlabel: a short label (suitable for map) infodock_units: * units display */ public class InfoDock extends LinearLayout implements InfoField.OnChangedListener, LifeCycleHandler { static final String TAG = "InfoDock"; /** * The layout we are using for this doc */ int layoutId; InfoField contents = null; TextView shortLabel; TextView label, addendum, text, units; ImageView image; // Need handler for callbacks to the UI thread private final Handler handler = new Handler(); /** * Used to check for 'real' changes of text values */ private String oldText; private String oldAddendum; /** * Have we already inflated this component? */ private boolean isInflated = false; private int defaultTextColor; /** * Constructor * * @param context * @param layoutId * An ID such as R.layout.info_dock_wide * @param fieldName */ public InfoDock(Context context, int layoutId, String fieldName) { super(context); listenToLifecycle(); this.layoutId = layoutId; setInfoField(fieldName); } public InfoDock(Context context, AttributeSet attrs) { super(context, attrs); listenToLifecycle(); TypedArray arr = context .obtainStyledAttributes(attrs, R.styleable.InfoDock); // The user probably wants to specify a field name String fieldName = arr.getString(R.styleable.InfoDock_info_field); // default to a wide layout unless the user asked for something else layoutId = arr.getResourceId(R.styleable.InfoDock_layout_id, R.layout.info_dock_wide); arr.recycle(); if (fieldName != null) setInfoField(fieldName); } /** * When our app is paused, we want to stop updating our widgets */ void listenToLifecycle() { if (getContext() instanceof LifeCyclePublisher) ((LifeCyclePublisher) getContext()).addLifeCycleHandler(this); } /** * Add the children from our layout.xml - FIXME, is there a better way to do * this? */ @Override protected void onFinishInflate() { super.onFinishInflate(); Context context = getContext(); ((LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)) .inflate(layoutId, this); label = (TextView) findViewById(R.id.infodock_label); shortLabel = (TextView) findViewById(R.id.infodock_shortlabel); addendum = (TextView) findViewById(R.id.infodock_addendum); text = (TextView) findViewById(R.id.infodock_text); units = (TextView) findViewById(R.id.infodock_units); image = (ImageView) findViewById(R.id.infodock_image); defaultTextColor = text.getTextColors().getDefaultColor(); // If we already have contents, fill fields and invalidate as needed setContents(contents); isInflated = true; setSaveEnabled(true); } /* * (non-Javadoc) * * @see com.geeksville.android.LifeCycleHandler#onPause() */ @Override public void onPause() { if (contents != null) contents.onHidden(); } /* * (non-Javadoc) * * @see com.geeksville.android.LifeCycleHandler#onResume() */ @Override public void onResume() { if (contents != null) { contents.onShown(); updateLabels(); } } /* * (non-Javadoc) * * @see com.geeksville.android.LifeCycleHandler#onStart() */ @Override public void onStart() { // nothing } /* * (non-Javadoc) * * @see com.geeksville.android.LifeCycleHandler#onStop() */ @Override public void onStop() { // nothing } // We keep a cache of all infofields, so we can keep reusing them static Map<String, InfoField> infoFields = new HashMap<String, InfoField>(); @SuppressWarnings("unchecked") public void setInfoField(String fieldName) { InfoField f; try { if (!infoFields.containsKey(fieldName)) { Class c = Class.forName(fieldName); Constructor<InfoField> cons = c.getConstructor((Class[]) null); f = cons.newInstance(); // if onCreate fails, we still leave our dock around but // disabled try { // Pass in null if we are running in eclipse (by noticing // the // context is not Activity) Activity activity = (getContext() == null) ? null : Activity.class .isInstance(getContext()) ? (Activity) getContext() : null; f.onCreate(activity); } catch (Throwable ex) { // We catch Throwable instead of Exception because VerifyErrors can // occur on android 1.5 // If we failed to create the info field the user probably // doesn't have the hardware on their phone Log.e(TAG, "Can't create info dock for " + fieldName + " " + ex.getMessage()); setEnabled(false); } infoFields.put(fieldName, f); } else f = infoFields.get(fieldName); // If we are already inflated, we'll need to swap info fields now if (isInflated) setContents(f); else contents = f; } catch (Exception ex) { throw new RuntimeException("Can't create InfoField", ex); // Should // not // happen // post // development } } /** * Update our units and short label */ private void updateLabels() { if (label != null) label.setText(contents.getLabel()); if (shortLabel != null) shortLabel.setText(contents.getShortLabel()); if(addendum != null) addendum.setText(contents.getAddendum()); // We might not be displaying the units field if (units != null) units.setText(contents.getUnits()); } /** * Install a new info field into this dock * * @param f */ private void setContents(InfoField f) { // Tell the old field that it wasn't being shown no more // onVisibilityChanged(false); // We no longer care about our old contents if (contents != null) { contents.setOnChanged(null); contents.onHidden(); } contents = f; if (contents != null) { contents.onShown(); updateLabels(); // Subscribe to this info field contents.setOnChanged(this); // Draw the adjustable contents of the control now, so as to prevent // flicker drawInfoContents(); } } @Override public void onInfoChanged(InfoField source) { // Only update the GUI if we must String newText = contents.getText(); String newAddendum = contents.getAddendum(); // If the user is using an image, we are not yet smart enough to optize // that case - just let the call happen if (!newText.equals(oldText) || image != null || !newAddendum.equals(oldAddendum)) { oldText = newText; oldAddendum = newAddendum; handler.post(infoChangedGuiWork); } } private void drawInfoContents() { int color = contents.getTextColor(); text.setTextColor(color != -1 ? color : defaultTextColor); text.setText(oldText); //Addendum might change as well if(addendum != null) addendum.setText(contents.getAddendum()); if (image != null) { Drawable img = contents.getImage(); if (img != null) { image.setImageDrawable(img); image.setVisibility(VISIBLE); image.invalidate(); } else { // If this info field doesn't have a drawable now, no point // in drawing the image view image.setVisibility(INVISIBLE); } } // Redraw our drawable too // if (image != null && image.getVisibility() == VISIBLE) // image.invalidate(); } /** * Handles updates from the infofield, but guaranteed to run in the gui thread */ final Runnable infoChangedGuiWork = new Runnable() { public void run() { drawInfoContents(); } }; }