/* * Copyright 2016 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.plaidapp.ui; import android.app.Activity; import android.app.ActivityOptions; import android.content.Context; import android.content.Intent; import android.graphics.drawable.AnimatedVectorDrawable; import android.os.Bundle; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; import com.bumptech.glide.Glide; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.text.NumberFormat; import java.util.ArrayList; import java.util.List; import butterknife.BindDimen; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; import io.plaidapp.R; import io.plaidapp.data.DataLoadingSubject; import io.plaidapp.data.PaginatedDataManager; import io.plaidapp.data.api.dribbble.FollowersDataManager; import io.plaidapp.data.api.dribbble.ShotLikesDataManager; import io.plaidapp.data.api.dribbble.model.Follow; import io.plaidapp.data.api.dribbble.model.Like; import io.plaidapp.data.api.dribbble.model.PlayerListable; import io.plaidapp.data.api.dribbble.model.Shot; import io.plaidapp.data.api.dribbble.model.User; import io.plaidapp.ui.recyclerview.InfiniteScrollListener; import io.plaidapp.ui.recyclerview.SlideInItemAnimator; import io.plaidapp.ui.widget.BottomSheet; import io.plaidapp.util.DribbbleUtils; import io.plaidapp.util.glide.CircleTransform; import static io.plaidapp.util.AnimUtils.getLinearOutSlowInInterpolator; public class PlayerSheet extends Activity { private static final int MODE_SHOT_LIKES = 1; private static final int MODE_FOLLOWERS = 2; private static final int DISMISS_DOWN = 0; private static final int DISMISS_CLOSE = 1; private static final String EXTRA_MODE = "EXTRA_MODE"; private static final String EXTRA_SHOT = "EXTRA_SHOT"; private static final String EXTRA_USER = "EXTRA_USER"; @Retention(RetentionPolicy.SOURCE) @IntDef({ MODE_SHOT_LIKES, MODE_FOLLOWERS }) @interface PlayerSheetMode { } @BindView(R.id.bottom_sheet) BottomSheet bottomSheet; @BindView(R.id.bottom_sheet_content) ViewGroup content; @BindView(R.id.title_bar) ViewGroup titleBar; @BindView(R.id.close) ImageView close; @BindView(R.id.title) TextView title; @BindView(R.id.player_list) RecyclerView playerList; @BindDimen(R.dimen.large_avatar_size) int largeAvatarSize; private @Nullable Shot shot; private @Nullable User player; private PaginatedDataManager dataManager; private LinearLayoutManager layoutManager; private PlayerAdapter adapter; private int dismissState = DISMISS_DOWN; public static void start(Activity launching, Shot shot) { Intent starter = new Intent(launching, PlayerSheet.class); starter.putExtra(EXTRA_MODE, MODE_SHOT_LIKES); starter.putExtra(EXTRA_SHOT, shot); launching.startActivity(starter, ActivityOptions.makeSceneTransitionAnimation(launching).toBundle()); } public static void start(Activity launching, User player) { Intent starter = new Intent(launching, PlayerSheet.class); starter.putExtra(EXTRA_MODE, MODE_FOLLOWERS); starter.putExtra(EXTRA_USER, player); launching.startActivity(starter, ActivityOptions.makeSceneTransitionAnimation(launching).toBundle()); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.player_sheet); ButterKnife.bind(this); final Intent intent = getIntent(); final @PlayerSheetMode int mode = intent.getIntExtra(EXTRA_MODE, -1); switch (mode) { case MODE_SHOT_LIKES: shot = intent.getParcelableExtra(EXTRA_SHOT); title.setText(getResources().getQuantityString( R.plurals.fans, (int) shot.likes_count, NumberFormat.getInstance().format(shot.likes_count))); dataManager = new ShotLikesDataManager(this, shot.id) { @Override public void onDataLoaded(List<Like> likes) { adapter.addItems(likes); } }; break; case MODE_FOLLOWERS: player = intent.getParcelableExtra(EXTRA_USER); title.setText(getResources().getQuantityString( R.plurals.follower_count, player.followers_count, NumberFormat.getInstance().format(player.followers_count))); dataManager = new FollowersDataManager(this, player.id) { @Override public void onDataLoaded(List<Follow> followers) { adapter.addItems(followers); } }; break; default: throw new IllegalArgumentException("Unknown launch mode."); } bottomSheet.registerCallback(new BottomSheet.Callbacks() { @Override public void onSheetDismissed() { finishAfterTransition(); } @Override public void onSheetPositionChanged(int sheetTop, boolean interacted) { if (interacted && close.getVisibility() != View.VISIBLE) { close.setVisibility(View.VISIBLE); close.setAlpha(0f); close.animate() .alpha(1f) .setDuration(400L) .setInterpolator(getLinearOutSlowInInterpolator(PlayerSheet.this)) .start(); } if (sheetTop == 0) { showClose(); } else { showDown(); } } }); layoutManager = new LinearLayoutManager(this); playerList.setLayoutManager(layoutManager); playerList.setItemAnimator(new SlideInItemAnimator()); adapter = new PlayerAdapter(this); dataManager.registerCallback(adapter); playerList.setAdapter(adapter); playerList.addOnScrollListener(new InfiniteScrollListener(layoutManager, dataManager) { @Override public void onLoadMore() { dataManager.loadData(); } }); playerList.addOnScrollListener(titleElevation); dataManager.loadData(); // kick off initial load } @Override protected void onDestroy() { dataManager.cancelLoading(); super.onDestroy(); } private RecyclerView.OnScrollListener titleElevation = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { final boolean raiseTitleBar = dy > 0 || playerList.computeVerticalScrollOffset() != 0; titleBar.setActivated(raiseTitleBar); // animated via a StateListAnimator } }; private void showClose() { if (dismissState == DISMISS_CLOSE) return; dismissState = DISMISS_CLOSE; final AnimatedVectorDrawable downToClose = (AnimatedVectorDrawable) ContextCompat.getDrawable(this, R.drawable.avd_down_to_close); close.setImageDrawable(downToClose); downToClose.start(); } private void showDown() { if (dismissState == DISMISS_DOWN) return; dismissState = DISMISS_DOWN; final AnimatedVectorDrawable closeToDown = (AnimatedVectorDrawable) ContextCompat.getDrawable(this, R.drawable.avd_close_to_down); close.setImageDrawable(closeToDown); closeToDown.start(); } @OnClick({ R.id.bottom_sheet, R.id.close }) public void dismiss(View view) { if (view.getVisibility() != View.VISIBLE) return; bottomSheet.dismiss(); } private class PlayerAdapter<T extends PlayerListable> extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements DataLoadingSubject.DataLoadingCallbacks{ private static final int TYPE_PLAYER = 7; private static final int TYPE_LOADING = -1; private List<T> items; private boolean loading = true; private LayoutInflater layoutInflater; private CircleTransform circleTransform; PlayerAdapter(Context context) { layoutInflater = LayoutInflater.from(context); items = new ArrayList<>(); circleTransform = new CircleTransform(context); setHasStableIds(true); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { switch (viewType) { case TYPE_PLAYER: return createPlayerViewHolder(parent); default: // TYPE_LOADING return new LoadingViewHolder( layoutInflater.inflate(R.layout.list_loading, parent, false)); } } @NonNull private PlayerViewHolder createPlayerViewHolder(ViewGroup parent) { final PlayerViewHolder holder = new PlayerViewHolder( layoutInflater.inflate(R.layout.player_item, parent, false)); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { final User user = items.get(holder.getAdapterPosition()).getPlayer(); final Intent player = new Intent(PlayerSheet.this, PlayerActivity.class); player.putExtra(PlayerActivity.EXTRA_PLAYER, user); final ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(PlayerSheet.this, Pair.create((View) holder.playerAvatar, getString(R.string.transition_player_avatar)), Pair.create(holder.itemView, getString(R.string.transition_player_background))); startActivity(player, options.toBundle()); } }); return holder; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (position == getLoadingMoreItemPosition()) return; bindPlayer(((PlayerViewHolder) holder), items.get(position)); } private void bindPlayer(PlayerViewHolder holder, T player) { Glide.with(holder.itemView.getContext()) .load(player.getPlayer().getHighQualityAvatarUrl()) .transform(circleTransform) .placeholder(R.drawable.avatar_placeholder) .override(largeAvatarSize, largeAvatarSize) .into(holder.playerAvatar); holder.playerName.setText(player.getPlayer().name.toLowerCase()); if (!TextUtils.isEmpty(player.getPlayer().bio)) { DribbbleUtils.parseAndSetText(holder.playerBio, player.getPlayer().bio); } else if (!TextUtils.isEmpty(player.getPlayer().location)) { holder.playerBio.setText(player.getPlayer().location); } holder.timeAgo.setText( DateUtils.getRelativeTimeSpanString(player.getDateCreated().getTime(), System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS) .toString().toLowerCase()); } @Override public int getItemViewType(int position) { if (position < getDataItemCount() && getDataItemCount() > 0) { return TYPE_PLAYER; } return TYPE_LOADING; } @Override public long getItemId(int position) { if (getItemViewType(position) == TYPE_LOADING) { return -1L; } return items.get(position).getId(); } public int getDataItemCount() { return items.size(); } private int getLoadingMoreItemPosition() { return loading ? getItemCount() - 1 : RecyclerView.NO_POSITION; } @Override public int getItemCount() { return getDataItemCount() + (loading ? 1 : 0); } @Override public void dataStartedLoading() { if (loading) return; loading = true; notifyItemInserted(getLoadingMoreItemPosition()); } @Override public void dataFinishedLoading() { if (!loading) return; final int loadingPos = getLoadingMoreItemPosition(); loading = false; notifyItemRemoved(loadingPos); } public void addItems(List<T> newItems) { final int insertRangeStart = getDataItemCount(); items.addAll(newItems); notifyItemRangeInserted(insertRangeStart, newItems.size()); } } /* package */ static class PlayerViewHolder extends RecyclerView.ViewHolder { @BindView(R.id.player_avatar) ImageView playerAvatar; @BindView(R.id.player_name) TextView playerName; @BindView(R.id.player_bio) TextView playerBio; @BindView(R.id.time_ago) TextView timeAgo; public PlayerViewHolder(View itemView) { super(itemView); ButterKnife.bind(this, itemView); } } private static class LoadingViewHolder extends RecyclerView.ViewHolder { ProgressBar progress; public LoadingViewHolder(View itemView) { super(itemView); progress = (ProgressBar) itemView; } } }