Giphy integration

// FREEBIE
This commit is contained in:
Moxie Marlinspike
2016-10-16 19:05:07 -07:00
parent 8e9e3847b6
commit 69d0242574
58 changed files with 1644 additions and 15 deletions

View File

@@ -0,0 +1,62 @@
package org.thoughtcrime.securesms.giph.model;
import com.fasterxml.jackson.annotation.JsonProperty;
public class GiphyImage {
@JsonProperty
private ImageTypes images;
public String getGifUrl() {
return images.downsized_medium.url;
}
public float getGifAspectRatio() {
return (float)images.downsized_medium.width / (float)images.downsized_medium.height;
}
public String getStillUrl() {
return images.fixed_width_still.url;
}
public static class ImageTypes {
@JsonProperty
private ImageData fixed_height;
@JsonProperty
private ImageData fixed_height_still;
@JsonProperty
private ImageData fixed_height_downsampled;
@JsonProperty
private ImageData fixed_width;
@JsonProperty
private ImageData fixed_width_still;
@JsonProperty
private ImageData fixed_width_downsampled;
@JsonProperty
private ImageData fixed_width_small;
@JsonProperty
private ImageData downsized_medium;
}
public static class ImageData {
@JsonProperty
private String url;
@JsonProperty
private int width;
@JsonProperty
private int height;
@JsonProperty
private int size;
@JsonProperty
private String mp4;
@JsonProperty
private String webp;
}
}

View File

@@ -0,0 +1,17 @@
package org.thoughtcrime.securesms.giph.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class GiphyResponse {
@JsonProperty
private List<GiphyImage> data;
public List<GiphyImage> getData() {
return data;
}
}

View File

@@ -0,0 +1,23 @@
package org.thoughtcrime.securesms.giph.net;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
public class GiphyGifLoader extends GiphyLoader {
public GiphyGifLoader(@NonNull Context context, @Nullable String searchString) {
super(context, searchString);
}
@Override
protected String getTrendingUrl() {
return "https://api.giphy.com/v1/gifs/trending?api_key=3o6ZsYH6U6Eri53TXy&offset=%d&limit=" + PAGE_SIZE;
}
@Override
protected String getSearchUrl() {
return "https://api.giphy.com/v1/gifs/search?api_key=3o6ZsYH6U6Eri53TXy&offset=%d&limit=" + PAGE_SIZE + "&q=%s";
}
}

View File

@@ -0,0 +1,70 @@
package org.thoughtcrime.securesms.giph.net;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import org.thoughtcrime.securesms.giph.model.GiphyImage;
import org.thoughtcrime.securesms.giph.model.GiphyResponse;
import org.thoughtcrime.securesms.util.AsyncLoader;
import org.thoughtcrime.securesms.util.JsonUtils;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
public abstract class GiphyLoader extends AsyncLoader<List<GiphyImage>> {
private static final String TAG = GiphyLoader.class.getName();
public static int PAGE_SIZE = 100;
@Nullable private String searchString;
private final OkHttpClient client = new OkHttpClient();
protected GiphyLoader(@NonNull Context context, @Nullable String searchString) {
super(context);
this.searchString = searchString;
this.client.setProxySelector(new GiphyProxySelector());
}
@Override
public List<GiphyImage> loadInBackground() {
return loadPage(0);
}
public List<GiphyImage> loadPage(int offset) {
try {
String url;
if (TextUtils.isEmpty(searchString)) url = String.format(getTrendingUrl(), offset);
else url = String.format(getSearchUrl(), offset, Uri.encode(searchString));
Request request = new Request.Builder().url(url).build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
GiphyResponse giphyResponse = JsonUtils.fromJson(response.body().byteStream(), GiphyResponse.class);
return giphyResponse.getData();
} catch (IOException e) {
Log.w(TAG, e);
return new LinkedList<>();
}
}
protected abstract String getTrendingUrl();
protected abstract String getSearchUrl();
}

View File

@@ -0,0 +1,73 @@
package org.thoughtcrime.securesms.giph.net;
import android.os.AsyncTask;
import android.util.Log;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
public class GiphyProxySelector extends ProxySelector {
private static final String TAG = GiphyProxySelector.class.getSimpleName();
private final List<Proxy> EMPTY = new ArrayList<>(1);
private volatile List<Proxy> GIPHY = null;
public GiphyProxySelector() {
EMPTY.add(Proxy.NO_PROXY);
if (Util.isMainThread()) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
synchronized (GiphyProxySelector.this) {
initializeGiphyProxy();
GiphyProxySelector.this.notifyAll();
}
return null;
}
}.execute();
} else {
initializeGiphyProxy();
}
}
@Override
public List<Proxy> select(URI uri) {
if (uri.getHost().endsWith("giphy.com")) return getOrCreateGiphyProxy();
else return EMPTY;
}
@Override
public void connectFailed(URI uri, SocketAddress address, IOException failure) {
Log.w(TAG, failure);
}
private void initializeGiphyProxy() {
GIPHY = new ArrayList<Proxy>(1) {{
add(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(BuildConfig.GIPHY_PROXY_HOST,
BuildConfig.GIPHY_PROXY_PORT)));
}};
}
private List<Proxy> getOrCreateGiphyProxy() {
if (GIPHY == null) {
synchronized (this) {
while (GIPHY == null) Util.wait(this, 0);
}
}
return GIPHY;
}
}

View File

@@ -0,0 +1,23 @@
package org.thoughtcrime.securesms.giph.net;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
public class GiphyStickerLoader extends GiphyLoader {
public GiphyStickerLoader(@NonNull Context context, @Nullable String searchString) {
super(context, searchString);
}
@Override
protected String getTrendingUrl() {
return "https://api.giphy.com/v1/stickers/trending?api_key=3o6ZsYH6U6Eri53TXy&offset=%d&limit=" + PAGE_SIZE;
}
@Override
protected String getSearchUrl() {
return "https://api.giphy.com/v1/stickers/search?q=cat&api_key=3o6ZsYH6U6Eri53TXy&offset=%d&limit=" + PAGE_SIZE + "&q=%s";
}
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright (C) 2015 Twitter, 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 org.thoughtcrime.securesms.giph.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.ImageView;
/**
* AspectRatioImageView maintains an aspect ratio by adjusting the width or height dimension. The
* aspect ratio (width to height ratio) and adjustment dimension can be configured.
*/
public class AspectRatioImageView extends ImageView {
private static final float DEFAULT_ASPECT_RATIO = 1.0f;
private static final int DEFAULT_ADJUST_DIMENSION = 0;
// defined by attrs.xml enum
static final int ADJUST_DIMENSION_HEIGHT = 0;
static final int ADJUST_DIMENSION_WIDTH = 1;
private double aspectRatio; // width to height ratio
private int dimensionToAdjust; // ADJUST_DIMENSION_HEIGHT or ADJUST_DIMENSION_WIDTH
public AspectRatioImageView(Context context) {
this(context, null);
}
public AspectRatioImageView(Context context, AttributeSet attrs) {
super(context, attrs);
// final TypedArray a = context.obtainStyledAttributes(attrs,
// R.styleable.tw__AspectRatioImageView);
// try {
// aspectRatio = a.getFloat(R.styleable.tw__AspectRatioImageView_tw__image_aspect_ratio,
// DEFAULT_ASPECT_RATIO);
// dimensionToAdjust
// = a.getInt(R.styleable.tw__AspectRatioImageView_tw__image_dimension_to_adjust,
// DEFAULT_ADJUST_DIMENSION);
// } finally {
// a.recycle();
// }
}
public double getAspectRatio() {
return aspectRatio;
}
public int getDimensionToAdjust() {
return dimensionToAdjust;
}
/**
* Sets the aspect ratio that should be respected during measurement.
*
* @param aspectRatio desired width to height ratio
*/
public void setAspectRatio(final double aspectRatio) {
this.aspectRatio = aspectRatio;
}
/**
* Resets the size to 0.
*/
public void resetSize() {
if (getMeasuredWidth() == 0 && getMeasuredHeight() == 0) {
return;
}
measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY));
layout(0, 0, 0, 0);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
if (dimensionToAdjust == ADJUST_DIMENSION_HEIGHT) {
height = calculateHeight(width, aspectRatio);
} else {
width = calculateWidth(height, aspectRatio);
}
setMeasuredDimension(width, height);
}
/**
* Returns the height that will satisfy the width to height aspect ratio, keeping the given
* width fixed.
*/
int calculateHeight(int width, double ratio) {
if (ratio == 0) {
return 0;
}
return (int) Math.round(width / ratio);
}
/**
* Returns the width that will satisfy the width to height aspect ratio, keeping the given
* height fixed.
*/
int calculateWidth(int height, double ratio) {
return (int) Math.round(height * ratio);
}
}

View File

@@ -0,0 +1,163 @@
package org.thoughtcrime.securesms.giph.ui;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.concurrent.ExecutionException;
public class GiphyActivity extends PassphraseRequiredActionBarActivity
implements GiphyActivityToolbar.OnLayoutChangedListener,
GiphyActivityToolbar.OnFilterChangedListener,
GiphyAdapter.OnItemClickListener
{
private static final String TAG = GiphyActivity.class.getSimpleName();
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
private GiphyGifFragment gifFragment;
private GiphyStickerFragment stickerFragment;
private GiphyAdapter.GiphyViewHolder finishingImage;
@Override
public void onPreCreate() {
dynamicTheme.onCreate(this);
dynamicLanguage.onCreate(this);
}
@Override
public void onCreate(Bundle bundle, @NonNull MasterSecret masterSecret) {
setContentView(R.layout.giphy_activity);
initializeToolbar();
initializeResources();
}
private void initializeToolbar() {
GiphyActivityToolbar toolbar = ViewUtil.findById(this, R.id.giphy_toolbar);
toolbar.setOnFilterChangedListener(this);
toolbar.setOnLayoutChangedListener(this);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
getSupportActionBar().setDisplayShowTitleEnabled(false);
}
private void initializeResources() {
ViewPager viewPager = ViewUtil.findById(this, R.id.giphy_pager);
TabLayout tabLayout = ViewUtil.findById(this, R.id.tab_layout);
this.gifFragment = new GiphyGifFragment();
this.stickerFragment = new GiphyStickerFragment();
gifFragment.setClickListener(this);
stickerFragment.setClickListener(this);
viewPager.setAdapter(new GiphyFragmentPagerAdapter(this, getSupportFragmentManager(),
gifFragment, stickerFragment));
tabLayout.setupWithViewPager(viewPager);
}
@Override
public void onFilterChanged(String filter) {
this.gifFragment.setSearchString(filter);
this.stickerFragment.setSearchString(filter);
}
@Override
public void onLayoutChanged(int type) {
this.gifFragment.setLayoutManager(type);
this.stickerFragment.setLayoutManager(type);
}
@Override
public void onClick(final GiphyAdapter.GiphyViewHolder viewHolder) {
if (finishingImage != null) finishingImage.gifProgress.setVisibility(View.GONE);
finishingImage = viewHolder;
finishingImage.gifProgress.setVisibility(View.VISIBLE);
new AsyncTask<Void, Void, Uri>() {
@Override
protected Uri doInBackground(Void... params) {
try {
return Uri.fromFile(viewHolder.getFile());
} catch (InterruptedException | ExecutionException e) {
Log.w(TAG, e);
return null;
}
}
protected void onPostExecute(@Nullable Uri uri) {
if (uri == null) {
Toast.makeText(GiphyActivity.this, R.string.GiphyActivity_error_while_retrieving_full_resolution_gif, Toast.LENGTH_LONG).show();
} else if (viewHolder == finishingImage) {
setResult(RESULT_OK, new Intent().setData(uri));
finish();
} else {
Log.w(TAG, "Resolved Uri is no longer the selected element...");
}
}
}.execute();
}
private static class GiphyFragmentPagerAdapter extends FragmentPagerAdapter {
private final Context context;
private final GiphyGifFragment gifFragment;
private final GiphyStickerFragment stickerFragment;
private GiphyFragmentPagerAdapter(@NonNull Context context,
@NonNull FragmentManager fragmentManager,
@NonNull GiphyGifFragment gifFragment,
@NonNull GiphyStickerFragment stickerFragment)
{
super(fragmentManager);
this.context = context.getApplicationContext();
this.gifFragment = gifFragment;
this.stickerFragment = stickerFragment;
}
@Override
public Fragment getItem(int position) {
if (position == 0) return gifFragment;
else return stickerFragment;
}
@Override
public int getCount() {
return 2;
}
@Override
public CharSequence getPageTitle(int position) {
if (position == 0) return context.getString(R.string.GiphyFragmentPagerAdapter_gifs);
else return context.getString(R.string.GiphyFragmentPagerAdapter_stickers);
}
}
}

View File

@@ -0,0 +1,174 @@
package org.thoughtcrime.securesms.giph.ui;
import android.content.Context;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.support.v7.widget.Toolbar;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.TouchDelegate;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.util.ViewUtil;
public class GiphyActivityToolbar extends Toolbar {
@Nullable private OnFilterChangedListener filterListener;
@Nullable private OnLayoutChangedListener layoutListener;
private EditText searchText;
private AnimatingToggle toggle;
private ImageView action;
private ImageView listToggle;
private ImageView gridToggle;
private ImageView clearToggle;
private LinearLayout toggleContainer;
public GiphyActivityToolbar(Context context) {
this(context, null);
}
public GiphyActivityToolbar(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.toolbarStyle);
}
public GiphyActivityToolbar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.giphy_activity_toolbar, this);
this.action = ViewUtil.findById(this, R.id.action_icon);
this.searchText = ViewUtil.findById(this, R.id.search_view);
this.toggle = ViewUtil.findById(this, R.id.button_toggle);
this.listToggle = ViewUtil.findById(this, R.id.view_stream);
this.gridToggle = ViewUtil.findById(this, R.id.view_grid);
this.clearToggle = ViewUtil.findById(this, R.id.search_clear);
this.toggleContainer = ViewUtil.findById(this, R.id.toggle_container);
this.listToggle.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
displayTogglingView(gridToggle);
if (layoutListener != null) layoutListener.onLayoutChanged(OnLayoutChangedListener.LAYOUT_LIST);
}
});
this.gridToggle.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
displayTogglingView(listToggle);
if (layoutListener != null) layoutListener.onLayoutChanged(OnLayoutChangedListener.LAYOUT_GRID);
}
});
this.clearToggle.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
searchText.setText("");
clearToggle.setVisibility(View.INVISIBLE);
}
});
this.searchText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (SearchUtil.isEmpty(searchText)) clearToggle.setVisibility(View.INVISIBLE);
else clearToggle.setVisibility(View.VISIBLE);
notifyListener();
}
});
expandTapArea(this, action);
expandTapArea(toggleContainer, gridToggle);
}
@Override
public void setNavigationIcon(int resId) {
action.setImageResource(resId);
}
public void clear() {
searchText.setText("");
notifyListener();
}
public void setOnLayoutChangedListener(@Nullable OnLayoutChangedListener layoutListener) {
this.layoutListener = layoutListener;
}
public void setOnFilterChangedListener(@Nullable OnFilterChangedListener filterListener) {
this.filterListener = filterListener;
}
private void notifyListener() {
if (filterListener != null) filterListener.onFilterChanged(searchText.getText().toString());
}
private void displayTogglingView(View view) {
toggle.display(view);
expandTapArea(toggleContainer, view);
}
private void expandTapArea(final View container, final View child) {
final int padding = getResources().getDimensionPixelSize(R.dimen.contact_selection_actions_tap_area);
container.post(new Runnable() {
@Override
public void run() {
Rect rect = new Rect();
child.getHitRect(rect);
rect.top -= padding;
rect.left -= padding;
rect.right += padding;
rect.bottom += padding;
container.setTouchDelegate(new TouchDelegate(rect, child));
}
});
}
private static class SearchUtil {
public static boolean isTextInput(EditText editText) {
return (editText.getInputType() & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT;
}
public static boolean isPhoneInput(EditText editText) {
return (editText.getInputType() & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_PHONE;
}
public static boolean isEmpty(EditText editText) {
return editText.getText().length() <= 0;
}
}
public interface OnFilterChangedListener {
void onFilterChanged(String filter);
}
public interface OnLayoutChangedListener {
public static final int LAYOUT_GRID = 1;
public static final int LAYOUT_LIST = 2;
void onLayoutChanged(int type);
}
}

View File

@@ -0,0 +1,155 @@
package org.thoughtcrime.securesms.giph.ui;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import com.bumptech.glide.DrawableRequestBuilder;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.giph.model.GiphyImage;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.io.File;
import java.util.List;
import java.util.concurrent.ExecutionException;
public class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
private static final String TAG = GiphyAdapter.class.getSimpleName();
private List<GiphyImage> images;
private Context context;
private OnItemClickListener listener;
class GiphyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, RequestListener<String, GlideDrawable> {
public AspectRatioImageView thumbnail;
public GiphyImage image;
public ProgressBar gifProgress;
public volatile boolean modelReady;
GiphyViewHolder(View view) {
super(view);
thumbnail = ViewUtil.findById(view, R.id.thumbnail);
gifProgress = ViewUtil.findById(view, R.id.gif_progress);
thumbnail.setOnClickListener(this);
gifProgress.setVisibility(View.GONE);
}
@Override
public void onClick(View v) {
if (listener != null) listener.onClick(this);
}
@Override
public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) {
Log.w(TAG, e);
synchronized (this) {
if (image.getGifUrl().equals(model)) {
this.modelReady = true;
notifyAll();
}
}
return false;
}
@Override
public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
synchronized (this) {
if (image.getGifUrl().equals(model)) {
this.modelReady = true;
notifyAll();
}
}
return false;
}
public File getFile() throws ExecutionException, InterruptedException {
synchronized (this) {
while (!modelReady) {
Util.wait(this, 0);
}
}
return Glide.with(context)
.load(image.getGifUrl())
.downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
.get();
}
}
GiphyAdapter(Context context, List<GiphyImage> images) {
this.context = context;
this.images = images;
}
public void setImages(List<GiphyImage> images) {
this.images = images;
notifyDataSetChanged();
}
public void addImages(List<GiphyImage> images) {
this.images.addAll(images);
notifyDataSetChanged();
}
@Override
public GiphyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.giphy_thumbnail, parent, false);
return new GiphyViewHolder(itemView);
}
@Override
public void onBindViewHolder(GiphyViewHolder holder, int position) {
GiphyImage image = images.get(position);
holder.modelReady = false;
holder.image = image;
holder.thumbnail.setAspectRatio(image.getGifAspectRatio());
holder.gifProgress.setVisibility(View.GONE);
DrawableRequestBuilder<String> thumbnailRequest = Glide.with(context)
.load(image.getStillUrl());
Glide.with(context)
.load(image.getGifUrl())
.thumbnail(thumbnailRequest)
.placeholder(new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(context)))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.listener(holder)
.into(holder.thumbnail);
}
@Override
public int getItemCount() {
return images.size();
}
public void setListener(OnItemClickListener listener) {
this.listener = listener;
}
public interface OnItemClickListener {
void onClick(GiphyViewHolder viewHolder);
}
}

View File

@@ -0,0 +1,122 @@
package org.thoughtcrime.securesms.giph.ui;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.giph.model.GiphyImage;
import org.thoughtcrime.securesms.giph.net.GiphyLoader;
import org.thoughtcrime.securesms.giph.util.InfiniteScrollListener;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.LinkedList;
import java.util.List;
public abstract class GiphyFragment extends Fragment implements LoaderManager.LoaderCallbacks<List<GiphyImage>>, GiphyAdapter.OnItemClickListener {
private static final String TAG = GiphyFragment.class.getSimpleName();
private GiphyAdapter giphyAdapter;
private RecyclerView recyclerView;
private ProgressBar loadingProgress;
private TextView noResultsView;
private GiphyAdapter.OnItemClickListener listener;
protected String searchString;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
ViewGroup container = ViewUtil.inflate(inflater, viewGroup, R.layout.giphy_fragment);
this.recyclerView = ViewUtil.findById(container, R.id.giphy_list);
this.loadingProgress = ViewUtil.findById(container, R.id.loading_progress);
this.noResultsView = ViewUtil.findById(container, R.id.no_results);
return container;
}
@Override
public void onActivityCreated(Bundle bundle) {
super.onActivityCreated(bundle);
this.giphyAdapter = new GiphyAdapter(getActivity(), new LinkedList<GiphyImage>());
this.giphyAdapter.setListener(this);
this.recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
this.recyclerView.setItemAnimator(new DefaultItemAnimator());
this.recyclerView.setAdapter(giphyAdapter);
this.recyclerView.addOnScrollListener(new GiphyScrollListener());
getLoaderManager().initLoader(0, null, this);
}
@Override
public void onLoadFinished(Loader<List<GiphyImage>> loader, List<GiphyImage> data) {
this.loadingProgress.setVisibility(View.GONE);
if (data.isEmpty()) noResultsView.setVisibility(View.VISIBLE);
else noResultsView.setVisibility(View.GONE);
this.giphyAdapter.setImages(data);
}
@Override
public void onLoaderReset(Loader<List<GiphyImage>> loader) {
noResultsView.setVisibility(View.GONE);
this.giphyAdapter.setImages(new LinkedList<GiphyImage>());
}
public void setLayoutManager(int type) {
if (type == GiphyActivityToolbar.OnLayoutChangedListener.LAYOUT_GRID) {
this.recyclerView.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));
} else {
this.recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
}
}
public void setClickListener(GiphyAdapter.OnItemClickListener listener) {
this.listener = listener;
}
public void setSearchString(@Nullable String searchString) {
this.searchString = searchString;
this.noResultsView.setVisibility(View.GONE);
this.getLoaderManager().restartLoader(0, null, this);
}
@Override
public void onClick(GiphyAdapter.GiphyViewHolder viewHolder) {
if (listener != null) listener.onClick(viewHolder);
}
private class GiphyScrollListener extends InfiniteScrollListener {
@Override
public void onLoadMore(final int currentPage) {
final Loader<List<GiphyImage>> loader = getLoaderManager().getLoader(0);
if (loader == null) return;
new AsyncTask<Void, Void, List<GiphyImage>>() {
@Override
protected List<GiphyImage> doInBackground(Void... params) {
return ((GiphyLoader)loader).loadPage(currentPage * GiphyLoader.PAGE_SIZE);
}
protected void onPostExecute(List<GiphyImage> images) {
giphyAdapter.addImages(images);
}
}.execute();
}
}
}

View File

@@ -0,0 +1,19 @@
package org.thoughtcrime.securesms.giph.ui;
import android.os.Bundle;
import android.support.v4.content.Loader;
import org.thoughtcrime.securesms.giph.model.GiphyImage;
import org.thoughtcrime.securesms.giph.net.GiphyGifLoader;
import java.util.List;
public class GiphyGifFragment extends GiphyFragment {
@Override
public Loader<List<GiphyImage>> onCreateLoader(int id, Bundle args) {
return new GiphyGifLoader(getActivity(), searchString);
}
}

View File

@@ -0,0 +1,17 @@
package org.thoughtcrime.securesms.giph.ui;
import android.os.Bundle;
import android.support.v4.content.Loader;
import org.thoughtcrime.securesms.giph.model.GiphyImage;
import org.thoughtcrime.securesms.giph.net.GiphyStickerLoader;
import java.util.List;
public class GiphyStickerFragment extends GiphyFragment {
@Override
public Loader<List<GiphyImage>> onCreateLoader(int id, Bundle args) {
return new GiphyStickerLoader(getActivity(), searchString);
}
}

View File

@@ -0,0 +1,48 @@
// From https://gist.github.com/mipreamble/b6d4b3d65b0b4775a22e#file-recyclerviewpositionhelper-java
package org.thoughtcrime.securesms.giph.util;
import android.support.v7.widget.RecyclerView;
public abstract class InfiniteScrollListener extends RecyclerView.OnScrollListener {
public static String TAG = InfiniteScrollListener.class.getSimpleName();
private int previousTotal = 0; // The total number of items in the dataset after the last load
private boolean loading = true; // True if we are still waiting for the last set of data to load.
private int visibleThreshold = 5; // The minimum amount of items to have below your current scroll position before loading more.
int firstVisibleItem, visibleItemCount, totalItemCount;
private int currentPage = 1;
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
RecyclerViewPositionHelper recyclerViewPositionHelper = RecyclerViewPositionHelper.createHelper(recyclerView);
visibleItemCount = recyclerView.getChildCount();
totalItemCount = recyclerViewPositionHelper.getItemCount();
firstVisibleItem = recyclerViewPositionHelper.findFirstVisibleItemPosition();
if (loading) {
if (totalItemCount > previousTotal) {
loading = false;
previousTotal = totalItemCount;
}
}
if (!loading && (totalItemCount - visibleItemCount)
<= (firstVisibleItem + visibleThreshold)) {
// End has been reached
// Do something
currentPage++;
onLoadMore(currentPage);
loading = true;
}
}
public abstract void onLoadMore(int currentPage);
}

View File

@@ -0,0 +1,115 @@
// From https://gist.github.com/mipreamble/b6d4b3d65b0b4775a22e#file-recyclerviewpositionhelper-java
package org.thoughtcrime.securesms.giph.util;
import android.support.v7.widget.OrientationHelper;
import android.support.v7.widget.RecyclerView;
import android.view.View;
public class RecyclerViewPositionHelper {
final RecyclerView recyclerView;
final RecyclerView.LayoutManager layoutManager;
RecyclerViewPositionHelper(RecyclerView recyclerView) {
this.recyclerView = recyclerView;
this.layoutManager = recyclerView.getLayoutManager();
}
public static RecyclerViewPositionHelper createHelper(RecyclerView recyclerView) {
if (recyclerView == null) {
throw new NullPointerException("Recycler View is null");
}
return new RecyclerViewPositionHelper(recyclerView);
}
/**
* Returns the adapter item count.
*
* @return The total number on items in a layout manager
*/
public int getItemCount() {
return layoutManager == null ? 0 : layoutManager.getItemCount();
}
/**
* Returns the adapter position of the first visible view. This position does not include
* adapter changes that were dispatched after the last layout pass.
*
* @return The adapter position of the first visible item or {@link RecyclerView#NO_POSITION} if
* there aren't any visible items.
*/
public int findFirstVisibleItemPosition() {
final View child = findOneVisibleChild(0, layoutManager.getChildCount(), false, true);
return child == null ? RecyclerView.NO_POSITION : recyclerView.getChildAdapterPosition(child);
}
/**
* Returns the adapter position of the first fully visible view. This position does not include
* adapter changes that were dispatched after the last layout pass.
*
* @return The adapter position of the first fully visible item or
* {@link RecyclerView#NO_POSITION} if there aren't any visible items.
*/
public int findFirstCompletelyVisibleItemPosition() {
final View child = findOneVisibleChild(0, layoutManager.getChildCount(), true, false);
return child == null ? RecyclerView.NO_POSITION : recyclerView.getChildAdapterPosition(child);
}
/**
* Returns the adapter position of the last visible view. This position does not include
* adapter changes that were dispatched after the last layout pass.
*
* @return The adapter position of the last visible view or {@link RecyclerView#NO_POSITION} if
* there aren't any visible items
*/
public int findLastVisibleItemPosition() {
final View child = findOneVisibleChild(layoutManager.getChildCount() - 1, -1, false, true);
return child == null ? RecyclerView.NO_POSITION : recyclerView.getChildAdapterPosition(child);
}
/**
* Returns the adapter position of the last fully visible view. This position does not include
* adapter changes that were dispatched after the last layout pass.
*
* @return The adapter position of the last fully visible view or
* {@link RecyclerView#NO_POSITION} if there aren't any visible items.
*/
public int findLastCompletelyVisibleItemPosition() {
final View child = findOneVisibleChild(layoutManager.getChildCount() - 1, -1, true, false);
return child == null ? RecyclerView.NO_POSITION : recyclerView.getChildAdapterPosition(child);
}
View findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible,
boolean acceptPartiallyVisible) {
OrientationHelper helper;
if (layoutManager.canScrollVertically()) {
helper = OrientationHelper.createVerticalHelper(layoutManager);
} else {
helper = OrientationHelper.createHorizontalHelper(layoutManager);
}
final int start = helper.getStartAfterPadding();
final int end = helper.getEndAfterPadding();
final int next = toIndex > fromIndex ? 1 : -1;
View partiallyVisible = null;
for (int i = fromIndex; i != toIndex; i += next) {
final View child = layoutManager.getChildAt(i);
final int childStart = helper.getDecoratedStart(child);
final int childEnd = helper.getDecoratedEnd(child);
if (childStart < end && childEnd > start) {
if (completelyVisible) {
if (childStart >= start && childEnd <= end) {
return child;
} else if (acceptPartiallyVisible && partiallyVisible == null) {
partiallyVisible = child;
}
} else {
return child;
}
}
}
return partiallyVisible;
}
}