Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 package org.mozilla.gecko.home;
8 import org.json.JSONException;
9 import org.json.JSONObject;
10 import org.mozilla.gecko.GeckoAppShell;
11 import org.mozilla.gecko.GeckoEvent;
12 import org.mozilla.gecko.R;
13 import org.mozilla.gecko.animation.PropertyAnimator;
14 import org.mozilla.gecko.animation.PropertyAnimator.Property;
15 import org.mozilla.gecko.animation.ViewHelper;
16 import org.mozilla.gecko.gfx.BitmapUtils;
17 import org.mozilla.gecko.util.GeckoEventListener;
18 import org.mozilla.gecko.util.ThreadUtils;
19 import org.mozilla.gecko.widget.EllipsisTextView;
21 import android.content.Context;
22 import android.graphics.drawable.Drawable;
23 import android.os.Build;
24 import android.text.Html;
25 import android.text.Spanned;
26 import android.text.TextUtils;
27 import android.util.AttributeSet;
28 import android.util.Log;
29 import android.view.LayoutInflater;
30 import android.view.MotionEvent;
31 import android.view.View;
32 import android.widget.ImageButton;
33 import android.widget.ImageView;
34 import android.widget.LinearLayout;
35 import android.widget.TextView;
37 public class HomeBanner extends LinearLayout
38 implements GeckoEventListener {
39 private static final String LOGTAG = "GeckoHomeBanner";
41 // Used for tracking scroll length
42 private float mTouchY = -1;
44 // Used to detect for upwards scroll to push banner all the way up
45 private boolean mSnapBannerToTop;
47 // Tracks whether or not the banner should be shown on the current panel.
48 private boolean mActive = false;
50 // The user is currently swiping between HomePager pages
51 private boolean mScrollingPages = false;
53 // Tracks whether the user swiped the banner down, preventing us from autoshowing when the user
54 // switches back to the default page.
55 private boolean mUserSwipedDown = false;
57 // We must use this custom TextView to address an issue on 2.3 and lower where ellipsized text
58 // will not wrap more than 2 lines.
59 private final EllipsisTextView mTextView;
60 private final ImageView mIconView;
62 // The height of the banner view.
63 private final float mHeight;
65 // Listener that gets called when the banner is dismissed from the close button.
66 private OnDismissListener mOnDismissListener;
68 public interface OnDismissListener {
69 public void onDismiss();
70 }
72 public HomeBanner(Context context) {
73 this(context, null);
74 }
76 public HomeBanner(Context context, AttributeSet attrs) {
77 super(context, attrs);
79 LayoutInflater.from(context).inflate(R.layout.home_banner_content, this);
81 mTextView = (EllipsisTextView) findViewById(R.id.text);
82 mIconView = (ImageView) findViewById(R.id.icon);
84 mHeight = getResources().getDimensionPixelSize(R.dimen.home_banner_height);
86 // Disable the banner until a message is set.
87 setEnabled(false);
88 }
90 @Override
91 public void onAttachedToWindow() {
92 super.onAttachedToWindow();
94 // Tapping on the close button will ensure that the banner is never
95 // showed again on this session.
96 final ImageButton closeButton = (ImageButton) findViewById(R.id.close);
98 // The drawable should have 50% opacity.
99 closeButton.getDrawable().setAlpha(127);
101 closeButton.setOnClickListener(new View.OnClickListener() {
102 @Override
103 public void onClick(View view) {
104 HomeBanner.this.dismiss();
106 // Send the current message id back to JS.
107 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Dismiss", (String) getTag()));
108 }
109 });
111 setOnClickListener(new View.OnClickListener() {
112 @Override
113 public void onClick(View v) {
114 HomeBanner.this.dismiss();
116 // Send the current message id back to JS.
117 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Click", (String) getTag()));
118 }
119 });
121 GeckoAppShell.getEventDispatcher().registerEventListener("HomeBanner:Data", this);
122 }
124 @Override
125 public void onDetachedFromWindow() {
126 super.onDetachedFromWindow();
128 GeckoAppShell.getEventDispatcher().unregisterEventListener("HomeBanner:Data", this);
129 }
131 @Override
132 public void setVisibility(int visibility) {
133 // On pre-Honeycomb devices, setting the visibility to GONE won't actually
134 // hide the view unless we clear animations first.
135 if (Build.VERSION.SDK_INT < 11 && visibility == View.GONE) {
136 clearAnimation();
137 }
139 super.setVisibility(visibility);
140 }
142 public void setScrollingPages(boolean scrollingPages) {
143 mScrollingPages = scrollingPages;
144 }
146 public void setOnDismissListener(OnDismissListener listener) {
147 mOnDismissListener = listener;
148 }
150 /**
151 * Hides and disables the banner.
152 */
153 private void dismiss() {
154 setVisibility(View.GONE);
155 setEnabled(false);
157 if (mOnDismissListener != null) {
158 mOnDismissListener.onDismiss();
159 }
160 }
162 /**
163 * Sends a message to gecko to request a new banner message. UI is updated in handleMessage.
164 */
165 public void update() {
166 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Get", null));
167 }
169 @Override
170 public void handleMessage(String event, JSONObject message) {
171 final String id = message.optString("id");
172 final String text = message.optString("text");
173 final String iconURI = message.optString("iconURI");
175 // Don't update the banner if the message doesn't have valid id and text.
176 if (TextUtils.isEmpty(id) || TextUtils.isEmpty(text)) {
177 return;
178 }
180 // Update the banner message on the UI thread.
181 ThreadUtils.postToUiThread(new Runnable() {
182 @Override
183 public void run() {
184 // Store the current message id to pass back to JS in the view's OnClickListener.
185 setTag(id);
186 mTextView.setOriginalText(Html.fromHtml(text));
188 BitmapUtils.getDrawable(getContext(), iconURI, new BitmapUtils.BitmapLoader() {
189 @Override
190 public void onBitmapFound(final Drawable d) {
191 // Hide the image view if we don't have an icon to show.
192 if (d == null) {
193 mIconView.setVisibility(View.GONE);
194 } else {
195 mIconView.setImageDrawable(d);
196 }
197 }
198 });
200 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Shown", id));
202 // Enable the banner after a message is set.
203 setEnabled(true);
205 // Animate the banner if it is currently active.
206 if (mActive) {
207 animateUp();
208 }
209 }
210 });
211 }
213 public void setActive(boolean active) {
214 // No need to animate if not changing
215 if (mActive == active) {
216 return;
217 }
219 mActive = active;
221 // Don't animate if the banner isn't enabled.
222 if (!isEnabled()) {
223 return;
224 }
226 if (active) {
227 animateUp();
228 } else {
229 animateDown();
230 }
231 }
233 private void ensureVisible() {
234 // The banner visibility is set to GONE after it is animated off screen,
235 // so we need to make it visible again.
236 if (getVisibility() == View.GONE) {
237 // Translate the banner off screen before setting it to VISIBLE.
238 ViewHelper.setTranslationY(this, mHeight);
239 setVisibility(View.VISIBLE);
240 }
241 }
243 private void animateUp() {
244 // Don't try to animate if the user swiped the banner down previously to hide it.
245 if (mUserSwipedDown) {
246 return;
247 }
249 ensureVisible();
251 final PropertyAnimator animator = new PropertyAnimator(100);
252 animator.attach(this, Property.TRANSLATION_Y, 0);
253 animator.start();
254 }
256 private void animateDown() {
257 if (ViewHelper.getTranslationY(this) == mHeight) {
258 // Hide the banner to avoid intercepting clicks on pre-honeycomb devices.
259 setVisibility(View.GONE);
260 return;
261 }
263 final PropertyAnimator animator = new PropertyAnimator(100);
264 animator.attach(this, Property.TRANSLATION_Y, mHeight);
265 animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
266 @Override
267 public void onPropertyAnimationStart() {
268 }
270 @Override
271 public void onPropertyAnimationEnd() {
272 // Hide the banner to avoid intercepting clicks on pre-honeycomb devices.
273 setVisibility(View.GONE);
274 }
275 });
276 animator.start();
277 }
279 public void handleHomeTouch(MotionEvent event) {
280 if (!mActive || !isEnabled() || mScrollingPages) {
281 return;
282 }
284 ensureVisible();
286 switch (event.getActionMasked()) {
287 case MotionEvent.ACTION_DOWN: {
288 // Track the beginning of the touch
289 mTouchY = event.getRawY();
290 break;
291 }
293 case MotionEvent.ACTION_MOVE: {
294 final float curY = event.getRawY();
295 final float delta = mTouchY - curY;
296 mSnapBannerToTop = delta <= 0.0f;
298 float newTranslationY = ViewHelper.getTranslationY(this) + delta;
300 // Clamp the values to be between 0 and height.
301 if (newTranslationY < 0.0f) {
302 newTranslationY = 0.0f;
303 } else if (newTranslationY > mHeight) {
304 newTranslationY = mHeight;
305 }
307 // Don't change this value if it wasn't a significant movement
308 if (delta >= 10 || delta <= -10) {
309 mUserSwipedDown = (newTranslationY == mHeight);
310 }
312 ViewHelper.setTranslationY(this, newTranslationY);
313 mTouchY = curY;
314 break;
315 }
317 case MotionEvent.ACTION_UP:
318 case MotionEvent.ACTION_CANCEL: {
319 mTouchY = -1;
320 if (mSnapBannerToTop) {
321 animateUp();
322 } else {
323 animateDown();
324 mUserSwipedDown = true;
325 }
326 break;
327 }
328 }
329 }
330 }