michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.home; michael@0: michael@0: import org.json.JSONException; michael@0: import org.json.JSONObject; michael@0: import org.mozilla.gecko.GeckoAppShell; michael@0: import org.mozilla.gecko.GeckoEvent; michael@0: import org.mozilla.gecko.R; michael@0: import org.mozilla.gecko.animation.PropertyAnimator; michael@0: import org.mozilla.gecko.animation.PropertyAnimator.Property; michael@0: import org.mozilla.gecko.animation.ViewHelper; michael@0: import org.mozilla.gecko.gfx.BitmapUtils; michael@0: import org.mozilla.gecko.util.GeckoEventListener; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: import org.mozilla.gecko.widget.EllipsisTextView; michael@0: michael@0: import android.content.Context; michael@0: import android.graphics.drawable.Drawable; michael@0: import android.os.Build; michael@0: import android.text.Html; michael@0: import android.text.Spanned; michael@0: import android.text.TextUtils; michael@0: import android.util.AttributeSet; michael@0: import android.util.Log; michael@0: import android.view.LayoutInflater; michael@0: import android.view.MotionEvent; michael@0: import android.view.View; michael@0: import android.widget.ImageButton; michael@0: import android.widget.ImageView; michael@0: import android.widget.LinearLayout; michael@0: import android.widget.TextView; michael@0: michael@0: public class HomeBanner extends LinearLayout michael@0: implements GeckoEventListener { michael@0: private static final String LOGTAG = "GeckoHomeBanner"; michael@0: michael@0: // Used for tracking scroll length michael@0: private float mTouchY = -1; michael@0: michael@0: // Used to detect for upwards scroll to push banner all the way up michael@0: private boolean mSnapBannerToTop; michael@0: michael@0: // Tracks whether or not the banner should be shown on the current panel. michael@0: private boolean mActive = false; michael@0: michael@0: // The user is currently swiping between HomePager pages michael@0: private boolean mScrollingPages = false; michael@0: michael@0: // Tracks whether the user swiped the banner down, preventing us from autoshowing when the user michael@0: // switches back to the default page. michael@0: private boolean mUserSwipedDown = false; michael@0: michael@0: // We must use this custom TextView to address an issue on 2.3 and lower where ellipsized text michael@0: // will not wrap more than 2 lines. michael@0: private final EllipsisTextView mTextView; michael@0: private final ImageView mIconView; michael@0: michael@0: // The height of the banner view. michael@0: private final float mHeight; michael@0: michael@0: // Listener that gets called when the banner is dismissed from the close button. michael@0: private OnDismissListener mOnDismissListener; michael@0: michael@0: public interface OnDismissListener { michael@0: public void onDismiss(); michael@0: } michael@0: michael@0: public HomeBanner(Context context) { michael@0: this(context, null); michael@0: } michael@0: michael@0: public HomeBanner(Context context, AttributeSet attrs) { michael@0: super(context, attrs); michael@0: michael@0: LayoutInflater.from(context).inflate(R.layout.home_banner_content, this); michael@0: michael@0: mTextView = (EllipsisTextView) findViewById(R.id.text); michael@0: mIconView = (ImageView) findViewById(R.id.icon); michael@0: michael@0: mHeight = getResources().getDimensionPixelSize(R.dimen.home_banner_height); michael@0: michael@0: // Disable the banner until a message is set. michael@0: setEnabled(false); michael@0: } michael@0: michael@0: @Override michael@0: public void onAttachedToWindow() { michael@0: super.onAttachedToWindow(); michael@0: michael@0: // Tapping on the close button will ensure that the banner is never michael@0: // showed again on this session. michael@0: final ImageButton closeButton = (ImageButton) findViewById(R.id.close); michael@0: michael@0: // The drawable should have 50% opacity. michael@0: closeButton.getDrawable().setAlpha(127); michael@0: michael@0: closeButton.setOnClickListener(new View.OnClickListener() { michael@0: @Override michael@0: public void onClick(View view) { michael@0: HomeBanner.this.dismiss(); michael@0: michael@0: // Send the current message id back to JS. michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Dismiss", (String) getTag())); michael@0: } michael@0: }); michael@0: michael@0: setOnClickListener(new View.OnClickListener() { michael@0: @Override michael@0: public void onClick(View v) { michael@0: HomeBanner.this.dismiss(); michael@0: michael@0: // Send the current message id back to JS. michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Click", (String) getTag())); michael@0: } michael@0: }); michael@0: michael@0: GeckoAppShell.getEventDispatcher().registerEventListener("HomeBanner:Data", this); michael@0: } michael@0: michael@0: @Override michael@0: public void onDetachedFromWindow() { michael@0: super.onDetachedFromWindow(); michael@0: michael@0: GeckoAppShell.getEventDispatcher().unregisterEventListener("HomeBanner:Data", this); michael@0: } michael@0: michael@0: @Override michael@0: public void setVisibility(int visibility) { michael@0: // On pre-Honeycomb devices, setting the visibility to GONE won't actually michael@0: // hide the view unless we clear animations first. michael@0: if (Build.VERSION.SDK_INT < 11 && visibility == View.GONE) { michael@0: clearAnimation(); michael@0: } michael@0: michael@0: super.setVisibility(visibility); michael@0: } michael@0: michael@0: public void setScrollingPages(boolean scrollingPages) { michael@0: mScrollingPages = scrollingPages; michael@0: } michael@0: michael@0: public void setOnDismissListener(OnDismissListener listener) { michael@0: mOnDismissListener = listener; michael@0: } michael@0: michael@0: /** michael@0: * Hides and disables the banner. michael@0: */ michael@0: private void dismiss() { michael@0: setVisibility(View.GONE); michael@0: setEnabled(false); michael@0: michael@0: if (mOnDismissListener != null) { michael@0: mOnDismissListener.onDismiss(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Sends a message to gecko to request a new banner message. UI is updated in handleMessage. michael@0: */ michael@0: public void update() { michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Get", null)); michael@0: } michael@0: michael@0: @Override michael@0: public void handleMessage(String event, JSONObject message) { michael@0: final String id = message.optString("id"); michael@0: final String text = message.optString("text"); michael@0: final String iconURI = message.optString("iconURI"); michael@0: michael@0: // Don't update the banner if the message doesn't have valid id and text. michael@0: if (TextUtils.isEmpty(id) || TextUtils.isEmpty(text)) { michael@0: return; michael@0: } michael@0: michael@0: // Update the banner message on the UI thread. michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: // Store the current message id to pass back to JS in the view's OnClickListener. michael@0: setTag(id); michael@0: mTextView.setOriginalText(Html.fromHtml(text)); michael@0: michael@0: BitmapUtils.getDrawable(getContext(), iconURI, new BitmapUtils.BitmapLoader() { michael@0: @Override michael@0: public void onBitmapFound(final Drawable d) { michael@0: // Hide the image view if we don't have an icon to show. michael@0: if (d == null) { michael@0: mIconView.setVisibility(View.GONE); michael@0: } else { michael@0: mIconView.setImageDrawable(d); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Shown", id)); michael@0: michael@0: // Enable the banner after a message is set. michael@0: setEnabled(true); michael@0: michael@0: // Animate the banner if it is currently active. michael@0: if (mActive) { michael@0: animateUp(); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: public void setActive(boolean active) { michael@0: // No need to animate if not changing michael@0: if (mActive == active) { michael@0: return; michael@0: } michael@0: michael@0: mActive = active; michael@0: michael@0: // Don't animate if the banner isn't enabled. michael@0: if (!isEnabled()) { michael@0: return; michael@0: } michael@0: michael@0: if (active) { michael@0: animateUp(); michael@0: } else { michael@0: animateDown(); michael@0: } michael@0: } michael@0: michael@0: private void ensureVisible() { michael@0: // The banner visibility is set to GONE after it is animated off screen, michael@0: // so we need to make it visible again. michael@0: if (getVisibility() == View.GONE) { michael@0: // Translate the banner off screen before setting it to VISIBLE. michael@0: ViewHelper.setTranslationY(this, mHeight); michael@0: setVisibility(View.VISIBLE); michael@0: } michael@0: } michael@0: michael@0: private void animateUp() { michael@0: // Don't try to animate if the user swiped the banner down previously to hide it. michael@0: if (mUserSwipedDown) { michael@0: return; michael@0: } michael@0: michael@0: ensureVisible(); michael@0: michael@0: final PropertyAnimator animator = new PropertyAnimator(100); michael@0: animator.attach(this, Property.TRANSLATION_Y, 0); michael@0: animator.start(); michael@0: } michael@0: michael@0: private void animateDown() { michael@0: if (ViewHelper.getTranslationY(this) == mHeight) { michael@0: // Hide the banner to avoid intercepting clicks on pre-honeycomb devices. michael@0: setVisibility(View.GONE); michael@0: return; michael@0: } michael@0: michael@0: final PropertyAnimator animator = new PropertyAnimator(100); michael@0: animator.attach(this, Property.TRANSLATION_Y, mHeight); michael@0: animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { michael@0: @Override michael@0: public void onPropertyAnimationStart() { michael@0: } michael@0: michael@0: @Override michael@0: public void onPropertyAnimationEnd() { michael@0: // Hide the banner to avoid intercepting clicks on pre-honeycomb devices. michael@0: setVisibility(View.GONE); michael@0: } michael@0: }); michael@0: animator.start(); michael@0: } michael@0: michael@0: public void handleHomeTouch(MotionEvent event) { michael@0: if (!mActive || !isEnabled() || mScrollingPages) { michael@0: return; michael@0: } michael@0: michael@0: ensureVisible(); michael@0: michael@0: switch (event.getActionMasked()) { michael@0: case MotionEvent.ACTION_DOWN: { michael@0: // Track the beginning of the touch michael@0: mTouchY = event.getRawY(); michael@0: break; michael@0: } michael@0: michael@0: case MotionEvent.ACTION_MOVE: { michael@0: final float curY = event.getRawY(); michael@0: final float delta = mTouchY - curY; michael@0: mSnapBannerToTop = delta <= 0.0f; michael@0: michael@0: float newTranslationY = ViewHelper.getTranslationY(this) + delta; michael@0: michael@0: // Clamp the values to be between 0 and height. michael@0: if (newTranslationY < 0.0f) { michael@0: newTranslationY = 0.0f; michael@0: } else if (newTranslationY > mHeight) { michael@0: newTranslationY = mHeight; michael@0: } michael@0: michael@0: // Don't change this value if it wasn't a significant movement michael@0: if (delta >= 10 || delta <= -10) { michael@0: mUserSwipedDown = (newTranslationY == mHeight); michael@0: } michael@0: michael@0: ViewHelper.setTranslationY(this, newTranslationY); michael@0: mTouchY = curY; michael@0: break; michael@0: } michael@0: michael@0: case MotionEvent.ACTION_UP: michael@0: case MotionEvent.ACTION_CANCEL: { michael@0: mTouchY = -1; michael@0: if (mSnapBannerToTop) { michael@0: animateUp(); michael@0: } else { michael@0: animateDown(); michael@0: mUserSwipedDown = true; michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: }