| |
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/. */ |
| |
5 |
| |
6 package org.mozilla.gecko.home; |
| |
7 |
| |
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; |
| |
20 |
| |
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; |
| |
36 |
| |
37 public class HomeBanner extends LinearLayout |
| |
38 implements GeckoEventListener { |
| |
39 private static final String LOGTAG = "GeckoHomeBanner"; |
| |
40 |
| |
41 // Used for tracking scroll length |
| |
42 private float mTouchY = -1; |
| |
43 |
| |
44 // Used to detect for upwards scroll to push banner all the way up |
| |
45 private boolean mSnapBannerToTop; |
| |
46 |
| |
47 // Tracks whether or not the banner should be shown on the current panel. |
| |
48 private boolean mActive = false; |
| |
49 |
| |
50 // The user is currently swiping between HomePager pages |
| |
51 private boolean mScrollingPages = false; |
| |
52 |
| |
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; |
| |
56 |
| |
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; |
| |
61 |
| |
62 // The height of the banner view. |
| |
63 private final float mHeight; |
| |
64 |
| |
65 // Listener that gets called when the banner is dismissed from the close button. |
| |
66 private OnDismissListener mOnDismissListener; |
| |
67 |
| |
68 public interface OnDismissListener { |
| |
69 public void onDismiss(); |
| |
70 } |
| |
71 |
| |
72 public HomeBanner(Context context) { |
| |
73 this(context, null); |
| |
74 } |
| |
75 |
| |
76 public HomeBanner(Context context, AttributeSet attrs) { |
| |
77 super(context, attrs); |
| |
78 |
| |
79 LayoutInflater.from(context).inflate(R.layout.home_banner_content, this); |
| |
80 |
| |
81 mTextView = (EllipsisTextView) findViewById(R.id.text); |
| |
82 mIconView = (ImageView) findViewById(R.id.icon); |
| |
83 |
| |
84 mHeight = getResources().getDimensionPixelSize(R.dimen.home_banner_height); |
| |
85 |
| |
86 // Disable the banner until a message is set. |
| |
87 setEnabled(false); |
| |
88 } |
| |
89 |
| |
90 @Override |
| |
91 public void onAttachedToWindow() { |
| |
92 super.onAttachedToWindow(); |
| |
93 |
| |
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); |
| |
97 |
| |
98 // The drawable should have 50% opacity. |
| |
99 closeButton.getDrawable().setAlpha(127); |
| |
100 |
| |
101 closeButton.setOnClickListener(new View.OnClickListener() { |
| |
102 @Override |
| |
103 public void onClick(View view) { |
| |
104 HomeBanner.this.dismiss(); |
| |
105 |
| |
106 // Send the current message id back to JS. |
| |
107 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Dismiss", (String) getTag())); |
| |
108 } |
| |
109 }); |
| |
110 |
| |
111 setOnClickListener(new View.OnClickListener() { |
| |
112 @Override |
| |
113 public void onClick(View v) { |
| |
114 HomeBanner.this.dismiss(); |
| |
115 |
| |
116 // Send the current message id back to JS. |
| |
117 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Click", (String) getTag())); |
| |
118 } |
| |
119 }); |
| |
120 |
| |
121 GeckoAppShell.getEventDispatcher().registerEventListener("HomeBanner:Data", this); |
| |
122 } |
| |
123 |
| |
124 @Override |
| |
125 public void onDetachedFromWindow() { |
| |
126 super.onDetachedFromWindow(); |
| |
127 |
| |
128 GeckoAppShell.getEventDispatcher().unregisterEventListener("HomeBanner:Data", this); |
| |
129 } |
| |
130 |
| |
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 } |
| |
138 |
| |
139 super.setVisibility(visibility); |
| |
140 } |
| |
141 |
| |
142 public void setScrollingPages(boolean scrollingPages) { |
| |
143 mScrollingPages = scrollingPages; |
| |
144 } |
| |
145 |
| |
146 public void setOnDismissListener(OnDismissListener listener) { |
| |
147 mOnDismissListener = listener; |
| |
148 } |
| |
149 |
| |
150 /** |
| |
151 * Hides and disables the banner. |
| |
152 */ |
| |
153 private void dismiss() { |
| |
154 setVisibility(View.GONE); |
| |
155 setEnabled(false); |
| |
156 |
| |
157 if (mOnDismissListener != null) { |
| |
158 mOnDismissListener.onDismiss(); |
| |
159 } |
| |
160 } |
| |
161 |
| |
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 } |
| |
168 |
| |
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"); |
| |
174 |
| |
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 } |
| |
179 |
| |
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)); |
| |
187 |
| |
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 }); |
| |
199 |
| |
200 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Shown", id)); |
| |
201 |
| |
202 // Enable the banner after a message is set. |
| |
203 setEnabled(true); |
| |
204 |
| |
205 // Animate the banner if it is currently active. |
| |
206 if (mActive) { |
| |
207 animateUp(); |
| |
208 } |
| |
209 } |
| |
210 }); |
| |
211 } |
| |
212 |
| |
213 public void setActive(boolean active) { |
| |
214 // No need to animate if not changing |
| |
215 if (mActive == active) { |
| |
216 return; |
| |
217 } |
| |
218 |
| |
219 mActive = active; |
| |
220 |
| |
221 // Don't animate if the banner isn't enabled. |
| |
222 if (!isEnabled()) { |
| |
223 return; |
| |
224 } |
| |
225 |
| |
226 if (active) { |
| |
227 animateUp(); |
| |
228 } else { |
| |
229 animateDown(); |
| |
230 } |
| |
231 } |
| |
232 |
| |
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 } |
| |
242 |
| |
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 } |
| |
248 |
| |
249 ensureVisible(); |
| |
250 |
| |
251 final PropertyAnimator animator = new PropertyAnimator(100); |
| |
252 animator.attach(this, Property.TRANSLATION_Y, 0); |
| |
253 animator.start(); |
| |
254 } |
| |
255 |
| |
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 } |
| |
262 |
| |
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 } |
| |
269 |
| |
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 } |
| |
278 |
| |
279 public void handleHomeTouch(MotionEvent event) { |
| |
280 if (!mActive || !isEnabled() || mScrollingPages) { |
| |
281 return; |
| |
282 } |
| |
283 |
| |
284 ensureVisible(); |
| |
285 |
| |
286 switch (event.getActionMasked()) { |
| |
287 case MotionEvent.ACTION_DOWN: { |
| |
288 // Track the beginning of the touch |
| |
289 mTouchY = event.getRawY(); |
| |
290 break; |
| |
291 } |
| |
292 |
| |
293 case MotionEvent.ACTION_MOVE: { |
| |
294 final float curY = event.getRawY(); |
| |
295 final float delta = mTouchY - curY; |
| |
296 mSnapBannerToTop = delta <= 0.0f; |
| |
297 |
| |
298 float newTranslationY = ViewHelper.getTranslationY(this) + delta; |
| |
299 |
| |
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 } |
| |
306 |
| |
307 // Don't change this value if it wasn't a significant movement |
| |
308 if (delta >= 10 || delta <= -10) { |
| |
309 mUserSwipedDown = (newTranslationY == mHeight); |
| |
310 } |
| |
311 |
| |
312 ViewHelper.setTranslationY(this, newTranslationY); |
| |
313 mTouchY = curY; |
| |
314 break; |
| |
315 } |
| |
316 |
| |
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 } |