package org.wikipedia.page.tabs;
import android.content.DialogInterface;
import android.support.annotation.ColorRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.FrameLayout;
import android.widget.ListView;
import android.widget.TextView;
import com.facebook.drawee.view.SimpleDraweeView;
import org.wikipedia.R;
import org.wikipedia.page.PageBackStackItem;
import org.wikipedia.page.PageFragment;
import org.wikipedia.page.PageTitle;
import org.wikipedia.util.DimenUtil;
import org.wikipedia.views.ViewUtil;
import java.util.List;
import static org.wikipedia.util.DimenUtil.getContentTopOffsetPx;
import static org.wikipedia.util.ResourceUtil.getThemedAttributeId;
public class TabsProvider {
public interface TabsProviderListener {
void onEnterTabView();
void onCancelTabView();
void onTabSelected(int position);
void onNewTabRequested();
void onCloseTabRequested(int position);
void onCloseAllTabs();
}
public enum TabPosition {
CURRENT_TAB,
NEW_TAB_BACKGROUND,
NEW_TAB_FOREGROUND
}
private PageFragment fragment;
private View pageContentView;
private View tabContainerView;
private ListView tabListView;
private TabListAdapter tabListAdapter;
private ActionMode tabActionMode;
// True when action mode is terminated by the side effect of creating a new tab or selecting an
// existing tab.
private boolean isActionModeDismissedIndirectly;
private List<Tab> tabList;
private boolean launchedExternally;
@NonNull
private TabsProviderListener providerListener = new DefaultTabsProviderListener();
public void setTabsProviderListener(TabsProviderListener listener) {
providerListener = DefaultTabsProviderListener.defaultIfNull(listener);
}
public TabsProvider(PageFragment fragment, List<Tab> tabList) {
this.fragment = fragment;
this.tabList = tabList;
pageContentView = fragment.getContentView();
tabContainerView = fragment.getTabsContainerView();
tabListView = (ListView) tabContainerView.findViewById(R.id.tabs_list);
tabListAdapter = new TabListAdapter(fragment.getActivity().getLayoutInflater());
tabListView.setAdapter(tabListAdapter);
tabContainerView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
isActionModeDismissedIndirectly = true;
providerListener.onCancelTabView();
}
});
tabListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
isActionModeDismissedIndirectly = true;
providerListener.onTabSelected(position);
}
});
}
public boolean onBackPressed() {
if (isTabMode()) {
exitTabMode();
providerListener.onCancelTabView();
return true;
}
return false;
}
public void enterTabMode(boolean launchedExternally) {
enterTabMode(launchedExternally, null);
providerListener.onEnterTabView();
}
public boolean shouldPopFragment() {
return launchedExternally && !isActionModeDismissedIndirectly;
}
private boolean isTabMode() {
return tabActionMode != null;
}
private void enterTabMode(boolean launchedExternally, @Nullable Runnable onTabModeEntered) {
this.launchedExternally = launchedExternally;
if (isTabMode()) {
// already inside action mode...
// but make sure to update the list of tabs.
tabListAdapter.notifyDataSetInvalidated();
tabListView.smoothScrollToPosition(tabList.size() - 1);
if (onTabModeEntered != null) {
onTabModeEntered.run();
}
return;
}
fragment.startSupportActionMode(new TabActionModeCallback(onTabModeEntered));
}
private class TabActionModeCallback implements ActionMode.Callback {
private static final String TAB_ACTION_MODE_TAG = "actionModeTabList";
private final Runnable onTabModeEntered;
TabActionModeCallback(Runnable onTabModeEntered) {
this.onTabModeEntered = onTabModeEntered;
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
tabActionMode = mode;
mode.getMenuInflater().inflate(R.menu.menu_tabs, menu);
Animation anim = loadPageContentViewAnimation();
fragment.getContentView().startAnimation(anim);
layoutTabList(onTabModeEntered);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
mode.setTag(TAB_ACTION_MODE_TAG);
// find the action mode base view, and give it an empty click listener,
// otherwise click events within the empty area of the action mode will be passed
// down to the view beneath it, which is the Search bar, and we don't want to
// unintentionally initiate Search.
getActionBar().setClickable(true);
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_new_tab:
isActionModeDismissedIndirectly = true;
providerListener.onNewTabRequested();
return true;
case R.id.menu_close_all_tabs:
AlertDialog.Builder alert = new AlertDialog.Builder(fragment.getContext());
alert.setMessage(R.string.close_all_tabs_confirm);
alert.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
providerListener.onCloseAllTabs();
}
});
alert.setNegativeButton(R.string.no, null);
alert.create().show();
return true;
default:
return false;
}
}
@Override
public void onDestroyActionMode(ActionMode mode) {
Animation anim = AnimationUtils.loadAnimation(fragment.getContext(), R.anim.tab_list_zoom_exit);
fragment.getContentView().startAnimation(anim);
hideTabList();
tabActionMode = null;
fragment.showToolbar();
if (!isActionModeDismissedIndirectly) {
providerListener.onCancelTabView();
}
}
@NonNull
private View getActionBar() {
return fragment.getActivity().findViewById(R.id.action_mode_bar);
}
}
public void exitTabMode() {
if (isTabMode()) {
tabActionMode.finish();
}
}
public void showAndHideTabs() {
enterTabMode(false, new Runnable() {
private final int animDelay = 500;
@Override
public void run() {
tabContainerView.postDelayed(new Runnable() {
@Override
public void run() {
isActionModeDismissedIndirectly = true;
exitTabMode();
}
}, animDelay);
}
});
}
public void onConfigurationChanged() {
if (isTabMode()) {
setViewLayoutListener();
}
}
private void layoutTabList(final Runnable onTabModeEntered) {
tabContainerView.setVisibility(View.VISIBLE);
tabListAdapter.notifyDataSetInvalidated();
setTabListViewLayoutParams();
Animation anim = loadTabListViewAnimation();
anim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (onTabModeEntered != null) {
onTabModeEntered.run();
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
tabListView.startAnimation(anim);
// scroll to the bottom of the tab list
tabListView.smoothScrollToPosition(tabList.size() - 1);
}
private void hideTabList() {
Animation anim = AnimationUtils.loadAnimation(fragment.getContext(),
R.anim.tab_list_items_exit);
anim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
tabContainerView.setVisibility(View.GONE);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
tabListView.startAnimation(anim);
}
public void invalidate() {
tabListAdapter.notifyDataSetInvalidated();
}
private View.OnClickListener onItemCloseButtonListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
ViewHolder holder = (ViewHolder) v.getTag();
final int position = holder.position;
// if we're closing the last tab, don't do the animation...
if (tabList.size() == 1) {
providerListener.onCloseTabRequested(position);
} else {
Animation anim = AnimationUtils
.loadAnimation(fragment.getContext(), R.anim.slide_out_right);
anim.setAnimationListener(new Animation.AnimationListener() {
@Override public void onAnimationStart(Animation animation) { }
@Override
public void onAnimationEnd(Animation animation) {
providerListener.onCloseTabRequested(position);
}
@Override public void onAnimationRepeat(Animation animation) { }
});
holder.container.startAnimation(anim);
}
}
};
// size the listview to be the same width as the scaled-down webview...
private void setTabListViewLayoutParams() {
final float proportionHorz = 0.15f;
final float proportionVert = 0.4f;
final int heightOffset = 16;
int contentOffset = getContentTopOffsetPx(fragment.getContext());
int maxHeight = (int) (pageContentView.getHeight() * proportionVert
+ pageContentView.getHeight() * proportionHorz
- contentOffset - heightOffset * DimenUtil.getDensityScalar());
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, maxHeight);
float margin = pageContentView.getWidth() * proportionHorz / 2f;
params.leftMargin = (int) margin;
params.rightMargin = (int) margin;
params.topMargin = contentOffset;
tabListView.setLayoutParams(params);
}
private void setViewLayoutListener() {
tabListView.addOnLayoutChangeListener(new ViewLayoutListener());
}
private Animation loadPageContentViewAnimation() {
return AnimationUtils.loadAnimation(fragment.getContext(), R.anim.tab_list_zoom_enter);
}
private Animation loadTabListViewAnimation() {
return AnimationUtils.loadAnimation(fragment.getContext(), R.anim.tab_list_items_enter);
}
private class ViewLayoutListener implements View.OnLayoutChangeListener {
@Override
@SuppressWarnings("checkstyle:parameternumber")
public void onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft,
int oldTop, int oldRight, int oldBottom) {
view.removeOnLayoutChangeListener(this);
invalidateAnimations();
}
// Recalculate animations and apply the final frame.
private void invalidateAnimations() {
invalidatePageContentViewAnimation();
invalidateTabListViewAnimation();
}
private void invalidatePageContentViewAnimation() {
ViewUtil.setAnimationMatrix(pageContentView, loadPageContentViewAnimation());
}
private void invalidateTabListViewAnimation() {
// Post the layout for update and animation _after_ the current layout is applied.
tabListView.post(new Runnable() {
@Override
public void run() {
setTabListViewLayoutParams();
ViewUtil.setAnimationMatrix(tabListView, loadTabListViewAnimation());
}
});
}
}
private static class DefaultTabsProviderListener implements TabsProviderListener {
public static TabsProviderListener defaultIfNull(TabsProviderListener listener) {
return listener == null ? new DefaultTabsProviderListener() : listener;
}
@Override public void onEnterTabView() { }
@Override public void onCancelTabView() { }
@Override public void onTabSelected(int position) { }
@Override public void onNewTabRequested() { }
@Override public void onCloseTabRequested(int position) { }
@Override public void onCloseAllTabs() { }
}
private static class ViewHolder {
private View container;
private TextView title;
private SimpleDraweeView thumbnail;
private View gradient;
private View closeButton;
private int position;
}
private final class TabListAdapter extends BaseAdapter {
private final LayoutInflater inflater;
TabListAdapter(LayoutInflater inflater) {
this.inflater = inflater;
}
@Override
public int getCount() {
return tabList.size();
}
@Override
public Tab getItem(int position) {
return tabList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if (convertView == null) {
viewHolder = new ViewHolder();
convertView = inflater.inflate(R.layout.item_tab_entry, parent, false);
convertView.setTag(viewHolder);
viewHolder.container = convertView;
viewHolder.title = (TextView) convertView.findViewById(R.id.tab_item_title);
viewHolder.thumbnail = (SimpleDraweeView) convertView.findViewById(R.id.tab_item_thumbnail);
viewHolder.gradient = convertView.findViewById(R.id.tab_item_bottom_gradient);
viewHolder.closeButton = convertView.findViewById(R.id.tab_item_close);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.position = position;
viewHolder.closeButton.setTag(viewHolder);
viewHolder.closeButton.setOnClickListener(onItemCloseButtonListener);
// hide the shadow if this is the topmost tab
viewHolder.gradient.setVisibility(
position == tabList.size() - 1 ? View.GONE : View.VISIBLE);
@ColorRes int colorId = position == 0
? R.color.darkest_gray
: getThemedAttributeId(fragment.getContext(), R.attr.tab_shadow_color);
int color = ContextCompat.getColor(fragment.getContext(), colorId);
// dynamically set the background color that will show through the rounded corners.
// if it's the first last item in the tab list, we want the background to be the same
// as the activity background, otherwise it should match the tab shadow color.
convertView.setBackgroundColor(color);
List<PageBackStackItem> backstack = tabList.get(position).getBackStack();
if (backstack.size() > 0) {
PageTitle title = backstack.get(backstack.size() - 1).getTitle();
viewHolder.title.setText(title.getDisplayText());
ViewUtil.loadImageUrlInto(viewHolder.thumbnail, title.getThumbUrl());
}
return convertView;
}
}
}