/* * InfoBox.java * Copyright (C) 2015 Nicholas Killewald * * This file is distributed under the terms of the BSD license. * The source package should have a LICENSE file at the toplevel. */ package net.exclaimindustries.geohashdroid.widgets; import android.app.Activity; import android.content.Context; import android.graphics.Rect; import android.location.Location; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.util.AttributeSet; import android.view.View; import android.view.ViewTreeObserver; import android.widget.LinearLayout; import android.widget.TextView; import com.google.android.gms.location.LocationListener; import net.exclaimindustries.geohashdroid.R; import net.exclaimindustries.geohashdroid.util.GHDConstants; import net.exclaimindustries.geohashdroid.util.Info; import net.exclaimindustries.geohashdroid.util.UnitConverter; import java.text.DecimalFormat; /** * This is the info box. It sits neatly on top of the map screen. Given an * Info and a stream of updates, it'll report on where the user is and how far * from the target they are. */ public class InfoBox extends LinearLayout implements LocationListener { private Info mInfo; private TextView mDest; private TextView mYou; private TextView mDistance; private TextView mAccuracyLow; private TextView mAccuracyReallyLow; private Location mLastLocation; private static final DecimalFormat DIST_FORMAT = new DecimalFormat("###.###"); private boolean mAlreadyLaidOut = false; private boolean mWaitingToShow = false; private boolean mUnavailable = false; /** If the InfoBox should be faded out. */ private boolean mFaded = false; /** If the InfoBox should be visible and not off-screen. */ private boolean mVisible = false; // The last-seen states for each type (so we don't overwrite any animation // in progress). private boolean mLastFaded = false; private boolean mLastVisible = false; public InfoBox(Context c) { this(c, null); } public InfoBox(Context c, AttributeSet attrs) { super(c, attrs); // How about some setup? setBackgroundColor(ContextCompat.getColor(c, R.color.infobox_background)); int padding = getResources().getDimensionPixelSize(R.dimen.infobox_padding); setPadding(padding, padding, padding, padding); setOrientation(LinearLayout.VERTICAL); // I stand by my decision to elevate! Even though I'm pretty sure it // doesn't do anything with a translucent background. if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) setElevation(getResources().getDimension(R.dimen.elevation_infobox)); // INFLATE! inflate(c, R.layout.infobox, this); mDest = (TextView)findViewById(R.id.infobox_hashpoint); mYou = (TextView)findViewById(R.id.infobox_you); mDistance = (TextView)findViewById(R.id.infobox_distance); mAccuracyLow = (TextView)findViewById(R.id.infobox_accuracy_low); mAccuracyReallyLow = (TextView)findViewById(R.id.infobox_accuracy_really_low); // Make it not visible immediately. We'll re-enable it if need be once // we get the global layout listener called. setAlpha(0.0f); // As usual, make sure the view's just gone until we need it. // ExpeditionMode will pull it back in. getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { // Got a height! Hopefully. if(!mAlreadyLaidOut) { mAlreadyLaidOut = true; // Make it off-screen first, then animate it on if need be. setInfoBoxVisible(false); animateInfoBoxVisible(mWaitingToShow); } } }); } /** * Sets the Info. If null, this will make it go to standby. * * @param info the new Info */ public void setInfo(@Nullable final Info info) { // New info! mInfo = info; updateBox(); } private void updateBox() { ((Activity)getContext()).runOnUiThread(new Runnable() { @Override public void run() { float accuracy = 5.0f; if(mLastLocation != null) accuracy = mLastLocation.getAccuracy(); // Make sure we're dealing with sane data if we got this from an // emulator or mock location data... if(accuracy == 0.0f) accuracy = 5.0f; // Redraw the Info. Always do this. The user might be coming // back from Preferences, for instance. if(mInfo == null) { mDest.setText(R.string.unknown_title); } else { mDest.setText(UnitConverter.makeFullCoordinateString(getContext(), mInfo.getFinalLocation(), false, UnitConverter.OUTPUT_SHORT)); } // Reset the accuracy warnings. The right one will go back up // as need be. mAccuracyLow.setVisibility(View.GONE); mAccuracyReallyLow.setVisibility(View.GONE); // If we've got a location yet, use that. If not, to standby // with you! if(mUnavailable) { mYou.setVisibility(View.GONE); } else { mYou.setVisibility(View.VISIBLE); } if(mLastLocation == null) { mYou.setText(R.string.unknown_title); } else { mYou.setText(UnitConverter.makeFullCoordinateString(getContext(), mLastLocation, false, UnitConverter.OUTPUT_SHORT)); // Hey, as long as we're here, let's also do accuracy. if(accuracy >= GHDConstants.REALLY_LOW_ACCURACY_THRESHOLD) mAccuracyReallyLow.setVisibility(View.VISIBLE); else if(accuracy >= GHDConstants.LOW_ACCURACY_THRESHOLD) mAccuracyLow.setVisibility(View.VISIBLE); } // Next, calculate the distance, if possible. if(mUnavailable) { mDistance.setVisibility(View.GONE); } else { mDistance.setVisibility(View.VISIBLE); } if(mLastLocation == null || mInfo == null) { mDistance.setText(R.string.unknown_title); mDistance.setTextColor(ContextCompat.getColor(getContext(), R.color.infobox_text)); } else { float distance = mLastLocation.distanceTo(mInfo.getFinalLocation()); mDistance.setText(UnitConverter.makeDistanceString(getContext(), DIST_FORMAT, distance)); // Plus, if we're close enough AND accurate enough, make the // text be green. We COULD do this with geofencing // callbacks and all, but, I mean, we're already HERE, // aren't we? if(accuracy < GHDConstants.LOW_ACCURACY_THRESHOLD && distance <= accuracy) mDistance.setTextColor(ContextCompat.getColor(getContext(), R.color.infobox_in_range)); else mDistance.setTextColor(ContextCompat.getColor(getContext(), R.color.infobox_text)); } } }); } /** * Slides the InfoBox in to or out of view. * * @param visible true to slide in, false to slide out */ public void animateInfoBoxVisible(boolean visible) { if(!mAlreadyLaidOut) { mWaitingToShow = visible; } else { mVisible = visible; resolveInfoBoxState(null); } } /** * Slides the InfoBox out of view (ONLY out of view), with an ending action. * * @param endAction action to perform after animation completes */ public void animateInfoBoxOutWithEndAction(@Nullable Runnable endAction) { mVisible = false; resolveInfoBoxState(endAction); } /** * Makes the InfoBox be in or out of view without animating it. * @param visible true to appear, false to vanish */ public void setInfoBoxVisible(boolean visible) { mVisible = visible; forceInfoBoxState(); } /** * Fades out the InfoBox and shuts off the click handler. This is used to * keep the final destination flag visible if it's underneath the box. * * @param faded true to be faded, false to be normal */ public void fadeOutInfoBox(boolean faded) { mFaded = faded; resolveInfoBoxState(null); } /** * Forces the InfoBox to the faded state, shutting off the click handler in * the process. * * @param faded true to be faded, false to be normal */ public void setInfoBoxFaded(boolean faded) { mFaded = faded; forceInfoBoxState(); } private void resolveInfoBoxState(Runnable endAction) { if(mVisible == mLastVisible && mFaded == mLastFaded) return; // Quick note: The size of the InfoBox might change due to the width // of the text shown (as well as the accuracy warning), but since we // alpha it out anyway, that shouldn't be a real major issue. if(!mVisible) animate().translationX(getWidth()).alpha(0.0f).withEndAction(endAction); else if(mFaded) animate().translationX(0.0f).alpha(0.2f).withEndAction(endAction); else animate().translationX(0.0f).alpha(1.0f).withEndAction(endAction); // If the box is faded OR hidden, we can't click it. setClickable(mVisible && !mFaded); mLastVisible = mVisible; mLastFaded = mFaded; } private void forceInfoBoxState() { // Same as in resolve, but without animation. if(!mVisible) { setTranslationX(getWidth()); setAlpha(0.0f); } else if(mFaded) { setTranslationX(0.0f); setAlpha(0.2f); } else { setTranslationX(0.0f); setAlpha(1.0f); } setClickable(mVisible && !mFaded); // Note that we don't bother checking if this has changed, since we // should be overwriting any animation at this point anyway. mLastVisible = mVisible; mLastFaded = mFaded; } /** * Sets whether or not the location data is unavailable. It's unavailable * if the user didn't give us permission to get it. If so, the current * location and distance fields will just say they're unavailable. This is * different from if we don't have the appropriate data YET, but CAN get it * if it shows up. The widget defaults to available (false). * * @param flag true for unavailable, false for available */ public void setUnavailable(boolean flag) { mUnavailable = flag; updateBox(); } @Override public void onLocationChanged(Location location) { // Hey, look, a location! mLastLocation = location; updateBox(); } /** * Gets the current location and size of the InfoBox, as a Rect (pass one in * and it'll be updated), relative to the parent view. * * @param output the Rect in which data will go */ public void getLocationRect(@NonNull Rect output) { output.set(getLeft(), getTop(), getRight(), getBottom()); } }