/*
* Copyright 2015 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.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import io.plaidapp.R;
import io.plaidapp.data.Source;
import io.plaidapp.data.prefs.DribbblePrefs;
import io.plaidapp.data.prefs.SourceManager;
import io.plaidapp.ui.recyclerview.ItemTouchHelperAdapter;
import io.plaidapp.util.AnimUtils;
import io.plaidapp.util.ColorUtils;
import io.plaidapp.util.ViewUtils;
/**
* Adapter for showing the list of data sources used as filters for the home grid.
*/
public class FilterAdapter extends RecyclerView.Adapter<FilterAdapter.FilterViewHolder>
implements ItemTouchHelperAdapter, DribbblePrefs.DribbbleLoginStatusListener {
public interface FilterAuthoriser {
void requestDribbbleAuthorisation(View sharedElement, Source forSource);
}
private static final int FILTER_ICON_ENABLED_ALPHA = 179; // 70%
private static final int FILTER_ICON_DISABLED_ALPHA = 51; // 20%
private final List<Source> filters;
private final FilterAuthoriser authoriser;
private final Context context;
private @Nullable List<FiltersChangedCallbacks> callbacks;
public FilterAdapter(@NonNull Context context,
@NonNull List<Source> filters,
@NonNull FilterAuthoriser authoriser) {
this.context = context.getApplicationContext();
this.filters = filters;
this.authoriser = authoriser;
setHasStableIds(true);
}
public List<Source> getFilters() {
return filters;
}
/**
* Adds a new data source to the list of filters. If the source already exists then it is simply
* activated.
*
* @param toAdd the source to add
* @return whether the filter was added (i.e. if it did not already exist)
*/
public boolean addFilter(Source toAdd) {
// first check if it already exists
final int count = filters.size();
for (int i = 0; i < count; i++) {
Source existing = filters.get(i);
if (existing.getClass() == toAdd.getClass()
&& existing.key.equalsIgnoreCase(toAdd.key)) {
// already exists, just ensure it's active
if (!existing.active) {
existing.active = true;
dispatchFiltersChanged(existing);
notifyItemChanged(i, FilterAnimator.FILTER_ENABLED);
SourceManager.updateSource(existing, context);
}
return false;
}
}
// didn't already exist, so add it
filters.add(toAdd);
Collections.sort(filters, new Source.SourceComparator());
dispatchFiltersChanged(toAdd);
notifyDataSetChanged();
SourceManager.addSource(toAdd, context);
return true;
}
public void removeFilter(Source removing) {
int position = filters.indexOf(removing);
filters.remove(position);
notifyItemRemoved(position);
dispatchFilterRemoved(removing);
SourceManager.removeSource(removing, context);
}
public int getFilterPosition(Source filter) {
return filters.indexOf(filter);
}
public void enableFilterByKey(@NonNull String key, @NonNull Context context) {
final int count = filters.size();
for (int i = 0; i < count; i++) {
Source filter = filters.get(i);
if (filter.key.equals(key)) {
if (!filter.active) {
filter.active = true;
notifyItemChanged(i, FilterAnimator.FILTER_ENABLED);
dispatchFiltersChanged(filter);
SourceManager.updateSource(filter, context);
}
return;
}
}
}
public void highlightFilter(int adapterPosition) {
notifyItemChanged(adapterPosition, FilterAnimator.HIGHLIGHT);
}
@Override
public void onDribbbleLogin() {
// no-op
}
@Override
public void onDribbbleLogout() {
for (int i = 0; i < filters.size(); i++) {
Source filter = filters.get(i);
if (filter.active && isAuthorisedDribbbleSource(filter)) {
filter.active = false;
SourceManager.updateSource(filter, context);
dispatchFiltersChanged(filter);
notifyItemChanged(i, FilterAnimator.FILTER_DISABLED);
}
}
}
@Override
public FilterViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
final FilterViewHolder holder = new FilterViewHolder(LayoutInflater.from(viewGroup
.getContext()).inflate(R.layout.filter_item, viewGroup, false));
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final int position = holder.getAdapterPosition();
if (position == RecyclerView.NO_POSITION) return;
final Source filter = filters.get(position);
if (isAuthorisedDribbbleSource(filter) &&
!DribbblePrefs.get(holder.itemView.getContext()).isLoggedIn()) {
authoriser.requestDribbbleAuthorisation(holder.filterIcon, filter);
} else {
filter.active = !filter.active;
holder.filterName.setEnabled(filter.active);
notifyItemChanged(position, filter.active ? FilterAnimator.FILTER_ENABLED
: FilterAnimator.FILTER_DISABLED);
SourceManager.updateSource(filter, holder.itemView.getContext());
dispatchFiltersChanged(filter);
}
}
});
return holder;
}
@Override
public void onBindViewHolder(final FilterViewHolder holder, int position) {
final Source filter = filters.get(position);
holder.isSwipeable = filter.isSwipeDismissable();
holder.filterName.setText(filter.name);
holder.filterName.setEnabled(filter.active);
if (filter.iconRes > 0) {
holder.filterIcon.setImageDrawable(
holder.itemView.getContext().getDrawable(filter.iconRes));
}
holder.filterIcon.setImageAlpha(filter.active ? FILTER_ICON_ENABLED_ALPHA :
FILTER_ICON_DISABLED_ALPHA);
}
@Override
public void onBindViewHolder(FilterViewHolder holder,
int position,
List<Object> partialChangePayloads) {
if (!partialChangePayloads.isEmpty()) {
// if we're doing a partial re-bind i.e. an item is enabling/disabling or being
// highlighted then data hasn't changed. Just set state based on the payload
boolean filterEnabled = partialChangePayloads.contains(FilterAnimator.FILTER_ENABLED);
boolean filterDisabled = partialChangePayloads.contains(FilterAnimator.FILTER_DISABLED);
if (filterEnabled || filterDisabled) {
holder.filterName.setEnabled(filterEnabled);
// icon is handled by the animator
}
// nothing to do for highlight
} else {
onBindViewHolder(holder, position);
}
}
@Override
public int getItemCount() {
return filters.size();
}
@Override
public long getItemId(int position) {
return filters.get(position).key.hashCode();
}
@Override
public void onItemDismiss(int position) {
Source removing = filters.get(position);
if (removing.isSwipeDismissable()) {
removeFilter(removing);
}
}
public int getEnabledSourcesCount() {
int count = 0;
for (Source source : filters) {
if (source.active) {
count++;
}
}
return count;
}
public void registerFilterChangedCallback(FiltersChangedCallbacks callback) {
if (callbacks == null) {
callbacks = new ArrayList<>();
}
callbacks.add(callback);
}
public void unregisterFilterChangedCallback(FiltersChangedCallbacks callback) {
if (callbacks != null && !callbacks.isEmpty()) {
callbacks.remove(callback);
}
}
private boolean isAuthorisedDribbbleSource(Source source) {
return source.key.equals(SourceManager.SOURCE_DRIBBBLE_FOLLOWING)
|| source.key.equals(SourceManager.SOURCE_DRIBBBLE_USER_LIKES)
|| source.key.equals(SourceManager.SOURCE_DRIBBBLE_USER_SHOTS);
}
private void dispatchFiltersChanged(Source filter) {
if (callbacks != null && !callbacks.isEmpty()) {
for (FiltersChangedCallbacks callback : callbacks) {
callback.onFiltersChanged(filter);
}
}
}
private void dispatchFilterRemoved(Source filter) {
if (callbacks != null && !callbacks.isEmpty()) {
for (FiltersChangedCallbacks callback : callbacks) {
callback.onFilterRemoved(filter);
}
}
}
public static abstract class FiltersChangedCallbacks {
public void onFiltersChanged(Source changedFilter) { }
public void onFilterRemoved(Source removed) { }
}
public static class FilterViewHolder extends RecyclerView.ViewHolder {
public TextView filterName;
public ImageView filterIcon;
public boolean isSwipeable;
public FilterViewHolder(View itemView) {
super(itemView);
filterName = (TextView) itemView.findViewById(R.id.filter_name);
filterIcon = (ImageView) itemView.findViewById(R.id.filter_icon);
}
}
public static class FilterAnimator extends DefaultItemAnimator {
public static final int FILTER_ENABLED = 1;
public static final int FILTER_DISABLED = 2;
public static final int HIGHLIGHT = 3;
@Override
public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) {
return true;
}
@Override
public ItemHolderInfo obtainHolderInfo() {
return new FilterHolderInfo();
}
/* package */ static class FilterHolderInfo extends ItemHolderInfo {
boolean doEnable;
boolean doDisable;
boolean doHighlight;
}
@NonNull
@Override
public ItemHolderInfo recordPreLayoutInformation(RecyclerView.State state,
RecyclerView.ViewHolder viewHolder,
int changeFlags,
List<Object> payloads) {
FilterHolderInfo info = (FilterHolderInfo)
super.recordPreLayoutInformation(state, viewHolder, changeFlags, payloads);
if (!payloads.isEmpty()) {
info.doEnable = payloads.contains(FILTER_ENABLED);
info.doDisable = payloads.contains(FILTER_DISABLED);
info.doHighlight = payloads.contains(HIGHLIGHT);
}
return info;
}
@Override
public boolean animateChange(RecyclerView.ViewHolder oldHolder,
RecyclerView.ViewHolder newHolder,
ItemHolderInfo preInfo,
ItemHolderInfo postInfo) {
if (newHolder instanceof FilterViewHolder && preInfo instanceof FilterHolderInfo) {
final FilterViewHolder holder = (FilterViewHolder) newHolder;
final FilterHolderInfo info = (FilterHolderInfo) preInfo;
if (info.doEnable || info.doDisable) {
ObjectAnimator iconAlpha = ObjectAnimator.ofInt(holder.filterIcon,
ViewUtils.IMAGE_ALPHA,
info.doEnable ? FILTER_ICON_ENABLED_ALPHA :
FILTER_ICON_DISABLED_ALPHA);
iconAlpha.setDuration(300L);
iconAlpha.setInterpolator(AnimUtils.getFastOutSlowInInterpolator(holder
.itemView.getContext()));
iconAlpha.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
dispatchChangeStarting(holder, false);
holder.itemView.setHasTransientState(true);
}
@Override
public void onAnimationEnd(Animator animation) {
holder.itemView.setHasTransientState(false);
dispatchChangeFinished(holder, false);
}
});
iconAlpha.start();
} else if (info.doHighlight) {
int highlightColor =
ContextCompat.getColor(holder.itemView.getContext(), R.color.accent);
int fadeFromTo = ColorUtils.modifyAlpha(highlightColor, 0);
ObjectAnimator highlightBackground = ObjectAnimator.ofArgb(
holder.itemView,
ViewUtils.BACKGROUND_COLOR,
fadeFromTo,
highlightColor,
fadeFromTo);
highlightBackground.setDuration(1000L);
highlightBackground.setInterpolator(new LinearInterpolator());
highlightBackground.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
dispatchChangeStarting(holder, false);
holder.itemView.setHasTransientState(true);
}
@Override
public void onAnimationEnd(Animator animation) {
holder.itemView.setBackground(null);
holder.itemView.setHasTransientState(false);
dispatchChangeFinished(holder, false);
}
});
highlightBackground.start();
}
}
return super.animateChange(oldHolder, newHolder, preInfo, postInfo);
}
}
}