Android Open Source - appboy-android-sdk Appboy Feed Fragment






From Project

Back to project page appboy-android-sdk.

License

The source code is released under:

Copyright (c) 2014 Appboy, Inc. All rights reserved. * Use of source code or binaries contained within Appboy's Android SDK is permitted only to enable use of the Appboy platform by customers of Appb...

If you think the Android project appboy-android-sdk listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package com.appboy.ui;
//  w  w w  . jav  a 2 s. c  om
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.v4.app.ListFragment;
import android.support.v4.view.GestureDetectorCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.util.Log;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import com.appboy.Appboy;
import com.appboy.Constants;
import com.appboy.enums.CardCategory;
import com.appboy.events.FeedUpdatedEvent;
import com.appboy.events.IEventSubscriber;
import com.appboy.models.cards.Card;
import com.appboy.ui.adapters.AppboyListAdapter;
import java.util.ArrayList;
import java.util.EnumSet;

public class AppboyFeedFragment extends ListFragment implements SwipeRefreshLayout.OnRefreshListener {
  private static final String TAG = String.format("%s.%s", Constants.APPBOY_LOG_TAG_PREFIX, AppboyFeedFragment.class.getName());
  private static final int NETWORK_PROBLEM_WARNING_MS = 5000;
  private static final int MAX_FEED_TTL_SECONDS = 60;
  private static final long AUTO_HIDE_REFRESH_INDICATOR_DELAY_MS = 2500L;

  private final Handler mMainThreadLooper = new Handler(Looper.getMainLooper());
  // Shows the network error message. This should only be executed on the Main/UI thread.
  private final Runnable mShowNetworkError = new Runnable() {
    @Override
    public void run() {
      // null checks make sure that this only executes when the constituent views are valid references.
      if (mLoadingSpinner != null) {
        mLoadingSpinner.setVisibility(View.GONE);
      }
      if (mNetworkErrorLayout != null) {
        mNetworkErrorLayout.setVisibility(View.VISIBLE);
      }
    }
  };

  private Appboy mAppboy;
  private IEventSubscriber<FeedUpdatedEvent> mFeedUpdatedSubscriber;
  private AppboyListAdapter mAdapter;
  private LinearLayout mNetworkErrorLayout;
  private LinearLayout mEmptyFeedLayout;
  private ProgressBar mLoadingSpinner;
  private RelativeLayout mFeedRootLayout;
  private boolean mSkipCardImpressionsReset;
  private EnumSet<CardCategory> mCategories;
  private SwipeRefreshLayout mFeedSwipeLayout;
  private int previousVisibleHeadCardIndex, currentCardIndexAtBottomOfScreen;
  private GestureDetectorCompat mGestureDetector;

  // This view should only be in the View.VISIBLE state when the listview is not visible. This view's
  // purpose is to let the "network error" and "no card" states to have the swipe-to-refresh functionality
  // when their respective views are visible.
  private View mTransparentFullBoundsContainerView;

  public AppboyFeedFragment() {}

  @Override
  public void onAttach(final Activity activity) {
    super.onAttach(activity);
    mAppboy = Appboy.getInstance(activity);
    if (mAdapter == null) {
      mAdapter = new AppboyListAdapter(activity, R.id.tag, new ArrayList<Card>());
      mCategories = CardCategory.ALL_CATEGORIES;
    }
    setRetainInstance(true);
    mGestureDetector = new GestureDetectorCompat(activity, new FeedGestureListener());
  }

  @Override
  public View onCreateView(LayoutInflater layoutInflater, ViewGroup container, Bundle savedInstanceState) {
    View view = layoutInflater.inflate(R.layout.com_appboy_feed, container, false);
    mNetworkErrorLayout = (LinearLayout) view.findViewById(R.id.com_appboy_feed_network_error);
    mLoadingSpinner = (ProgressBar) view.findViewById(R.id.com_appboy_feed_loading_spinner);
    mEmptyFeedLayout = (LinearLayout) view.findViewById(R.id.com_appboy_feed_empty_feed);
    mFeedRootLayout = (RelativeLayout) view.findViewById(R.id.com_appboy_feed_root);
    mFeedSwipeLayout = (SwipeRefreshLayout) view.findViewById(R.id.appboy_feed_swipe_container);
    mFeedSwipeLayout.setOnRefreshListener(this);
    mFeedSwipeLayout.setEnabled(false);
    mFeedSwipeLayout.setColorScheme(R.color.com_appboy_newsfeed_swipe_refresh_color_1,
      R.color.com_appboy_newsfeed_swipe_refresh_color_2,
      R.color.com_appboy_newsfeed_swipe_refresh_color_3,
      R.color.com_appboy_newsfeed_swipe_refresh_color_4);
    mTransparentFullBoundsContainerView = view.findViewById(R.id.com_appboy_feed_transparent_full_bounds_container_view);
    return view;
  }

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    if (mSkipCardImpressionsReset) {
      mSkipCardImpressionsReset = false;
    } else {
      mAdapter.resetCardImpressionTracker();
      Log.d(TAG, "Resetting card impressions.");
    }

    // Applying top and bottom padding as header and footer views allows for the top and bottom padding to be scrolled
    // away, as opposed to being a permanent frame around the feed.
    LayoutInflater inflater = LayoutInflater.from(getActivity());
    final ListView listView = getListView();
    listView.addHeaderView(inflater.inflate(R.layout.com_appboy_feed_header, null));
    listView.addFooterView(inflater.inflate(R.layout.com_appboy_feed_footer, null));

    mFeedRootLayout.setOnTouchListener(new View.OnTouchListener() {
      @Override
      public boolean onTouch(View view, MotionEvent motionEvent) {
        // Send touch events from the background view to the gesture detector to enable margin listview scrolling
        return mGestureDetector.onTouchEvent(motionEvent);
      }
    });

    // Enable the swipe-to-refresh view only when the user is at the head of the listview.
    listView.setOnScrollListener(new AbsListView.OnScrollListener() {
      @Override
      public void onScrollStateChanged(AbsListView absListView, int scrollState) {}
      @Override
      public void onScroll(AbsListView absListView, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        mFeedSwipeLayout.setEnabled(firstVisibleItem == 0);

        // Handle read/unread cards functionality below
        if (visibleItemCount == 0){
          // No cards/views have been loaded, do nothing
          return;
        }

        int currentVisibleHeadCardIndex = firstVisibleItem - 1;

        // Head index increased (scroll down)
        if (currentVisibleHeadCardIndex > previousVisibleHeadCardIndex){
          // Mark all cards in the gap as read
          mAdapter.batchSetCardsToRead(previousVisibleHeadCardIndex, currentVisibleHeadCardIndex);
        }
        previousVisibleHeadCardIndex = currentVisibleHeadCardIndex;

        // We take note of what card is at the bottom of the feed so that when this fragment is destroyed,
        // all on-screen cards have updated read indicators.
        currentCardIndexAtBottomOfScreen = firstVisibleItem + visibleItemCount;
      }
    });

    // We need the transparent view to pass it's touch events to the swipe-to-refresh view. We
    // do this by consuming touch events in the transparent view.
    mTransparentFullBoundsContainerView.setOnTouchListener(new View.OnTouchListener() {
      @Override
      public boolean onTouch(View view, MotionEvent motionEvent) {
        // Only consume events if the view is visible
        return view.getVisibility() == View.VISIBLE;
      }
    });

    // Remove the previous subscriber before rebuilding a new one with our new activity.
    mAppboy.removeSingleSubscription(mFeedUpdatedSubscriber, FeedUpdatedEvent.class);
    mFeedUpdatedSubscriber = new IEventSubscriber<FeedUpdatedEvent>() {
      @Override
      public void trigger(final FeedUpdatedEvent event) {
        Activity activity = getActivity();
        // Not strictly necessary, but being defensive in the face of a lot of inconsistent behavior with
        // fragment/activity lifecycles.
        if (activity == null) {
          return;
        }

        activity.runOnUiThread(new Runnable() {
          @Override
          public void run() {
            Log.d(TAG, "Updating feed views in response to FeedUpdatedEvent: " + event);
            // If a FeedUpdatedEvent comes in, we make sure that the network error isn't visible. It could become
            // visible again later if we need to request a new feed and it doesn't return in time, but we display a
            // network spinner while we wait, instead of keeping the network error up.
            mMainThreadLooper.removeCallbacks(mShowNetworkError);
            mNetworkErrorLayout.setVisibility(View.GONE);

            // If there are no cards, regardless of what happens further down, we're not going to show the list view, so
            // clear the list view and change relevant visibility now.
            if (event.getCardCount(mCategories) == 0) {
              listView.setVisibility(View.GONE);
              mAdapter.clear();
            } else {
              mEmptyFeedLayout.setVisibility(View.GONE);
              mLoadingSpinner.setVisibility(View.GONE);
              mTransparentFullBoundsContainerView.setVisibility(View.GONE);
            }

            // If we got our feed from offline storage, and it was old, we asynchronously request a new one from the server,
            // putting up a spinner if the old feed was empty.
            if (event.isFromOfflineStorage() && (event.lastUpdatedInSecondsFromEpoch() + MAX_FEED_TTL_SECONDS) * 1000 < System.currentTimeMillis()) {
              Log.i(TAG, String.format("Feed received was older than the max time to live of %d seconds, displaying it " +
                  "for now, but requesting an updated view from the server.", MAX_FEED_TTL_SECONDS));
              mAppboy.requestFeedRefresh();
              // If we don't have any cards to display, we put up the spinner while we wait for the network to return.
              // Eventually displaying an error message if it doesn't.
              if (event.getCardCount(mCategories) == 0) {
                Log.d(TAG, String.format("Old feed was empty, putting up a network spinner and registering the network error message on a delay of %dms.",
                    NETWORK_PROBLEM_WARNING_MS));
                mEmptyFeedLayout.setVisibility(View.GONE);
                mLoadingSpinner.setVisibility(View.VISIBLE);
                mTransparentFullBoundsContainerView.setVisibility(View.VISIBLE);
                mMainThreadLooper.postDelayed(mShowNetworkError, NETWORK_PROBLEM_WARNING_MS);
                return;
              }
            }

            // If we get here, we know that our feed is either fresh from the cache, or came down directly from a
            // network request. Thus, an empty feed shouldn't have a network error, or a spinner, we should just
            // tell the user that the feed is empty.
            if (event.getCardCount(mCategories) == 0) {
              mLoadingSpinner.setVisibility(View.GONE);
              mEmptyFeedLayout.setVisibility(View.VISIBLE);
              mTransparentFullBoundsContainerView.setVisibility(View.VISIBLE);
            } else {
              mAdapter.replaceFeed(event.getFeedCards(mCategories));
              listView.setVisibility(View.VISIBLE);
            }
            mFeedSwipeLayout.setRefreshing(false);
          }
        });
      }
    };
    mAppboy.subscribeToFeedUpdates(mFeedUpdatedSubscriber);

    // Once the header and footer views are set and our event handlers are ready to go, we set the adapter and hit the
    // cache for an initial feed load.
    listView.setAdapter(mAdapter);
    mAppboy.requestFeedRefreshFromCache();
  }

  @Override
  public void onResume() {
    super.onResume();
    Appboy.getInstance(getActivity()).logFeedDisplayed();
  }

  @Override
  public void onDestroyView() {
    super.onDestroyView();
    // If the view is destroyed, we don't care about updating it anymore. Remove the subscription immediately.
    mAppboy.removeSingleSubscription(mFeedUpdatedSubscriber, FeedUpdatedEvent.class);

    setOnScreenCardsToRead();
  }

  @Override
  public void onPause() {
    super.onPause();
    setOnScreenCardsToRead();
  }

  /**
   * This should be called whenever the feed goes off the user's screen.
   */
  private void setOnScreenCardsToRead() {
    // Set whatever cards are on screen to read since the view is being destroyed.
    mAdapter.batchSetCardsToRead(previousVisibleHeadCardIndex, currentCardIndexAtBottomOfScreen);
  }

  @Override
  public void onDetach() {
    super.onDetach();
    setListAdapter(null);
  }

  // The onSaveInstanceState method gets called before an orientation change when either the fragment is
  // the current fragment or exists in the fragment manager backstack.
  @Override
  public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    // We set mSkipCardImpressionsReset to true only when onSaveInstanceState is called while the fragment
    // is visible on the screen. That happens when the fragment is being managed by the fragment manager and
    // it is not in the backstack. We do this to avoid setting the mSkipCardImpressionsReset flag when the
    // device undergoes an orientation change while the fragment is in the backstack.
    if (isVisible()) {
      mSkipCardImpressionsReset = true;
    }
  }

  public EnumSet<CardCategory> getCategories() {
    return mCategories;
  }

  public void setCategory(CardCategory category) {
    setCategories(EnumSet.of(category));
  }

  /**
   * Calling this method will make AppboyFeedFragment display a list of cards where each card belongs
   * to at least one of the given categories.
   * When there are no cards in those categories, this method returns an empty list.
   * When the passed in categories are null, all cards will be returned.
   * When the passed in categories are empty EnumSet, an empty list will be returned.
   *
   * @param categories an EnumSet of CardCategory. Please pass in  a non-empty EnumSet of CardCategory,
   *                   or a null. An empty EnumSet is considered invalid.
   */
  public void setCategories(EnumSet<CardCategory> categories) {
    if (categories == null) {
      Log.i(TAG, "The categories passed into setCategories are null, AppboyFeedFragment is going to display all the cards in cache.");
      mCategories = CardCategory.ALL_CATEGORIES;
    } else if (categories.isEmpty()) {
      Log.w(TAG, "The categories set had no elements and have been ignored. Please pass a valid EnumSet of CardCategory.");
      return;
    } else if (categories.equals(mCategories)) {
      return;
    } else {
      mCategories = categories;
    }
    if (mAppboy != null) {
      mAppboy.requestFeedRefreshFromCache();
    }
  }

  // Called when the user swipes down and requests a feed refresh.
  @Override
  public void onRefresh() {
    mAppboy.requestFeedRefresh();
    mMainThreadLooper.postDelayed(new Runnable() {
      @Override
      public void run() {
        mFeedSwipeLayout.setRefreshing(false);
      }
    }, AUTO_HIDE_REFRESH_INDICATOR_DELAY_MS);
  }

  // This class is a custom listener to catch gestures happening outside the bounds of the listview that
  // should be fed into it.
  public class FeedGestureListener extends GestureDetector.SimpleOnGestureListener {
    @Override
    public boolean onDown(MotionEvent motionEvent) {
      return true;
    }
    @Override
    public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent2, float dx, float dy) {
      getListView().smoothScrollBy((int) dy, 0);
      return true;
    }
    @Override
    public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent2, float velocityX, float velocityY) {
      // We need to find the pixel distance of the scroll from the velocity with units (px / sec)
      // So d (px) = v (px / sec) * 1 (sec) / 1000 (ms) * deltaTimeMillis (ms)
      long deltaTimeMillis = (motionEvent2.getEventTime() - motionEvent.getEventTime()) * 2;
      int scrollDistance = (int) (velocityY * deltaTimeMillis / 1000);
      // Multiplied by 2 to get a smoother scroll effect during a fling
      getListView().smoothScrollBy(-scrollDistance, (int) (deltaTimeMillis * 2));
      return true;
    }
  }
}




Java Source Code List

com.android.vending.billing.utils.Base64DecoderException.java
com.android.vending.billing.utils.Base64.java
com.android.vending.billing.utils.IabException.java
com.android.vending.billing.utils.IabHelper.java
com.android.vending.billing.utils.IabResult.java
com.android.vending.billing.utils.Inventory.java
com.android.vending.billing.utils.Purchase.java
com.android.vending.billing.utils.Security.java
com.android.vending.billing.utils.SkuDetails.java
com.appboy.AppboyAdmReceiver.java
com.appboy.AppboyGcmReceiver.java
com.appboy.AppboyNotificationUtils.java
com.appboy.helloworld.HelloAppboyActivity.java
com.appboy.sample.AppboyBroadcastReceiver.java
com.appboy.sample.AppboyFragmentActivity.java
com.appboy.sample.CustomAppboyNavigator.java
com.appboy.sample.CustomSlideupManagerListener.java
com.appboy.sample.CustomSlideupViewFactory.java
com.appboy.sample.DecisionFragment.java
com.appboy.sample.DroidBoyActivity.java
com.appboy.sample.DroidGirlActivity.java
com.appboy.sample.DroidboyApplication.java
com.appboy.sample.FeedCategoriesFragment.java
com.appboy.sample.FeedFragmentActivity.java
com.appboy.sample.FeedbackFragmentActivity.java
com.appboy.sample.PreferencesActivity.java
com.appboy.sample.SlideupTesterActivity.java
com.appboy.sample.Test.java
com.appboy.sample.UserProfileDialog.java
com.appboy.sample.util.SharedPrefsUtil.java
com.appboy.ui.AppboyFeedFragment.java
com.appboy.ui.AppboyFeedbackFragment.java
com.appboy.ui.AppboyNavigator.java
com.appboy.ui.AppboyWebViewActivity.java
com.appboy.ui.actions.ActionFactory.java
com.appboy.ui.actions.ActivityAction.java
com.appboy.ui.actions.GooglePlayAppDetailsAction.java
com.appboy.ui.actions.IAction.java
com.appboy.ui.actions.ViewAction.java
com.appboy.ui.actions.WebAction.java
com.appboy.ui.activities.AppboyBaseActivity.java
com.appboy.ui.activities.AppboyBaseFragmentActivity.java
com.appboy.ui.activities.AppboyFeedActivity.java
com.appboy.ui.adapters.AppboyListAdapter.java
com.appboy.ui.configuration.XmlUIConfigurationProvider.java
com.appboy.ui.slideups.AppboySlideupManager.java
com.appboy.ui.slideups.ISlideupManagerListener.java
com.appboy.ui.slideups.ISlideupViewFactory.java
com.appboy.ui.slideups.ISlideupViewLifecycleListener.java
com.appboy.ui.slideups.SlideupCloser.java
com.appboy.ui.slideups.SlideupOperation.java
com.appboy.ui.slideups.SlideupViewWrapper.java
com.appboy.ui.slideups.SwipeDismissTouchListener.java
com.appboy.ui.slideups.TouchAwareSwipeDismissTouchListener.java
com.appboy.ui.support.DrawingUtils.java
com.appboy.ui.support.StringUtils.java
com.appboy.ui.support.UriUtils.java
com.appboy.ui.support.ViewUtils.java
com.appboy.ui.widget.BannerImageCardView.java
com.appboy.ui.widget.BaseCardView.java
com.appboy.ui.widget.CaptionedImageCardView.java
com.appboy.ui.widget.CrossPromotionSmallCardView.java
com.appboy.ui.widget.DefaultCardView.java
com.appboy.ui.widget.ShortNewsCardView.java
com.appboy.ui.widget.StarRatingView.java
com.appboy.ui.widget.TextAnnouncementCardView.java