Browse Source
Update Feed view, check description
Update Feed view, check description
Changes: 1. Separate out view holders for feed post types. 2. Improve performance for binding. 4. Initiating migration from Glide to Fresco, as performance of Fresco is way better. 3. Prefetch feed view thumbnails to get aspect ratio, to improve layout performance. 4. If auto play is off, the videos in feed are not loaded until play is pressed. 5. Lots of optimizations here and there.renovate/org.robolectric-robolectric-4.x
Ammar Githam
4 years ago
44 changed files with 4369 additions and 1268 deletions
-
6app/build.gradle
-
7app/src/main/java/awais/instagrabber/InstaGrabberApplication.java
-
462app/src/main/java/awais/instagrabber/MainHelper.java
-
17app/src/main/java/awais/instagrabber/activities/MainActivity.java
-
122app/src/main/java/awais/instagrabber/activities/PostViewer.java
-
201app/src/main/java/awais/instagrabber/activities/ProfileViewer.java
-
517app/src/main/java/awais/instagrabber/adapters/FeedAdapter.java
-
6app/src/main/java/awais/instagrabber/adapters/FeedStoriesAdapter.java
-
53app/src/main/java/awais/instagrabber/adapters/viewholder/FeedItemViewHolder.java
-
3app/src/main/java/awais/instagrabber/adapters/viewholder/HighlightViewHolder.java
-
153app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedItemViewHolder.java
-
87app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedPhotoViewHolder.java
-
339app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java
-
156app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedVideoViewHolder.java
-
162app/src/main/java/awais/instagrabber/customviews/CircularImageView.java
-
4app/src/main/java/awais/instagrabber/customviews/RamboTextView.java
-
170app/src/main/java/awais/instagrabber/customviews/drawee/AbstractAnimatedZoomableController.java
-
105app/src/main/java/awais/instagrabber/customviews/drawee/AnimatedZoomableController.java
-
720app/src/main/java/awais/instagrabber/customviews/drawee/DefaultZoomableController.java
-
84app/src/main/java/awais/instagrabber/customviews/drawee/DoubleTapGestureListener.java
-
72app/src/main/java/awais/instagrabber/customviews/drawee/GestureListenerWrapper.java
-
156app/src/main/java/awais/instagrabber/customviews/drawee/MultiGestureListener.java
-
286app/src/main/java/awais/instagrabber/customviews/drawee/MultiPointerGestureDetector.java
-
51app/src/main/java/awais/instagrabber/customviews/drawee/MultiZoomableControllerListener.java
-
203app/src/main/java/awais/instagrabber/customviews/drawee/TransformGestureDetector.java
-
134app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableController.java
-
416app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java
-
43app/src/main/java/awais/instagrabber/customviews/helpers/ImageResizingControllerListener.java
-
45app/src/main/java/awais/instagrabber/customviews/helpers/PauseGlideOnFlingScrollListener.java
-
497app/src/main/java/awais/instagrabber/customviews/helpers/VideoAwareRecyclerScroller.java
-
22app/src/main/java/awais/instagrabber/models/FeedModel.java
-
31app/src/main/java/awais/instagrabber/models/enums/MediaItemType.java
-
92app/src/main/java/awais/instagrabber/utils/Utils.java
-
11app/src/main/res/drawable/rounder_corner_semi_black_bg.xml
-
2app/src/main/res/layout/activity_main.xml
-
17app/src/main/res/layout/item_feed.xml
-
11app/src/main/res/layout/item_feed_bottom.xml
-
25app/src/main/res/layout/item_feed_photo.xml
-
19app/src/main/res/layout/item_feed_slider.xml
-
15app/src/main/res/layout/item_feed_top.xml
-
53app/src/main/res/layout/item_feed_video.xml
-
2app/src/main/res/layout/layout_feed_view.xml
-
59app/src/main/res/layout/layout_profile_view.xml
-
1app/src/main/res/values/color.xml
@ -1,499 +1,118 @@ |
|||
package awais.instagrabber.adapters; |
|||
|
|||
import android.annotation.SuppressLint; |
|||
import android.app.Activity; |
|||
import android.content.Context; |
|||
import android.content.DialogInterface; |
|||
import android.content.Intent; |
|||
import android.graphics.Typeface; |
|||
import android.net.Uri; |
|||
import android.text.SpannableString; |
|||
import android.text.SpannableStringBuilder; |
|||
import android.text.Spanned; |
|||
import android.text.method.LinkMovementMethod; |
|||
import android.text.style.StyleSpan; |
|||
import android.util.Log; |
|||
import android.view.LayoutInflater; |
|||
import android.view.View; |
|||
import android.view.ViewGroup; |
|||
import android.widget.ImageView; |
|||
import android.widget.RelativeLayout; |
|||
import android.widget.TextView; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.appcompat.app.AlertDialog; |
|||
import androidx.recyclerview.widget.RecyclerView; |
|||
import androidx.viewpager.widget.PagerAdapter; |
|||
import androidx.viewpager.widget.ViewPager; |
|||
import androidx.recyclerview.widget.DiffUtil; |
|||
import androidx.recyclerview.widget.ListAdapter; |
|||
|
|||
import com.bumptech.glide.Glide; |
|||
import com.bumptech.glide.RequestManager; |
|||
import com.github.chrisbanes.photoview.PhotoView; |
|||
import com.google.android.exoplayer2.Player; |
|||
import com.google.android.exoplayer2.SimpleExoPlayer; |
|||
import com.google.android.exoplayer2.source.ProgressiveMediaSource; |
|||
import com.google.android.exoplayer2.ui.PlayerView; |
|||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.Collections; |
|||
|
|||
import org.json.JSONObject; |
|||
|
|||
import awais.instagrabber.BuildConfig; |
|||
import awais.instagrabber.R; |
|||
import awais.instagrabber.activities.CommentsViewer; |
|||
import awais.instagrabber.activities.PostViewer; |
|||
import awais.instagrabber.adapters.viewholder.FeedItemViewHolder; |
|||
import awais.instagrabber.customviews.CommentMentionClickSpan; |
|||
import awais.instagrabber.adapters.viewholder.feed.FeedItemViewHolder; |
|||
import awais.instagrabber.adapters.viewholder.feed.FeedPhotoViewHolder; |
|||
import awais.instagrabber.adapters.viewholder.feed.FeedSliderViewHolder; |
|||
import awais.instagrabber.adapters.viewholder.feed.FeedVideoViewHolder; |
|||
import awais.instagrabber.customviews.RamboTextView; |
|||
import awais.instagrabber.databinding.ItemFeedPhotoBinding; |
|||
import awais.instagrabber.databinding.ItemFeedSliderBinding; |
|||
import awais.instagrabber.databinding.ItemFeedVideoBinding; |
|||
import awais.instagrabber.interfaces.MentionClickListener; |
|||
import awais.instagrabber.models.BasePostModel; |
|||
import awais.instagrabber.models.FeedModel; |
|||
import awais.instagrabber.models.PostModel; |
|||
import awais.instagrabber.models.ProfileModel; |
|||
import awais.instagrabber.models.ViewerPostModel; |
|||
import awais.instagrabber.models.enums.DownloadMethod; |
|||
import awais.instagrabber.models.enums.ItemGetType; |
|||
import awais.instagrabber.models.enums.MediaItemType; |
|||
import awais.instagrabber.utils.Constants; |
|||
import awais.instagrabber.utils.Utils; |
|||
|
|||
import static awais.instagrabber.utils.Utils.settingsHelper; |
|||
|
|||
public final class FeedAdapter extends RecyclerView.Adapter<FeedItemViewHolder> { |
|||
private final static String ellipsize = "… more"; |
|||
private final Activity activity; |
|||
private final LayoutInflater layoutInflater; |
|||
private final ArrayList<FeedModel> feedModels; |
|||
public final class FeedAdapter extends ListAdapter<FeedModel, FeedItemViewHolder> { |
|||
private static final String TAG = "FeedAdapter"; |
|||
// private final static String ellipsize = "… more"; |
|||
private final RequestManager glide; |
|||
private final View.OnClickListener clickListener; |
|||
private final MentionClickListener mentionClickListener; |
|||
private final View.OnClickListener clickListener = new View.OnClickListener() { |
|||
@Override |
|||
public void onClick(@NonNull final View v) { |
|||
final Object tag = v.getTag(); |
|||
|
|||
if (tag instanceof FeedModel) { |
|||
final FeedModel feedModel = (FeedModel) tag; |
|||
|
|||
if (v instanceof RamboTextView) { |
|||
if (feedModel.isMentionClicked()) |
|||
feedModel.toggleCaption(); |
|||
feedModel.setMentionClicked(false); |
|||
if (!expandCollapseTextView((RamboTextView) v, feedModel)) |
|||
feedModel.toggleCaption(); |
|||
|
|||
} else { |
|||
final int id = v.getId(); |
|||
switch (id) { |
|||
case R.id.btnComments: |
|||
activity.startActivityForResult(new Intent(activity, CommentsViewer.class) |
|||
.putExtra(Constants.EXTRAS_SHORTCODE, feedModel.getShortCode()) |
|||
.putExtra(Constants.EXTRAS_POST, feedModel.getPostId()) |
|||
.putExtra(Constants.EXTRAS_USER, feedModel.getProfileModel().getId()), 6969); |
|||
break; |
|||
|
|||
case R.id.viewStoryPost: |
|||
activity.startActivity(new Intent(activity, PostViewer.class) |
|||
.putExtra(Constants.EXTRAS_INDEX, feedModel.getPosition()) |
|||
.putExtra(Constants.EXTRAS_POST, new PostModel(feedModel.getShortCode(), false)) |
|||
.putExtra(Constants.EXTRAS_TYPE, ItemGetType.FEED_ITEMS)); |
|||
break; |
|||
|
|||
case R.id.btnDownload: |
|||
final Context context = v.getContext(); |
|||
ProfileModel profileModel = feedModel.getProfileModel(); |
|||
final String username = profileModel != null ? profileModel.getUsername() : null; |
|||
|
|||
final ViewerPostModel[] sliderItems = feedModel.getSliderItems(); |
|||
|
|||
if (feedModel.getItemType() != MediaItemType.MEDIA_TYPE_SLIDER || sliderItems == null || sliderItems.length == 1) |
|||
Utils.batchDownload(context, username, DownloadMethod.DOWNLOAD_FEED, Collections.singletonList(feedModel)); |
|||
else { |
|||
final ArrayList<BasePostModel> postModels = new ArrayList<>(); |
|||
final DialogInterface.OnClickListener clickListener = (dialog, which) -> { |
|||
postModels.clear(); |
|||
|
|||
final boolean breakWhenFoundSelected = which == DialogInterface.BUTTON_POSITIVE; |
|||
|
|||
for (final ViewerPostModel sliderItem : sliderItems) { |
|||
if (sliderItem != null) { |
|||
if (!breakWhenFoundSelected) postModels.add(sliderItem); |
|||
else if (sliderItem.isSelected()) { |
|||
postModels.add(sliderItem); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// shows 0 items on first item of viewpager cause onPageSelected hasn't been called yet |
|||
if (breakWhenFoundSelected && postModels.size() == 0) |
|||
postModels.add(sliderItems[0]); |
|||
|
|||
if (postModels.size() > 0) |
|||
Utils.batchDownload(context, username, DownloadMethod.DOWNLOAD_FEED, postModels); |
|||
}; |
|||
|
|||
new AlertDialog.Builder(context).setTitle(R.string.post_viewer_download_dialog_title) |
|||
.setPositiveButton(R.string.post_viewer_download_current, clickListener) |
|||
.setNegativeButton(R.string.post_viewer_download_album, clickListener).show(); |
|||
} |
|||
break; |
|||
|
|||
case R.id.ivProfilePic: |
|||
if (mentionClickListener != null) { |
|||
profileModel = feedModel.getProfileModel(); |
|||
if (profileModel != null) |
|||
mentionClickListener.onClick(null, profileModel.getUsername(), false); |
|||
} |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
public SimpleExoPlayer pagerPlayer; |
|||
private final View.OnLongClickListener longClickListener = v -> { |
|||
final Object tag; |
|||
if (v instanceof RamboTextView && (tag = v.getTag()) instanceof FeedModel) |
|||
Utils.copyText(v.getContext(), ((FeedModel) tag).getPostCaption()); |
|||
return true; |
|||
}; |
|||
public SimpleExoPlayer pagerPlayer; |
|||
private final PlayerChangeListener playerChangeListener = (childPos, player) -> { |
|||
// todo |
|||
pagerPlayer = player; |
|||
|
|||
private static final DiffUtil.ItemCallback<FeedModel> diffCallback = new DiffUtil.ItemCallback<FeedModel>() { |
|||
@Override |
|||
public boolean areItemsTheSame(@NonNull final FeedModel oldItem, @NonNull final FeedModel newItem) { |
|||
return oldItem.getPostId().equals(newItem.getPostId()); |
|||
} |
|||
|
|||
@Override |
|||
public boolean areContentsTheSame(@NonNull final FeedModel oldItem, @NonNull final FeedModel newItem) { |
|||
return oldItem.getPostId().equals(newItem.getPostId()); |
|||
} |
|||
}; |
|||
|
|||
public FeedAdapter(final Activity activity, final ArrayList<FeedModel> FeedModels, final MentionClickListener mentionClickListener) { |
|||
this.activity = activity; |
|||
this.feedModels = FeedModels; |
|||
public FeedAdapter(final RequestManager glide, |
|||
final View.OnClickListener clickListener, |
|||
final MentionClickListener mentionClickListener) { |
|||
super(diffCallback); |
|||
this.glide = glide; |
|||
this.clickListener = clickListener; |
|||
this.mentionClickListener = mentionClickListener; |
|||
this.layoutInflater = LayoutInflater.from(activity); |
|||
} |
|||
|
|||
@NonNull |
|||
@Override |
|||
public FeedItemViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { |
|||
final View view; |
|||
if (viewType == MediaItemType.MEDIA_TYPE_VIDEO.ordinal()) |
|||
view = layoutInflater.inflate(R.layout.item_feed_video, parent, false); |
|||
else if (viewType == MediaItemType.MEDIA_TYPE_SLIDER.ordinal()) |
|||
view = layoutInflater.inflate(R.layout.item_feed_slider, parent, false); |
|||
else |
|||
view = layoutInflater.inflate(R.layout.item_feed, parent, false); |
|||
return new FeedItemViewHolder(view); |
|||
} |
|||
|
|||
@SuppressLint("SetTextI18n") |
|||
@Override |
|||
public void onBindViewHolder(@NonNull final FeedItemViewHolder viewHolder, final int position) { |
|||
final FeedModel feedModel = feedModels.get(position); |
|||
if (feedModel != null) { |
|||
final RequestManager glideRequestManager = Glide.with(viewHolder.itemView); |
|||
|
|||
feedModel.setPosition(position); |
|||
|
|||
viewHolder.viewPost.setTag(feedModel); |
|||
viewHolder.profilePic.setTag(feedModel); |
|||
viewHolder.btnDownload.setTag(feedModel); |
|||
viewHolder.viewerCaption.setTag(feedModel); |
|||
|
|||
final ProfileModel profileModel = feedModel.getProfileModel(); |
|||
if (profileModel != null) { |
|||
glideRequestManager.load(profileModel.getSdProfilePic()).into(viewHolder.profilePic); |
|||
final int titleLen = profileModel.getUsername().length() + 1; |
|||
final SpannableString spannableString = new SpannableString("@"+profileModel.getUsername()); |
|||
spannableString.setSpan(new CommentMentionClickSpan(), 0, titleLen, 0); |
|||
viewHolder.username.setText(spannableString); |
|||
viewHolder.username.setMovementMethod(new LinkMovementMethod()); |
|||
viewHolder.username.setMentionClickListener((view, text, isHashtag) -> |
|||
mentionClickListener.onClick(null, profileModel.getUsername(), false)); |
|||
final Context context = parent.getContext(); |
|||
final LayoutInflater layoutInflater = LayoutInflater.from(context); |
|||
final MediaItemType type = MediaItemType.valueOf(viewType); |
|||
switch (type) { |
|||
case MEDIA_TYPE_VIDEO: { |
|||
final ItemFeedVideoBinding binding = ItemFeedVideoBinding.inflate(layoutInflater, parent, false); |
|||
return new FeedVideoViewHolder(binding, mentionClickListener, clickListener, longClickListener); |
|||
} |
|||
|
|||
viewHolder.viewPost.setOnClickListener(clickListener); |
|||
viewHolder.profilePic.setOnClickListener(clickListener); |
|||
viewHolder.btnDownload.setOnClickListener(clickListener); |
|||
|
|||
viewHolder.tvPostDate.setText(feedModel.getPostDate()); |
|||
|
|||
final long commentsCount = feedModel.getCommentsCount(); |
|||
viewHolder.commentsCount.setText(String.valueOf(commentsCount)); |
|||
|
|||
viewHolder.btnComments.setTag(feedModel); |
|||
viewHolder.btnComments.setOnClickListener(clickListener); |
|||
viewHolder.btnComments.setEnabled(true); |
|||
|
|||
final JSONObject location = feedModel.getLocation(); |
|||
|
|||
if (location == null) { |
|||
viewHolder.location.setVisibility(View.GONE); |
|||
viewHolder.username.setLayoutParams(new RelativeLayout.LayoutParams( |
|||
RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT |
|||
)); |
|||
} |
|||
else { |
|||
viewHolder.location.setVisibility(View.VISIBLE); |
|||
viewHolder.location.setText(location.optString("name")); |
|||
viewHolder.username.setLayoutParams(new RelativeLayout.LayoutParams( |
|||
RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT |
|||
)); |
|||
viewHolder.location.setOnClickListener(v -> |
|||
new AlertDialog.Builder(v.getContext()).setTitle(location.optString("name")) |
|||
.setMessage(R.string.comment_view_mention_location_search) |
|||
.setNegativeButton(R.string.cancel, null).setPositiveButton(R.string.ok, |
|||
(dialog, which) -> mentionClickListener.onClick(null, location.optString("id")+"/"+location.optString("slug"), false)).show() |
|||
); |
|||
} |
|||
|
|||
final String thumbnailUrl = feedModel.getThumbnailUrl(); |
|||
final String displayUrl = feedModel.getDisplayUrl(); |
|||
CharSequence postCaption = feedModel.getPostCaption(); |
|||
|
|||
final boolean captionEmpty = Utils.isEmpty(postCaption); |
|||
|
|||
viewHolder.viewerCaption.setOnClickListener(clickListener); |
|||
viewHolder.viewerCaption.setOnLongClickListener(longClickListener); |
|||
viewHolder.viewerCaption.setVisibility(captionEmpty ? View.GONE : View.VISIBLE); |
|||
|
|||
if (!captionEmpty && Utils.hasMentions(postCaption)) { |
|||
postCaption = Utils.getMentionText(postCaption); |
|||
feedModel.setPostCaption(postCaption); |
|||
viewHolder.viewerCaption.setText(postCaption, TextView.BufferType.SPANNABLE); |
|||
viewHolder.viewerCaption.setMentionClickListener(mentionClickListener); |
|||
} else { |
|||
viewHolder.viewerCaption.setText(postCaption); |
|||
} |
|||
|
|||
expandCollapseTextView(viewHolder.viewerCaption, feedModel); |
|||
|
|||
final MediaItemType itemType = feedModel.getItemType(); |
|||
final View viewToChangeHeight; |
|||
|
|||
if (itemType == MediaItemType.MEDIA_TYPE_VIDEO) { |
|||
viewToChangeHeight = viewHolder.playerView; |
|||
final Player player = viewHolder.playerView.getPlayer(); |
|||
if (player != null) { |
|||
final boolean shouldAutoplay = settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS); |
|||
player.setPlayWhenReady(shouldAutoplay); |
|||
} |
|||
viewHolder.videoViewsParent.setVisibility(View.VISIBLE); |
|||
viewHolder.videoViews.setText(String.valueOf(feedModel.getViewCount())); |
|||
} else { |
|||
viewHolder.videoViewsParent.setVisibility(View.GONE); |
|||
viewHolder.btnMute.setVisibility(View.GONE); |
|||
|
|||
if (itemType == MediaItemType.MEDIA_TYPE_SLIDER) { |
|||
viewToChangeHeight = viewHolder.mediaList; |
|||
|
|||
final ViewerPostModel[] sliderItems = feedModel.getSliderItems(); |
|||
final int sliderItemLen = sliderItems != null ? sliderItems.length : 0; |
|||
|
|||
if (sliderItemLen > 0) { |
|||
viewHolder.mediaCounter.setText("1/" + sliderItemLen); |
|||
viewHolder.mediaList.setOffscreenPageLimit(Math.min(5, sliderItemLen)); |
|||
|
|||
final ViewPager.SimpleOnPageChangeListener simpleOnPageChangeListener = new ViewPager.SimpleOnPageChangeListener() { |
|||
private int prevPos = 0; |
|||
|
|||
@Override |
|||
public void onPageSelected(final int position) { |
|||
ViewerPostModel sliderItem = sliderItems[prevPos]; |
|||
if (sliderItem != null) sliderItem.setSelected(false); |
|||
sliderItem = sliderItems[position]; |
|||
if (sliderItem != null) sliderItem.setSelected(true); |
|||
|
|||
View childAt = viewHolder.mediaList.getChildAt(prevPos); |
|||
if (childAt instanceof PlayerView) { |
|||
pagerPlayer = (SimpleExoPlayer) ((PlayerView) childAt).getPlayer(); |
|||
if (pagerPlayer != null) pagerPlayer.setPlayWhenReady(false); |
|||
} |
|||
childAt = viewHolder.mediaList.getChildAt(position); |
|||
if (childAt instanceof PlayerView) { |
|||
pagerPlayer = (SimpleExoPlayer) ((PlayerView) childAt).getPlayer(); |
|||
if (pagerPlayer != null) pagerPlayer.setPlayWhenReady(true); |
|||
} |
|||
prevPos = position; |
|||
viewHolder.mediaCounter.setText((position + 1) + "/" + sliderItemLen); |
|||
} |
|||
}; |
|||
|
|||
//noinspection deprecation |
|||
viewHolder.mediaList.setOnPageChangeListener(simpleOnPageChangeListener); // cause add listeners might add to recycled holders |
|||
|
|||
final View.OnClickListener muteClickListener = v -> { |
|||
Player player = null; |
|||
if (v instanceof PlayerView) player = ((PlayerView) v).getPlayer(); |
|||
else if (v instanceof ImageView || v == viewHolder.btnMute) { |
|||
final int currentItem = viewHolder.mediaList.getCurrentItem(); |
|||
if (currentItem < viewHolder.mediaList.getChildCount()) { |
|||
final View childAt = viewHolder.mediaList.getChildAt(currentItem); |
|||
if (childAt instanceof PlayerView) player = ((PlayerView) childAt).getPlayer(); |
|||
} |
|||
|
|||
} else { |
|||
final Object tag = v.getTag(); |
|||
if (tag instanceof Player) player = (Player) tag; |
|||
} |
|||
|
|||
if (player instanceof SimpleExoPlayer) { |
|||
final SimpleExoPlayer exoPlayer = (SimpleExoPlayer) player; |
|||
final float intVol = exoPlayer.getVolume() == 0f ? 1f : 0f; |
|||
exoPlayer.setVolume(intVol); |
|||
viewHolder.btnMute.setImageResource(intVol == 0f ? R.drawable.mute : R.drawable.vol); |
|||
Utils.sessionVolumeFull = intVol == 1f; |
|||
} |
|||
}; |
|||
|
|||
viewHolder.btnMute.setOnClickListener(muteClickListener); |
|||
viewHolder.mediaList.setAdapter(new ChildMediaItemsAdapter(sliderItems, viewHolder.btnMute, playerChangeListener)); |
|||
} |
|||
} else { |
|||
viewToChangeHeight = viewHolder.imageView; |
|||
String url = displayUrl; |
|||
if (Utils.isEmpty(url)) url = thumbnailUrl; |
|||
glideRequestManager.load(url).into(viewHolder.imageView); |
|||
} |
|||
case MEDIA_TYPE_SLIDER: { |
|||
final ItemFeedSliderBinding binding = ItemFeedSliderBinding.inflate(layoutInflater, parent, false); |
|||
return new FeedSliderViewHolder(binding, mentionClickListener, clickListener, longClickListener); |
|||
} |
|||
|
|||
if (viewToChangeHeight != null) { |
|||
final ViewGroup.LayoutParams layoutParams = viewToChangeHeight.getLayoutParams(); |
|||
layoutParams.height = Utils.displayMetrics.widthPixels + 1; |
|||
viewToChangeHeight.setLayoutParams(layoutParams); |
|||
default: |
|||
case MEDIA_TYPE_IMAGE: { |
|||
final ItemFeedPhotoBinding binding = ItemFeedPhotoBinding.inflate(layoutInflater, parent, false); |
|||
return new FeedPhotoViewHolder(binding, glide, mentionClickListener, clickListener, longClickListener); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public int getItemCount() { |
|||
return feedModels == null ? 0 : feedModels.size(); |
|||
public void onBindViewHolder(@NonNull final FeedItemViewHolder viewHolder, final int position) { |
|||
final FeedModel feedModel = getItem(position); |
|||
if (feedModel == null) { |
|||
return; |
|||
} |
|||
feedModel.setPosition(position); |
|||
viewHolder.bind(feedModel); |
|||
} |
|||
|
|||
@Override |
|||
public int getItemViewType(final int position) { |
|||
if (feedModels != null) return feedModels.get(position).getItemType().ordinal(); |
|||
return MediaItemType.MEDIA_TYPE_IMAGE.ordinal(); |
|||
return getItem(position).getItemType().getId(); |
|||
} |
|||
|
|||
/** |
|||
* expands or collapses {@link RamboTextView} [stg idek why i wrote this documentation] |
|||
* |
|||
* @param textView the {@link RamboTextView} view, to expand and collapse |
|||
* @param feedModel the {@link FeedModel} model to check wether model is collapsed to expanded |
|||
* |
|||
* @return true if expanded/collapsed, false if empty or text size is <= 255 chars |
|||
*/ |
|||
public static boolean expandCollapseTextView(@NonNull final RamboTextView textView, @NonNull final FeedModel feedModel) { |
|||
final CharSequence caption = feedModel.getPostCaption(); |
|||
if (Utils.isEmpty(caption)) return false; |
|||
|
|||
final TextView.BufferType bufferType = caption instanceof Spanned ? TextView.BufferType.SPANNABLE : TextView.BufferType.NORMAL; |
|||
|
|||
if (!feedModel.isCaptionExpanded()) { |
|||
int i = Utils.indexOfChar(caption, '\r', 0); |
|||
if (i == -1) i = Utils.indexOfChar(caption, '\n', 0); |
|||
if (i == -1) i = 255; |
|||
|
|||
final int captionLen = caption.length(); |
|||
final int minTrim = Math.min(255, i); |
|||
if (captionLen <= minTrim) return false; |
|||
|
|||
if (Utils.hasMentions(caption)) |
|||
textView.setText(Utils.getMentionText(caption), TextView.BufferType.SPANNABLE); |
|||
textView.setCaptionIsExpandable(true); |
|||
textView.setCaptionIsExpanded(true); |
|||
} else { |
|||
textView.setText(caption, bufferType); |
|||
textView.setCaptionIsExpanded(false); |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
private interface PlayerChangeListener { |
|||
void playerChanged(final int childPos, final SimpleExoPlayer player); |
|||
@Override |
|||
public void onViewAttachedToWindow(@NonNull final FeedItemViewHolder holder) { |
|||
super.onViewAttachedToWindow(holder); |
|||
// Log.d(TAG, "attached holder: " + holder); |
|||
if (!(holder instanceof FeedSliderViewHolder)) return; |
|||
final FeedSliderViewHolder feedSliderViewHolder = (FeedSliderViewHolder) holder; |
|||
feedSliderViewHolder.startPlayingVideo(); |
|||
} |
|||
|
|||
private static final class ChildMediaItemsAdapter extends PagerAdapter { |
|||
private final PlayerChangeListener playerChangeListener; |
|||
private final ViewerPostModel[] sliderItems; |
|||
private final View btnMute; |
|||
private SimpleExoPlayer player; |
|||
|
|||
private ChildMediaItemsAdapter(final ViewerPostModel[] sliderItems, final View btnMute, |
|||
final PlayerChangeListener playerChangeListener) { |
|||
this.sliderItems = sliderItems; |
|||
this.btnMute = btnMute; |
|||
if (BuildConfig.DEBUG) this.playerChangeListener = playerChangeListener; |
|||
else this.playerChangeListener = null; |
|||
} |
|||
|
|||
@NonNull |
|||
@Override |
|||
public Object instantiateItem(@NonNull final ViewGroup container, final int position) { |
|||
final Context context = container.getContext(); |
|||
final ViewerPostModel sliderItem = sliderItems[position]; |
|||
|
|||
if (sliderItem.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { |
|||
if (btnMute != null) btnMute.setVisibility(View.VISIBLE); |
|||
final PlayerView playerView = new PlayerView(context); |
|||
|
|||
player = new SimpleExoPlayer.Builder(context).build(); |
|||
playerView.setPlayer(player); |
|||
|
|||
float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f; |
|||
if (vol == 0f && Utils.sessionVolumeFull) vol = 1f; |
|||
player.setVolume(vol); |
|||
player.setPlayWhenReady(Utils.settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS)); |
|||
|
|||
final ProgressiveMediaSource mediaSource = new ProgressiveMediaSource.Factory(new DefaultDataSourceFactory(context, "instagram")) |
|||
.createMediaSource(Uri.parse(sliderItem.getDisplayUrl())); |
|||
|
|||
player.setRepeatMode(Player.REPEAT_MODE_ALL); |
|||
player.prepare(mediaSource); |
|||
player.setVolume(vol); |
|||
|
|||
playerView.setTag(player); |
|||
|
|||
if (playerChangeListener != null) { |
|||
//todo |
|||
// playerChangeListener.playerChanged(position, player); |
|||
Log.d("AWAISKING_APP", "playerChangeListener: " + playerChangeListener); |
|||
} |
|||
|
|||
container.addView(playerView); |
|||
return playerView; |
|||
} else { |
|||
if (btnMute != null) btnMute.setVisibility(View.GONE); |
|||
|
|||
final PhotoView photoView = new PhotoView(context); |
|||
photoView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); |
|||
Glide.with(context).load(sliderItem.getDisplayUrl()).into(photoView); |
|||
container.addView(photoView); |
|||
return photoView; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void destroyItem(@NonNull final ViewGroup container, final int position, @NonNull final Object object) { |
|||
final Player player = object instanceof PlayerView ? ((PlayerView) object).getPlayer() : this.player; |
|||
|
|||
if (player == this.player && this.player != null) { |
|||
this.player.stop(true); |
|||
this.player.release(); |
|||
} else if (player != null) { |
|||
player.stop(true); |
|||
player.release(); |
|||
} |
|||
|
|||
container.removeView((View) object); |
|||
} |
|||
|
|||
@Override |
|||
public int getCount() { |
|||
return sliderItems != null ? sliderItems.length : 0; |
|||
} |
|||
|
|||
@Override |
|||
public boolean isViewFromObject(@NonNull final View view, @NonNull final Object object) { |
|||
return view == object; |
|||
} |
|||
@Override |
|||
public void onViewDetachedFromWindow(@NonNull final FeedItemViewHolder holder) { |
|||
super.onViewDetachedFromWindow(holder); |
|||
// Log.d(TAG, "detached holder: " + holder); |
|||
if (!(holder instanceof FeedSliderViewHolder)) return; |
|||
final FeedSliderViewHolder feedSliderViewHolder = (FeedSliderViewHolder) holder; |
|||
feedSliderViewHolder.stopPlayingVideo(); |
|||
} |
|||
} |
@ -1,53 +0,0 @@ |
|||
package awais.instagrabber.adapters.viewholder; |
|||
|
|||
import android.view.View; |
|||
import android.widget.ImageView; |
|||
import android.widget.TextView; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.recyclerview.widget.RecyclerView; |
|||
import androidx.viewpager.widget.ViewPager; |
|||
|
|||
import com.github.chrisbanes.photoview.PhotoView; |
|||
import com.google.android.exoplayer2.ui.PlayerView; |
|||
|
|||
import awais.instagrabber.R; |
|||
import awais.instagrabber.customviews.RamboTextView; |
|||
|
|||
public final class FeedItemViewHolder extends RecyclerView.ViewHolder { |
|||
public final ImageView profilePic, btnMute, btnDownload; |
|||
public final TextView commentsCount, videoViews, mediaCounter, tvPostDate, location; |
|||
public final RamboTextView username, viewerCaption; |
|||
public final View btnComments, videoViewsParent, viewPost; |
|||
public final ViewPager mediaList; |
|||
public final PhotoView imageView; |
|||
public final PlayerView playerView; |
|||
|
|||
public FeedItemViewHolder(@NonNull final View itemView) { |
|||
super(itemView); |
|||
|
|||
// common |
|||
viewerCaption = itemView.findViewById(R.id.viewerCaption); |
|||
btnDownload = itemView.findViewById(R.id.btnDownload); |
|||
btnComments = itemView.findViewById(R.id.btnComments); |
|||
profilePic = itemView.findViewById(R.id.ivProfilePic); |
|||
tvPostDate = itemView.findViewById(R.id.tvPostDate); |
|||
viewPost = itemView.findViewById(R.id.viewStoryPost); |
|||
username = itemView.findViewById(R.id.title); |
|||
location = itemView.findViewById(R.id.location); |
|||
|
|||
// video view |
|||
btnMute = itemView.findViewById(R.id.btnMute); |
|||
videoViews = itemView.findViewById(R.id.tvVideoViews); |
|||
commentsCount = btnComments.findViewById(R.id.commentsCount); |
|||
videoViewsParent = videoViews != null ? (View) videoViews.getParent() : null; |
|||
|
|||
// slider view |
|||
mediaCounter = itemView.findViewById(R.id.mediaCounter); |
|||
|
|||
// different types |
|||
mediaList = itemView.findViewById(R.id.media_list); |
|||
imageView = itemView.findViewById(R.id.imageViewer); |
|||
playerView = itemView.findViewById(R.id.playerView); |
|||
} |
|||
} |
@ -0,0 +1,153 @@ |
|||
package awais.instagrabber.adapters.viewholder.feed; |
|||
|
|||
import android.text.SpannableString; |
|||
import android.text.Spanned; |
|||
import android.text.method.LinkMovementMethod; |
|||
import android.view.View; |
|||
import android.widget.RelativeLayout; |
|||
import android.widget.TextView; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.appcompat.app.AlertDialog; |
|||
import androidx.recyclerview.widget.RecyclerView; |
|||
|
|||
import com.bumptech.glide.RequestManager; |
|||
|
|||
import org.json.JSONObject; |
|||
|
|||
import awais.instagrabber.R; |
|||
import awais.instagrabber.customviews.CommentMentionClickSpan; |
|||
import awais.instagrabber.customviews.RamboTextView; |
|||
import awais.instagrabber.databinding.ItemFeedBottomBinding; |
|||
import awais.instagrabber.databinding.ItemFeedTopBinding; |
|||
import awais.instagrabber.interfaces.MentionClickListener; |
|||
import awais.instagrabber.models.FeedModel; |
|||
import awais.instagrabber.models.ProfileModel; |
|||
import awais.instagrabber.utils.Utils; |
|||
|
|||
public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder { |
|||
public static final int MAX_CHARS = 255; |
|||
private final ItemFeedTopBinding topBinding; |
|||
private final ItemFeedBottomBinding bottomBinding; |
|||
private final MentionClickListener mentionClickListener; |
|||
|
|||
boolean captionExpanded = false; |
|||
|
|||
public FeedItemViewHolder(@NonNull final View root, |
|||
final ItemFeedTopBinding topBinding, |
|||
final ItemFeedBottomBinding bottomBinding, |
|||
final MentionClickListener mentionClickListener, |
|||
final View.OnClickListener clickListener, |
|||
final View.OnLongClickListener longClickListener) { |
|||
super(root); |
|||
this.topBinding = topBinding; |
|||
this.bottomBinding = bottomBinding; |
|||
this.mentionClickListener = mentionClickListener; |
|||
topBinding.title.setMovementMethod(new LinkMovementMethod()); |
|||
bottomBinding.btnComments.setOnClickListener(clickListener); |
|||
topBinding.viewStoryPost.setOnClickListener(clickListener); |
|||
topBinding.ivProfilePic.setOnClickListener(clickListener); |
|||
bottomBinding.btnDownload.setOnClickListener(clickListener); |
|||
bottomBinding.viewerCaption.setOnClickListener(clickListener); |
|||
bottomBinding.viewerCaption.setOnLongClickListener(longClickListener); |
|||
bottomBinding.viewerCaption.setMentionClickListener(mentionClickListener); |
|||
} |
|||
|
|||
public void bind(final FeedModel feedModel) { |
|||
if (feedModel == null) { |
|||
return; |
|||
} |
|||
topBinding.viewStoryPost.setTag(feedModel); |
|||
topBinding.ivProfilePic.setTag(feedModel); |
|||
bottomBinding.btnDownload.setTag(feedModel); |
|||
bottomBinding.viewerCaption.setTag(feedModel); |
|||
bottomBinding.btnComments.setTag(feedModel); |
|||
final ProfileModel profileModel = feedModel.getProfileModel(); |
|||
if (profileModel != null) { |
|||
// glide.load(profileModel.getSdProfilePic()) |
|||
// .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) |
|||
// .into(topBinding.ivProfilePic); |
|||
topBinding.ivProfilePic.setImageURI(profileModel.getSdProfilePic()); |
|||
final int titleLen = profileModel.getUsername().length() + 1; |
|||
final SpannableString spannableString = new SpannableString("@" + profileModel.getUsername()); |
|||
spannableString.setSpan(new CommentMentionClickSpan(), 0, titleLen, 0); |
|||
topBinding.title.setText(spannableString); |
|||
topBinding.title.setMentionClickListener((view, text, isHashtag) -> mentionClickListener.onClick(null, profileModel.getUsername(), false)); |
|||
} |
|||
bottomBinding.tvPostDate.setText(feedModel.getPostDate()); |
|||
final long commentsCount = feedModel.getCommentsCount(); |
|||
bottomBinding.commentsCount.setText(String.valueOf(commentsCount)); |
|||
|
|||
final JSONObject location = feedModel.getLocation(); |
|||
setLocation(location); |
|||
CharSequence postCaption = feedModel.getPostCaption(); |
|||
final boolean captionEmpty = Utils.isEmpty(postCaption); |
|||
bottomBinding.viewerCaption.setVisibility(captionEmpty ? View.GONE : View.VISIBLE); |
|||
if (!captionEmpty) { |
|||
if (Utils.hasMentions(postCaption)) { |
|||
postCaption = Utils.getMentionText(postCaption); |
|||
feedModel.setPostCaption(postCaption); |
|||
bottomBinding.viewerCaption.setText(postCaption, TextView.BufferType.SPANNABLE); |
|||
} else { |
|||
bottomBinding.viewerCaption.setText(postCaption); |
|||
} |
|||
} |
|||
expandCollapseTextView(bottomBinding.viewerCaption, feedModel.getPostCaption()); |
|||
bindItem(feedModel); |
|||
} |
|||
|
|||
private void setLocation(final JSONObject location) { |
|||
if (location == null) { |
|||
topBinding.location.setVisibility(View.GONE); |
|||
topBinding.title.setLayoutParams(new RelativeLayout.LayoutParams( |
|||
RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT |
|||
)); |
|||
} else { |
|||
topBinding.location.setVisibility(View.VISIBLE); |
|||
topBinding.location.setText(location.optString("name")); |
|||
topBinding.title.setLayoutParams(new RelativeLayout.LayoutParams( |
|||
RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT |
|||
)); |
|||
topBinding.location.setOnClickListener(v -> { |
|||
new AlertDialog.Builder(v.getContext()).setTitle(location.optString("name")) |
|||
.setMessage(R.string.comment_view_mention_location_search) |
|||
.setNegativeButton(R.string.cancel, null).setPositiveButton(R.string.ok, |
|||
(dialog, which) -> mentionClickListener.onClick(null, location.optString("id") + "/" + location.optString("slug"), false)).show(); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* expands or collapses {@link RamboTextView} [stg idek why i wrote this documentation] |
|||
* |
|||
* @param textView the {@link RamboTextView} view, to expand and collapse |
|||
* @param caption |
|||
* @return isExpanded |
|||
*/ |
|||
public static boolean expandCollapseTextView(@NonNull final RamboTextView textView, final CharSequence caption) { |
|||
if (Utils.isEmpty(caption)) return false; |
|||
|
|||
final TextView.BufferType bufferType = caption instanceof Spanned ? TextView.BufferType.SPANNABLE : TextView.BufferType.NORMAL; |
|||
|
|||
if (!textView.isCaptionExpanded()) { |
|||
int i = Utils.indexOfChar(caption, '\r', 0); |
|||
if (i == -1) i = Utils.indexOfChar(caption, '\n', 0); |
|||
if (i == -1) i = MAX_CHARS; |
|||
|
|||
final int captionLen = caption.length(); |
|||
final int minTrim = Math.min(MAX_CHARS, i); |
|||
if (captionLen <= minTrim) return false; |
|||
|
|||
if (Utils.hasMentions(caption)) |
|||
textView.setText(Utils.getMentionText(caption), TextView.BufferType.SPANNABLE); |
|||
textView.setCaptionIsExpandable(true); |
|||
textView.setCaptionIsExpanded(true); |
|||
} else { |
|||
textView.setText(caption, bufferType); |
|||
textView.setCaptionIsExpanded(false); |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
public abstract void bindItem(final FeedModel feedModel); |
|||
} |
@ -0,0 +1,87 @@ |
|||
package awais.instagrabber.adapters.viewholder.feed; |
|||
|
|||
import android.graphics.Color; |
|||
import android.graphics.drawable.ColorDrawable; |
|||
import android.net.Uri; |
|||
import android.view.View; |
|||
import android.view.ViewGroup; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
|
|||
import com.bumptech.glide.RequestManager; |
|||
import com.facebook.drawee.backends.pipeline.Fresco; |
|||
import com.facebook.drawee.drawable.ScalingUtils; |
|||
import com.facebook.drawee.generic.GenericDraweeHierarchy; |
|||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; |
|||
import com.facebook.imagepipeline.request.ImageRequest; |
|||
import com.facebook.imagepipeline.request.ImageRequestBuilder; |
|||
|
|||
import awais.instagrabber.databinding.ItemFeedPhotoBinding; |
|||
import awais.instagrabber.interfaces.MentionClickListener; |
|||
import awais.instagrabber.models.FeedModel; |
|||
import awais.instagrabber.utils.Utils; |
|||
|
|||
public class FeedPhotoViewHolder extends FeedItemViewHolder { |
|||
private static final String TAG = "FeedPhotoViewHolder"; |
|||
|
|||
private final ItemFeedPhotoBinding binding; |
|||
private final RequestManager glide; |
|||
private final ColorDrawable drawable; |
|||
// private final PipelineDraweeControllerBuilder controllerBuilder; |
|||
// private final CustomTarget<Bitmap> customTarget; |
|||
|
|||
public FeedPhotoViewHolder(@NonNull final ItemFeedPhotoBinding binding, |
|||
final RequestManager glide, |
|||
final MentionClickListener mentionClickListener, |
|||
final View.OnClickListener clickListener, |
|||
final View.OnLongClickListener longClickListener) { |
|||
super(binding.getRoot(), binding.itemFeedTop, binding.itemFeedBottom, mentionClickListener, clickListener, longClickListener); |
|||
this.binding = binding; |
|||
this.glide = glide; |
|||
binding.itemFeedBottom.videoViewsContainer.setVisibility(View.GONE); |
|||
binding.itemFeedBottom.btnMute.setVisibility(View.GONE); |
|||
drawable = new ColorDrawable(Color.WHITE); |
|||
binding.imageViewer.setAllowTouchInterceptionWhileZoomed(false); |
|||
final GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(itemView.getContext().getResources()) |
|||
.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) |
|||
.build(); |
|||
binding.imageViewer.setHierarchy(hierarchy); |
|||
} |
|||
|
|||
@Override |
|||
public void bindItem(final FeedModel feedModel) { |
|||
// glide.clear(customTarget); |
|||
if (feedModel == null) { |
|||
return; |
|||
} |
|||
final ViewGroup.LayoutParams layoutParams = binding.imageViewer.getLayoutParams(); |
|||
final int requiredWidth = Utils.displayMetrics.widthPixels; |
|||
final int resultingHeight = Utils.getResultingHeight(requiredWidth, feedModel.getImageHeight(), feedModel.getImageWidth()); |
|||
layoutParams.width = requiredWidth; |
|||
layoutParams.height = resultingHeight; |
|||
binding.imageViewer.requestLayout(); |
|||
final String thumbnailUrl = feedModel.getThumbnailUrl(); |
|||
String url = feedModel.getDisplayUrl(); |
|||
if (Utils.isEmpty(url)) url = thumbnailUrl; |
|||
final ImageRequest requestBuilder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(url)) |
|||
.setLocalThumbnailPreviewsEnabled(true) |
|||
.setProgressiveRenderingEnabled(true) |
|||
.build(); |
|||
binding.imageViewer.setController(Fresco.newDraweeControllerBuilder() |
|||
.setImageRequest(requestBuilder) |
|||
.setOldController(binding.imageViewer.getController()) |
|||
.setLowResImageRequest(ImageRequest.fromUri(thumbnailUrl)) |
|||
.build()); |
|||
// binding.imageViewer.setImageURI(url); |
|||
// final RequestBuilder<Bitmap> thumbnailRequestBuilder = glide |
|||
// .asBitmap() |
|||
// .load(thumbnailUrl) |
|||
// .diskCacheStrategy(DiskCacheStrategy.ALL); |
|||
// glide.asBitmap() |
|||
// .load(url) |
|||
// .thumbnail(thumbnailRequestBuilder) |
|||
// .diskCacheStrategy(DiskCacheStrategy.ALL) |
|||
// .into(customTarget); |
|||
|
|||
} |
|||
} |
@ -0,0 +1,339 @@ |
|||
package awais.instagrabber.adapters.viewholder.feed; |
|||
|
|||
import android.content.Context; |
|||
import android.net.Uri; |
|||
import android.view.Gravity; |
|||
import android.view.View; |
|||
import android.view.ViewGroup; |
|||
import android.widget.FrameLayout; |
|||
import android.widget.ViewSwitcher; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.appcompat.widget.AppCompatImageView; |
|||
import androidx.viewpager.widget.PagerAdapter; |
|||
import androidx.viewpager.widget.ViewPager; |
|||
|
|||
import com.facebook.drawee.drawable.ScalingUtils; |
|||
import com.facebook.drawee.generic.GenericDraweeHierarchy; |
|||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; |
|||
import com.facebook.drawee.view.SimpleDraweeView; |
|||
import com.facebook.imagepipeline.request.ImageRequest; |
|||
import com.facebook.imagepipeline.request.ImageRequestBuilder; |
|||
import com.google.android.exoplayer2.Player; |
|||
import com.google.android.exoplayer2.SimpleExoPlayer; |
|||
import com.google.android.exoplayer2.source.ProgressiveMediaSource; |
|||
import com.google.android.exoplayer2.ui.PlayerView; |
|||
import com.google.android.exoplayer2.upstream.DataSource; |
|||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; |
|||
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; |
|||
import com.google.android.exoplayer2.upstream.cache.SimpleCache; |
|||
|
|||
import awais.instagrabber.R; |
|||
import awais.instagrabber.databinding.ItemFeedSliderBinding; |
|||
import awais.instagrabber.interfaces.MentionClickListener; |
|||
import awais.instagrabber.models.FeedModel; |
|||
import awais.instagrabber.models.ViewerPostModel; |
|||
import awais.instagrabber.models.enums.MediaItemType; |
|||
import awais.instagrabber.utils.Constants; |
|||
import awais.instagrabber.utils.Utils; |
|||
|
|||
import static awais.instagrabber.utils.Utils.settingsHelper; |
|||
|
|||
public class FeedSliderViewHolder extends FeedItemViewHolder { |
|||
private static final String TAG = "FeedSliderViewHolder"; |
|||
private static final boolean shouldAutoPlay = settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS); |
|||
|
|||
private final ItemFeedSliderBinding binding; |
|||
private final DefaultDataSourceFactory dataSourceFactory; |
|||
|
|||
private final PlayerChangeListener playerChangeListener = (position, player) -> { |
|||
pagerPlayer = player; |
|||
playerPosition = position; |
|||
}; |
|||
|
|||
private CacheDataSourceFactory cacheDataSourceFactory; |
|||
private SimpleExoPlayer pagerPlayer; |
|||
private int playerPosition = 0; |
|||
|
|||
public FeedSliderViewHolder(@NonNull final ItemFeedSliderBinding binding, |
|||
final MentionClickListener mentionClickListener, |
|||
final View.OnClickListener clickListener, |
|||
final View.OnLongClickListener longClickListener) { |
|||
super(binding.getRoot(), binding.itemFeedTop, binding.itemFeedBottom, mentionClickListener, clickListener, longClickListener); |
|||
this.binding = binding; |
|||
binding.itemFeedBottom.videoViewsContainer.setVisibility(View.GONE); |
|||
binding.itemFeedBottom.btnMute.setVisibility(View.GONE); |
|||
final ViewGroup.LayoutParams layoutParams = binding.mediaList.getLayoutParams(); |
|||
layoutParams.height = Utils.displayMetrics.widthPixels + 1; |
|||
binding.mediaList.setLayoutParams(layoutParams); |
|||
final Context context = binding.getRoot().getContext(); |
|||
dataSourceFactory = new DefaultDataSourceFactory(context, "instagram"); |
|||
final SimpleCache simpleCache = Utils.getSimpleCacheInstance(context); |
|||
if (simpleCache != null) { |
|||
cacheDataSourceFactory = new CacheDataSourceFactory(simpleCache, dataSourceFactory); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void bindItem(final FeedModel feedModel) { |
|||
final ViewerPostModel[] sliderItems = feedModel.getSliderItems(); |
|||
final int sliderItemLen = sliderItems != null ? sliderItems.length : 0; |
|||
if (sliderItemLen <= 0) { |
|||
return; |
|||
} |
|||
final String text = "1/" + sliderItemLen; |
|||
binding.mediaCounter.setText(text); |
|||
binding.mediaList.setOffscreenPageLimit(Math.min(5, sliderItemLen)); |
|||
|
|||
final PagerAdapter adapter = binding.mediaList.getAdapter(); |
|||
if (adapter != null) { |
|||
final int count = adapter.getCount(); |
|||
for (int i = 0; i < count; i++) { |
|||
adapter.destroyItem(binding.mediaList, i, binding.mediaList.getChildAt(i)); |
|||
} |
|||
} |
|||
final ChildMediaItemsAdapter itemsAdapter = new ChildMediaItemsAdapter(sliderItems, |
|||
cacheDataSourceFactory != null ? cacheDataSourceFactory : dataSourceFactory, |
|||
playerChangeListener); |
|||
binding.mediaList.setAdapter(itemsAdapter); |
|||
|
|||
//noinspection deprecation |
|||
binding.mediaList.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { |
|||
private int prevPos = 0; |
|||
|
|||
@Override |
|||
public void onPageSelected(final int position) { |
|||
ViewerPostModel sliderItem = sliderItems[prevPos]; |
|||
if (sliderItem != null) { |
|||
sliderItem.setSelected(false); |
|||
if (sliderItem.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { |
|||
// stop playing prev video |
|||
final ViewSwitcher prevChild = (ViewSwitcher) binding.mediaList.getChildAt(prevPos); |
|||
if (prevChild == null || prevChild.getTag() == null || !(prevChild.getTag() instanceof SimpleExoPlayer)) { |
|||
return; |
|||
} |
|||
((SimpleExoPlayer) prevChild.getTag()).setPlayWhenReady(false); |
|||
} |
|||
} |
|||
sliderItem = sliderItems[position]; |
|||
if (sliderItem == null) return; |
|||
sliderItem.setSelected(true); |
|||
final String text = (position + 1) + "/" + sliderItemLen; |
|||
binding.mediaCounter.setText(text); |
|||
prevPos = position; |
|||
if (sliderItem.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { |
|||
binding.itemFeedBottom.btnMute.setVisibility(View.VISIBLE); |
|||
if (shouldAutoPlay) { |
|||
autoPlay(position); |
|||
} |
|||
} else binding.itemFeedBottom.btnMute.setVisibility(View.GONE); |
|||
} |
|||
}); |
|||
|
|||
final View.OnClickListener muteClickListener = v -> { |
|||
final int currentItem = binding.mediaList.getCurrentItem(); |
|||
if (currentItem < 0 || currentItem >= binding.mediaList.getChildCount()) { |
|||
return; |
|||
} |
|||
final ViewerPostModel sliderItem = sliderItems[currentItem]; |
|||
if (sliderItem.getItemType() != MediaItemType.MEDIA_TYPE_VIDEO) { |
|||
return; |
|||
} |
|||
final View currentView = binding.mediaList.getChildAt(currentItem); |
|||
if (!(currentView instanceof ViewSwitcher)) { |
|||
return; |
|||
} |
|||
final ViewSwitcher viewSwitcher = (ViewSwitcher) currentView; |
|||
final Object tag = viewSwitcher.getTag(); |
|||
if (!(tag instanceof SimpleExoPlayer)) { |
|||
return; |
|||
} |
|||
final SimpleExoPlayer player = (SimpleExoPlayer) tag; |
|||
final float intVol = player.getVolume() == 0f ? 1f : 0f; |
|||
player.setVolume(intVol); |
|||
binding.itemFeedBottom.btnMute.setImageResource(intVol == 0f ? R.drawable.vol : R.drawable.mute); |
|||
Utils.sessionVolumeFull = intVol == 1f; |
|||
}; |
|||
final ViewerPostModel firstItem = sliderItems[0]; |
|||
if (firstItem.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { |
|||
binding.itemFeedBottom.btnMute.setVisibility(View.VISIBLE); |
|||
} |
|||
binding.itemFeedBottom.btnMute.setImageResource(Utils.sessionVolumeFull ? R.drawable.mute : R.drawable.vol); |
|||
binding.itemFeedBottom.btnMute.setOnClickListener(muteClickListener); |
|||
} |
|||
|
|||
private void autoPlay(final int position) { |
|||
if (!shouldAutoPlay) { |
|||
return; |
|||
} |
|||
final ChildMediaItemsAdapter adapter = (ChildMediaItemsAdapter) binding.mediaList.getAdapter(); |
|||
if (adapter == null) { |
|||
return; |
|||
} |
|||
final ViewerPostModel sliderItem = adapter.getItemAtPosition(position); |
|||
if (sliderItem.getItemType() != MediaItemType.MEDIA_TYPE_VIDEO) { |
|||
return; |
|||
} |
|||
final ViewSwitcher viewSwitcher = (ViewSwitcher) binding.mediaList.getChildAt(position); |
|||
loadPlayer(binding.getRoot().getContext(), |
|||
position, sliderItem.getDisplayUrl(), |
|||
viewSwitcher, |
|||
cacheDataSourceFactory != null ? cacheDataSourceFactory : dataSourceFactory, |
|||
playerChangeListener); |
|||
} |
|||
|
|||
public void startPlayingVideo() { |
|||
autoPlay(playerPosition); |
|||
} |
|||
|
|||
public void stopPlayingVideo() { |
|||
if (pagerPlayer == null) { |
|||
return; |
|||
} |
|||
pagerPlayer.setPlayWhenReady(false); |
|||
} |
|||
|
|||
private interface PlayerChangeListener { |
|||
void playerChanged(final int position, final SimpleExoPlayer player); |
|||
} |
|||
|
|||
private static void loadPlayer(final Context context, |
|||
final int position, final String displayUrl, |
|||
final ViewSwitcher viewSwitcher, |
|||
final DataSource.Factory factory, |
|||
final PlayerChangeListener playerChangeListener) { |
|||
if (viewSwitcher == null) { |
|||
return; |
|||
} |
|||
SimpleExoPlayer player = (SimpleExoPlayer) viewSwitcher.getTag(); |
|||
if (player != null) { |
|||
player.setPlayWhenReady(true); |
|||
return; |
|||
} |
|||
player = new SimpleExoPlayer.Builder(context).build(); |
|||
final PlayerView playerView = (PlayerView) viewSwitcher.getChildAt(1); |
|||
playerView.setPlayer(player); |
|||
if (viewSwitcher.getDisplayedChild() == 0) { |
|||
viewSwitcher.showNext(); |
|||
} |
|||
playerView.setControllerShowTimeoutMs(1000); |
|||
float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f; |
|||
if (vol == 0f && Utils.sessionVolumeFull) vol = 1f; |
|||
player.setVolume(vol); |
|||
player.setPlayWhenReady(Utils.settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS)); |
|||
final ProgressiveMediaSource mediaSource = new ProgressiveMediaSource.Factory(factory).createMediaSource(Uri.parse(displayUrl)); |
|||
player.setRepeatMode(Player.REPEAT_MODE_ALL); |
|||
player.prepare(mediaSource); |
|||
player.setVolume(vol); |
|||
playerChangeListener.playerChanged(position, player); |
|||
viewSwitcher.setTag(player); |
|||
} |
|||
|
|||
private static final class ChildMediaItemsAdapter extends PagerAdapter { |
|||
// private static final String TAG = "ChildMediaItemsAdapter"; |
|||
|
|||
private final ViewerPostModel[] sliderItems; |
|||
private final DataSource.Factory factory; |
|||
private final PlayerChangeListener playerChangeListener; |
|||
private final ViewGroup.LayoutParams layoutParams; |
|||
|
|||
private ChildMediaItemsAdapter(final ViewerPostModel[] sliderItems, |
|||
final DataSource.Factory factory, |
|||
final PlayerChangeListener playerChangeListener) { |
|||
this.sliderItems = sliderItems; |
|||
this.factory = factory; |
|||
this.playerChangeListener = playerChangeListener; |
|||
layoutParams = new ViewGroup.LayoutParams(Utils.displayMetrics.widthPixels, Utils.displayMetrics.widthPixels + 1); |
|||
} |
|||
|
|||
@NonNull |
|||
@Override |
|||
public Object instantiateItem(@NonNull final ViewGroup container, final int position) { |
|||
final Context context = container.getContext(); |
|||
final ViewerPostModel sliderItem = sliderItems[position]; |
|||
|
|||
final String displayUrl = sliderItem.getDisplayUrl(); |
|||
if (sliderItem.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { |
|||
final ViewSwitcher viewSwitcher = createViewSwitcher(context, position, sliderItem.getSliderDisplayUrl(), displayUrl); |
|||
container.addView(viewSwitcher); |
|||
return viewSwitcher; |
|||
} |
|||
final GenericDraweeHierarchy hierarchy = GenericDraweeHierarchyBuilder.newInstance(container.getResources()) |
|||
.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) |
|||
.build(); |
|||
final SimpleDraweeView photoView = new SimpleDraweeView(context, hierarchy); |
|||
photoView.setLayoutParams(layoutParams); |
|||
final ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(displayUrl)) |
|||
.setLocalThumbnailPreviewsEnabled(true) |
|||
.setProgressiveRenderingEnabled(true) |
|||
.build(); |
|||
photoView.setImageRequest(imageRequest); |
|||
container.addView(photoView); |
|||
return photoView; |
|||
} |
|||
|
|||
@NonNull |
|||
private ViewSwitcher createViewSwitcher(final Context context, final int position, final String sliderDisplayUrl, final String displayUrl) { |
|||
|
|||
final ViewSwitcher viewSwitcher = new ViewSwitcher(context); |
|||
viewSwitcher.setLayoutParams(layoutParams); |
|||
|
|||
final FrameLayout frameLayout = new FrameLayout(context); |
|||
frameLayout.setLayoutParams(layoutParams); |
|||
|
|||
final GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(context.getResources()) |
|||
.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) |
|||
.build(); |
|||
final SimpleDraweeView simpleDraweeView = new SimpleDraweeView(context, hierarchy); |
|||
simpleDraweeView.setLayoutParams(layoutParams); |
|||
simpleDraweeView.setImageURI(sliderDisplayUrl); |
|||
frameLayout.addView(simpleDraweeView); |
|||
|
|||
final AppCompatImageView imageView = new AppCompatImageView(context); |
|||
final int px = Utils.convertDpToPx(50); |
|||
final FrameLayout.LayoutParams playButtonLayoutParams = new FrameLayout.LayoutParams(px, px); |
|||
playButtonLayoutParams.gravity = Gravity.CENTER; |
|||
imageView.setLayoutParams(playButtonLayoutParams); |
|||
imageView.setImageResource(R.drawable.exo_icon_play); |
|||
frameLayout.addView(imageView); |
|||
|
|||
viewSwitcher.addView(frameLayout); |
|||
|
|||
final PlayerView playerView = new PlayerView(context); |
|||
viewSwitcher.addView(playerView); |
|||
if (shouldAutoPlay && position == 0) { |
|||
loadPlayer(context, position, displayUrl, viewSwitcher, factory, playerChangeListener); |
|||
} else |
|||
frameLayout.setOnClickListener(v -> loadPlayer(context, position, displayUrl, viewSwitcher, factory, playerChangeListener)); |
|||
return viewSwitcher; |
|||
} |
|||
|
|||
@Override |
|||
public void destroyItem(@NonNull final ViewGroup container, final int position, @NonNull final Object object) { |
|||
final View view = container.getChildAt(position); |
|||
// Log.d(TAG, "destroy position: " + position + ", view: " + view); |
|||
if (view instanceof ViewSwitcher) { |
|||
final Object tag = view.getTag(); |
|||
if (tag instanceof SimpleExoPlayer) { |
|||
final SimpleExoPlayer player = (SimpleExoPlayer) tag; |
|||
player.release(); |
|||
} |
|||
} |
|||
container.removeView((View) object); |
|||
} |
|||
|
|||
@Override |
|||
public int getCount() { |
|||
return sliderItems != null ? sliderItems.length : 0; |
|||
} |
|||
|
|||
@Override |
|||
public boolean isViewFromObject(@NonNull final View view, @NonNull final Object object) { |
|||
return view == object; |
|||
} |
|||
|
|||
public ViewerPostModel getItemAtPosition(final int position) { |
|||
return sliderItems[0]; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,156 @@ |
|||
package awais.instagrabber.adapters.viewholder.feed; |
|||
|
|||
import android.content.Context; |
|||
import android.net.Uri; |
|||
import android.os.Handler; |
|||
import android.os.Looper; |
|||
import android.view.View; |
|||
import android.view.ViewGroup; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
|
|||
import com.facebook.drawee.backends.pipeline.Fresco; |
|||
import com.facebook.drawee.interfaces.DraweeController; |
|||
import com.facebook.imagepipeline.request.ImageRequest; |
|||
import com.facebook.imagepipeline.request.ImageRequestBuilder; |
|||
import com.google.android.exoplayer2.Player; |
|||
import com.google.android.exoplayer2.SimpleExoPlayer; |
|||
import com.google.android.exoplayer2.source.ProgressiveMediaSource; |
|||
import com.google.android.exoplayer2.upstream.DataSource; |
|||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; |
|||
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; |
|||
import com.google.android.exoplayer2.upstream.cache.SimpleCache; |
|||
|
|||
import awais.instagrabber.R; |
|||
import awais.instagrabber.databinding.ItemFeedVideoBinding; |
|||
import awais.instagrabber.interfaces.MentionClickListener; |
|||
import awais.instagrabber.models.FeedModel; |
|||
import awais.instagrabber.utils.Constants; |
|||
import awais.instagrabber.utils.Utils; |
|||
|
|||
import static awais.instagrabber.utils.Utils.settingsHelper; |
|||
|
|||
public class FeedVideoViewHolder extends FeedItemViewHolder { |
|||
private static final String TAG = "FeedVideoViewHolder"; |
|||
|
|||
private final ItemFeedVideoBinding binding; |
|||
private final Handler handler; |
|||
private final DefaultDataSourceFactory dataSourceFactory; |
|||
|
|||
private CacheDataSourceFactory cacheDataSourceFactory; |
|||
private FeedModel feedModel; |
|||
private SimpleExoPlayer player; |
|||
|
|||
final Runnable loadRunnable = new Runnable() { |
|||
@Override |
|||
public void run() { |
|||
loadPlayer(feedModel); |
|||
} |
|||
}; |
|||
|
|||
public FeedVideoViewHolder(@NonNull final ItemFeedVideoBinding binding, |
|||
final MentionClickListener mentionClickListener, |
|||
final View.OnClickListener clickListener, |
|||
final View.OnLongClickListener longClickListener) { |
|||
super(binding.getRoot(), binding.itemFeedTop, binding.itemFeedBottom, mentionClickListener, clickListener, longClickListener); |
|||
this.binding = binding; |
|||
binding.itemFeedBottom.videoViewsContainer.setVisibility(View.VISIBLE); |
|||
handler = new Handler(Looper.getMainLooper()); |
|||
final Context context = binding.getRoot().getContext(); |
|||
dataSourceFactory = new DefaultDataSourceFactory(context, "instagram"); |
|||
final SimpleCache simpleCache = Utils.getSimpleCacheInstance(context); |
|||
if (simpleCache != null) { |
|||
cacheDataSourceFactory = new CacheDataSourceFactory(simpleCache, dataSourceFactory); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void bindItem(final FeedModel feedModel) { |
|||
// Log.d(TAG, "Binding post: " + feedModel.getPostId()); |
|||
this.feedModel = feedModel; |
|||
setThumbnail(feedModel); |
|||
binding.itemFeedBottom.tvVideoViews.setText(String.valueOf(feedModel.getViewCount())); |
|||
} |
|||
|
|||
private void setThumbnail(final FeedModel feedModel) { |
|||
final ViewGroup.LayoutParams layoutParams = binding.thumbnailParent.getLayoutParams(); |
|||
layoutParams.width = feedModel.getImageWidth(); |
|||
layoutParams.height = feedModel.getImageHeight(); |
|||
binding.thumbnailParent.requestLayout(); |
|||
final ImageRequest thumbnailRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(feedModel.getThumbnailUrl())) |
|||
.setProgressiveRenderingEnabled(true) |
|||
.build(); |
|||
final DraweeController controller = Fresco.newDraweeControllerBuilder() |
|||
.setImageRequest(thumbnailRequest) |
|||
.build(); |
|||
binding.thumbnail.setController(controller); |
|||
binding.thumbnailParent.setOnClickListener(v -> loadPlayer(feedModel)); |
|||
} |
|||
|
|||
private void loadPlayer(final FeedModel feedModel) { |
|||
if (feedModel == null) { |
|||
return; |
|||
} |
|||
// Log.d(TAG, "playing post:" + feedModel.getPostId()); |
|||
if (binding.viewSwitcher.getDisplayedChild() == 0) { |
|||
binding.viewSwitcher.showNext(); |
|||
} |
|||
binding.itemFeedBottom.btnMute.setVisibility(View.VISIBLE); |
|||
final ViewGroup.LayoutParams layoutParams = binding.playerView.getLayoutParams(); |
|||
final int requiredWidth = Utils.displayMetrics.widthPixels; |
|||
final int resultingHeight = Utils.getResultingHeight(requiredWidth, feedModel.getImageHeight(), feedModel.getImageWidth()); |
|||
layoutParams.width = requiredWidth; |
|||
layoutParams.height = resultingHeight; |
|||
binding.playerView.requestLayout(); |
|||
float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f; |
|||
if (vol == 0f && Utils.sessionVolumeFull) vol = 1f; |
|||
setMuteIcon(vol); |
|||
player = (SimpleExoPlayer) binding.playerView.getPlayer(); |
|||
if (player != null) { |
|||
player.release(); |
|||
} |
|||
player = new SimpleExoPlayer.Builder(itemView.getContext()) |
|||
.setLooper(Looper.getMainLooper()) |
|||
.build(); |
|||
player.setVolume(vol); |
|||
player.setPlayWhenReady(true); |
|||
final DataSource.Factory factory = cacheDataSourceFactory != null ? cacheDataSourceFactory : dataSourceFactory; |
|||
final ProgressiveMediaSource.Factory sourceFactory = new ProgressiveMediaSource.Factory(factory); |
|||
final ProgressiveMediaSource mediaSource = sourceFactory.createMediaSource(Uri.parse(feedModel.getDisplayUrl())); |
|||
player.setRepeatMode(Player.REPEAT_MODE_ALL); |
|||
player.prepare(mediaSource); |
|||
binding.playerView.setPlayer(player); |
|||
final SimpleExoPlayer finalPlayer = player; |
|||
binding.itemFeedBottom.btnMute.setOnClickListener(v -> { |
|||
final float intVol = finalPlayer.getVolume() == 0f ? 1f : 0f; |
|||
finalPlayer.setVolume(intVol); |
|||
setMuteIcon(intVol); |
|||
Utils.sessionVolumeFull = intVol == 1f; |
|||
}); |
|||
binding.playerView.setOnClickListener(v -> finalPlayer.setPlayWhenReady(!finalPlayer.getPlayWhenReady())); |
|||
} |
|||
|
|||
private void setMuteIcon(final float vol) { |
|||
binding.itemFeedBottom.btnMute.setImageResource(vol == 0f ? R.drawable.vol : R.drawable.mute); |
|||
} |
|||
|
|||
public FeedModel getCurrentFeedModel() { |
|||
return feedModel; |
|||
} |
|||
|
|||
public void stopPlaying() { |
|||
// Log.d(TAG, "Stopping post: " + feedModel.getPostId() + ", player: " + player + ", player.isPlaying: " + (player != null && player.isPlaying())); |
|||
handler.removeCallbacks(loadRunnable); |
|||
if (player != null) { |
|||
player.release(); |
|||
} |
|||
if (binding.viewSwitcher.getDisplayedChild() == 1) { |
|||
binding.viewSwitcher.showPrevious(); |
|||
} |
|||
} |
|||
|
|||
public void startPlaying() { |
|||
handler.removeCallbacks(loadRunnable); |
|||
handler.postDelayed(loadRunnable, 800); |
|||
} |
|||
} |
@ -0,0 +1,170 @@ |
|||
/* |
|||
* Copyright (c) Facebook, Inc. and its affiliates. |
|||
* |
|||
* This source code is licensed under the MIT license found in the |
|||
* LICENSE file in the root directory of this source tree. |
|||
*/ |
|||
|
|||
package awais.instagrabber.customviews.drawee; |
|||
|
|||
import android.graphics.Matrix; |
|||
import android.graphics.PointF; |
|||
|
|||
import androidx.annotation.Nullable; |
|||
|
|||
import com.facebook.common.logging.FLog; |
|||
|
|||
/** |
|||
* Abstract class for ZoomableController that adds animation capabilities to |
|||
* DefaultZoomableController. |
|||
*/ |
|||
public abstract class AbstractAnimatedZoomableController extends DefaultZoomableController { |
|||
|
|||
private boolean mIsAnimating; |
|||
private final float[] mStartValues = new float[9]; |
|||
private final float[] mStopValues = new float[9]; |
|||
private final float[] mCurrentValues = new float[9]; |
|||
private final Matrix mNewTransform = new Matrix(); |
|||
private final Matrix mWorkingTransform = new Matrix(); |
|||
|
|||
public AbstractAnimatedZoomableController(TransformGestureDetector transformGestureDetector) { |
|||
super(transformGestureDetector); |
|||
} |
|||
|
|||
@Override |
|||
public void reset() { |
|||
FLog.v(getLogTag(), "reset"); |
|||
stopAnimation(); |
|||
mWorkingTransform.reset(); |
|||
mNewTransform.reset(); |
|||
super.reset(); |
|||
} |
|||
|
|||
/** |
|||
* Returns true if the zoomable transform is identity matrix, and the controller is idle. |
|||
*/ |
|||
@Override |
|||
public boolean isIdentity() { |
|||
return !isAnimating() && super.isIdentity(); |
|||
} |
|||
|
|||
/** |
|||
* Zooms to the desired scale and positions the image so that the given image point corresponds to |
|||
* the given view point. |
|||
* |
|||
* <p>If this method is called while an animation or gesture is already in progress, the current |
|||
* animation or gesture will be stopped first. |
|||
* |
|||
* @param scale desired scale, will be limited to {min, max} scale factor |
|||
* @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1) |
|||
* @param viewPoint 2D point in view's absolute coordinate system |
|||
*/ |
|||
@Override |
|||
public void zoomToPoint(float scale, PointF imagePoint, PointF viewPoint) { |
|||
zoomToPoint(scale, imagePoint, viewPoint, LIMIT_ALL, 0, null); |
|||
} |
|||
|
|||
/** |
|||
* Zooms to the desired scale and positions the image so that the given image point corresponds to |
|||
* the given view point. |
|||
* |
|||
* <p>If this method is called while an animation or gesture is already in progress, the current |
|||
* animation or gesture will be stopped first. |
|||
* |
|||
* @param scale desired scale, will be limited to {min, max} scale factor |
|||
* @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1) |
|||
* @param viewPoint 2D point in view's absolute coordinate system |
|||
* @param limitFlags whether to limit translation and/or scale. |
|||
* @param durationMs length of animation of the zoom, or 0 if no animation desired |
|||
* @param onAnimationComplete code to run when the animation completes. Ignored if durationMs=0 |
|||
*/ |
|||
public void zoomToPoint( |
|||
float scale, |
|||
PointF imagePoint, |
|||
PointF viewPoint, |
|||
@LimitFlag int limitFlags, |
|||
long durationMs, |
|||
@Nullable Runnable onAnimationComplete) { |
|||
FLog.v(getLogTag(), "zoomToPoint: duration %d ms", durationMs); |
|||
calculateZoomToPointTransform(mNewTransform, scale, imagePoint, viewPoint, limitFlags); |
|||
setTransform(mNewTransform, durationMs, onAnimationComplete); |
|||
} |
|||
|
|||
/** |
|||
* Sets a new zoomable transformation and animates to it if desired. |
|||
* |
|||
* <p>If this method is called while an animation or gesture is already in progress, the current |
|||
* animation or gesture will be stopped first. |
|||
* |
|||
* @param newTransform new transform to make active |
|||
* @param durationMs duration of the animation, or 0 to not animate |
|||
* @param onAnimationComplete code to run when the animation completes. Ignored if durationMs=0 |
|||
*/ |
|||
public void setTransform( |
|||
Matrix newTransform, long durationMs, @Nullable Runnable onAnimationComplete) { |
|||
FLog.v(getLogTag(), "setTransform: duration %d ms", durationMs); |
|||
if (durationMs <= 0) { |
|||
setTransformImmediate(newTransform); |
|||
} else { |
|||
setTransformAnimated(newTransform, durationMs, onAnimationComplete); |
|||
} |
|||
} |
|||
|
|||
private void setTransformImmediate(final Matrix newTransform) { |
|||
FLog.v(getLogTag(), "setTransformImmediate"); |
|||
stopAnimation(); |
|||
mWorkingTransform.set(newTransform); |
|||
super.setTransform(newTransform); |
|||
getDetector().restartGesture(); |
|||
} |
|||
|
|||
protected boolean isAnimating() { |
|||
return mIsAnimating; |
|||
} |
|||
|
|||
protected void setAnimating(boolean isAnimating) { |
|||
mIsAnimating = isAnimating; |
|||
} |
|||
|
|||
protected float[] getStartValues() { |
|||
return mStartValues; |
|||
} |
|||
|
|||
protected float[] getStopValues() { |
|||
return mStopValues; |
|||
} |
|||
|
|||
protected Matrix getWorkingTransform() { |
|||
return mWorkingTransform; |
|||
} |
|||
|
|||
@Override |
|||
public void onGestureBegin(TransformGestureDetector detector) { |
|||
FLog.v(getLogTag(), "onGestureBegin"); |
|||
stopAnimation(); |
|||
super.onGestureBegin(detector); |
|||
} |
|||
|
|||
@Override |
|||
public void onGestureUpdate(TransformGestureDetector detector) { |
|||
FLog.v(getLogTag(), "onGestureUpdate %s", isAnimating() ? "(ignored)" : ""); |
|||
if (isAnimating()) { |
|||
return; |
|||
} |
|||
super.onGestureUpdate(detector); |
|||
} |
|||
|
|||
protected void calculateInterpolation(Matrix outMatrix, float fraction) { |
|||
for (int i = 0; i < 9; i++) { |
|||
mCurrentValues[i] = (1 - fraction) * mStartValues[i] + fraction * mStopValues[i]; |
|||
} |
|||
outMatrix.setValues(mCurrentValues); |
|||
} |
|||
|
|||
public abstract void setTransformAnimated( |
|||
final Matrix newTransform, long durationMs, @Nullable final Runnable onAnimationComplete); |
|||
|
|||
protected abstract void stopAnimation(); |
|||
|
|||
protected abstract Class<?> getLogTag(); |
|||
} |
@ -0,0 +1,105 @@ |
|||
/* |
|||
* Copyright (c) Facebook, Inc. and its affiliates. |
|||
* |
|||
* This source code is licensed under the MIT license found in the |
|||
* LICENSE file in the root directory of this source tree. |
|||
*/ |
|||
|
|||
package awais.instagrabber.customviews.drawee; |
|||
|
|||
import android.animation.Animator; |
|||
import android.animation.AnimatorListenerAdapter; |
|||
import android.animation.ValueAnimator; |
|||
import android.annotation.SuppressLint; |
|||
import android.graphics.Matrix; |
|||
import android.view.animation.DecelerateInterpolator; |
|||
|
|||
import androidx.annotation.Nullable; |
|||
|
|||
import com.facebook.common.internal.Preconditions; |
|||
import com.facebook.common.logging.FLog; |
|||
|
|||
|
|||
/** |
|||
* ZoomableController that adds animation capabilities to DefaultZoomableController using standard |
|||
* Android animation classes |
|||
*/ |
|||
public class AnimatedZoomableController extends AbstractAnimatedZoomableController { |
|||
|
|||
private static final Class<?> TAG = AnimatedZoomableController.class; |
|||
|
|||
private final ValueAnimator mValueAnimator; |
|||
|
|||
public static AnimatedZoomableController newInstance() { |
|||
return new AnimatedZoomableController(TransformGestureDetector.newInstance()); |
|||
} |
|||
|
|||
@SuppressLint("NewApi") |
|||
public AnimatedZoomableController(TransformGestureDetector transformGestureDetector) { |
|||
super(transformGestureDetector); |
|||
mValueAnimator = ValueAnimator.ofFloat(0, 1); |
|||
mValueAnimator.setInterpolator(new DecelerateInterpolator()); |
|||
} |
|||
|
|||
@SuppressLint("NewApi") |
|||
@Override |
|||
public void setTransformAnimated( |
|||
final Matrix newTransform, long durationMs, @Nullable final Runnable onAnimationComplete) { |
|||
FLog.v(getLogTag(), "setTransformAnimated: duration %d ms", durationMs); |
|||
stopAnimation(); |
|||
Preconditions.checkArgument(durationMs > 0); |
|||
Preconditions.checkState(!isAnimating()); |
|||
setAnimating(true); |
|||
mValueAnimator.setDuration(durationMs); |
|||
getTransform().getValues(getStartValues()); |
|||
newTransform.getValues(getStopValues()); |
|||
mValueAnimator.addUpdateListener( |
|||
new ValueAnimator.AnimatorUpdateListener() { |
|||
@Override |
|||
public void onAnimationUpdate(ValueAnimator valueAnimator) { |
|||
calculateInterpolation(getWorkingTransform(), (float) valueAnimator.getAnimatedValue()); |
|||
AnimatedZoomableController.super.setTransform(getWorkingTransform()); |
|||
} |
|||
}); |
|||
mValueAnimator.addListener( |
|||
new AnimatorListenerAdapter() { |
|||
@Override |
|||
public void onAnimationCancel(Animator animation) { |
|||
FLog.v(getLogTag(), "setTransformAnimated: animation cancelled"); |
|||
onAnimationStopped(); |
|||
} |
|||
|
|||
@Override |
|||
public void onAnimationEnd(Animator animation) { |
|||
FLog.v(getLogTag(), "setTransformAnimated: animation finished"); |
|||
onAnimationStopped(); |
|||
} |
|||
|
|||
private void onAnimationStopped() { |
|||
if (onAnimationComplete != null) { |
|||
onAnimationComplete.run(); |
|||
} |
|||
setAnimating(false); |
|||
getDetector().restartGesture(); |
|||
} |
|||
}); |
|||
mValueAnimator.start(); |
|||
} |
|||
|
|||
@SuppressLint("NewApi") |
|||
@Override |
|||
public void stopAnimation() { |
|||
if (!isAnimating()) { |
|||
return; |
|||
} |
|||
FLog.v(getLogTag(), "stopAnimation"); |
|||
mValueAnimator.cancel(); |
|||
mValueAnimator.removeAllUpdateListeners(); |
|||
mValueAnimator.removeAllListeners(); |
|||
} |
|||
|
|||
@Override |
|||
protected Class<?> getLogTag() { |
|||
return TAG; |
|||
} |
|||
} |
@ -0,0 +1,720 @@ |
|||
/* |
|||
* Copyright (c) Facebook, Inc. and its affiliates. |
|||
* |
|||
* This source code is licensed under the MIT license found in the |
|||
* LICENSE file in the root directory of this source tree. |
|||
*/ |
|||
|
|||
package awais.instagrabber.customviews.drawee; |
|||
|
|||
import android.graphics.Matrix; |
|||
import android.graphics.PointF; |
|||
import android.graphics.RectF; |
|||
import android.view.MotionEvent; |
|||
|
|||
import androidx.annotation.IntDef; |
|||
import androidx.annotation.Nullable; |
|||
|
|||
import com.facebook.common.logging.FLog; |
|||
|
|||
import java.lang.annotation.Retention; |
|||
import java.lang.annotation.RetentionPolicy; |
|||
|
|||
/** |
|||
* Zoomable controller that calculates transformation based on touch events. |
|||
*/ |
|||
public class DefaultZoomableController |
|||
implements ZoomableController, TransformGestureDetector.Listener { |
|||
|
|||
/** |
|||
* Interface for handling call backs when the image bounds are set. |
|||
*/ |
|||
public interface ImageBoundsListener { |
|||
void onImageBoundsSet(RectF imageBounds); |
|||
} |
|||
|
|||
@IntDef( |
|||
flag = true, |
|||
value = {LIMIT_NONE, LIMIT_TRANSLATION_X, LIMIT_TRANSLATION_Y, LIMIT_SCALE, LIMIT_ALL}) |
|||
@Retention(RetentionPolicy.SOURCE) |
|||
public @interface LimitFlag {} |
|||
|
|||
public static final int LIMIT_NONE = 0; |
|||
public static final int LIMIT_TRANSLATION_X = 1; |
|||
public static final int LIMIT_TRANSLATION_Y = 2; |
|||
public static final int LIMIT_SCALE = 4; |
|||
public static final int LIMIT_ALL = LIMIT_TRANSLATION_X | LIMIT_TRANSLATION_Y | LIMIT_SCALE; |
|||
|
|||
private static final float EPS = 1e-3f; |
|||
|
|||
private static final Class<?> TAG = DefaultZoomableController.class; |
|||
|
|||
private static final RectF IDENTITY_RECT = new RectF(0, 0, 1, 1); |
|||
|
|||
private TransformGestureDetector mGestureDetector; |
|||
|
|||
private @Nullable |
|||
ImageBoundsListener mImageBoundsListener; |
|||
|
|||
private @Nullable |
|||
Listener mListener = null; |
|||
|
|||
private boolean mIsEnabled = false; |
|||
private boolean mIsRotationEnabled = false; |
|||
private boolean mIsScaleEnabled = true; |
|||
private boolean mIsTranslationEnabled = true; |
|||
private boolean mIsGestureZoomEnabled = true; |
|||
|
|||
private float mMinScaleFactor = 1.0f; |
|||
private float mMaxScaleFactor = 2.0f; |
|||
|
|||
// View bounds, in view-absolute coordinates |
|||
private final RectF mViewBounds = new RectF(); |
|||
// Non-transformed image bounds, in view-absolute coordinates |
|||
private final RectF mImageBounds = new RectF(); |
|||
// Transformed image bounds, in view-absolute coordinates |
|||
private final RectF mTransformedImageBounds = new RectF(); |
|||
|
|||
private final Matrix mPreviousTransform = new Matrix(); |
|||
private final Matrix mActiveTransform = new Matrix(); |
|||
private final Matrix mActiveTransformInverse = new Matrix(); |
|||
private final float[] mTempValues = new float[9]; |
|||
private final RectF mTempRect = new RectF(); |
|||
private boolean mWasTransformCorrected; |
|||
|
|||
public static DefaultZoomableController newInstance() { |
|||
return new DefaultZoomableController(TransformGestureDetector.newInstance()); |
|||
} |
|||
|
|||
public DefaultZoomableController(TransformGestureDetector gestureDetector) { |
|||
mGestureDetector = gestureDetector; |
|||
mGestureDetector.setListener(this); |
|||
} |
|||
|
|||
/** |
|||
* Rests the controller. |
|||
*/ |
|||
public void reset() { |
|||
FLog.v(TAG, "reset"); |
|||
mGestureDetector.reset(); |
|||
mPreviousTransform.reset(); |
|||
mActiveTransform.reset(); |
|||
onTransformChanged(); |
|||
} |
|||
|
|||
/** |
|||
* Sets the zoomable listener. |
|||
*/ |
|||
@Override |
|||
public void setListener(Listener listener) { |
|||
mListener = listener; |
|||
} |
|||
|
|||
/** |
|||
* Sets whether the controller is enabled or not. |
|||
*/ |
|||
@Override |
|||
public void setEnabled(boolean enabled) { |
|||
mIsEnabled = enabled; |
|||
if (!enabled) { |
|||
reset(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Gets whether the controller is enabled or not. |
|||
*/ |
|||
@Override |
|||
public boolean isEnabled() { |
|||
return mIsEnabled; |
|||
} |
|||
|
|||
/** |
|||
* Sets whether the rotation gesture is enabled or not. |
|||
*/ |
|||
public void setRotationEnabled(boolean enabled) { |
|||
mIsRotationEnabled = enabled; |
|||
} |
|||
|
|||
/** |
|||
* Gets whether the rotation gesture is enabled or not. |
|||
*/ |
|||
public boolean isRotationEnabled() { |
|||
return mIsRotationEnabled; |
|||
} |
|||
|
|||
/** |
|||
* Sets whether the scale gesture is enabled or not. |
|||
*/ |
|||
public void setScaleEnabled(boolean enabled) { |
|||
mIsScaleEnabled = enabled; |
|||
} |
|||
|
|||
/** |
|||
* Gets whether the scale gesture is enabled or not. |
|||
*/ |
|||
public boolean isScaleEnabled() { |
|||
return mIsScaleEnabled; |
|||
} |
|||
|
|||
/** |
|||
* Sets whether the translation gesture is enabled or not. |
|||
*/ |
|||
public void setTranslationEnabled(boolean enabled) { |
|||
mIsTranslationEnabled = enabled; |
|||
} |
|||
|
|||
/** |
|||
* Gets whether the translations gesture is enabled or not. |
|||
*/ |
|||
public boolean isTranslationEnabled() { |
|||
return mIsTranslationEnabled; |
|||
} |
|||
|
|||
/** |
|||
* Sets the minimum scale factor allowed. |
|||
* |
|||
* <p>Hierarchy's scaling (if any) is not taken into account. |
|||
*/ |
|||
public void setMinScaleFactor(float minScaleFactor) { |
|||
mMinScaleFactor = minScaleFactor; |
|||
} |
|||
|
|||
/** |
|||
* Gets the minimum scale factor allowed. |
|||
*/ |
|||
public float getMinScaleFactor() { |
|||
return mMinScaleFactor; |
|||
} |
|||
|
|||
/** |
|||
* Sets the maximum scale factor allowed. |
|||
* |
|||
* <p>Hierarchy's scaling (if any) is not taken into account. |
|||
*/ |
|||
public void setMaxScaleFactor(float maxScaleFactor) { |
|||
mMaxScaleFactor = maxScaleFactor; |
|||
} |
|||
|
|||
/** |
|||
* Gets the maximum scale factor allowed. |
|||
*/ |
|||
public float getMaxScaleFactor() { |
|||
return mMaxScaleFactor; |
|||
} |
|||
|
|||
/** |
|||
* Sets whether gesture zooms are enabled or not. |
|||
*/ |
|||
public void setGestureZoomEnabled(boolean isGestureZoomEnabled) { |
|||
mIsGestureZoomEnabled = isGestureZoomEnabled; |
|||
} |
|||
|
|||
/** |
|||
* Gets whether gesture zooms are enabled or not. |
|||
*/ |
|||
public boolean isGestureZoomEnabled() { |
|||
return mIsGestureZoomEnabled; |
|||
} |
|||
|
|||
/** |
|||
* Gets the current scale factor. |
|||
*/ |
|||
@Override |
|||
public float getScaleFactor() { |
|||
return getMatrixScaleFactor(mActiveTransform); |
|||
} |
|||
|
|||
/** |
|||
* Sets the image bounds, in view-absolute coordinates. |
|||
*/ |
|||
@Override |
|||
public void setImageBounds(RectF imageBounds) { |
|||
if (!imageBounds.equals(mImageBounds)) { |
|||
mImageBounds.set(imageBounds); |
|||
onTransformChanged(); |
|||
if (mImageBoundsListener != null) { |
|||
mImageBoundsListener.onImageBoundsSet(mImageBounds); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Gets the non-transformed image bounds, in view-absolute coordinates. |
|||
*/ |
|||
public RectF getImageBounds() { |
|||
return mImageBounds; |
|||
} |
|||
|
|||
/** |
|||
* Gets the transformed image bounds, in view-absolute coordinates |
|||
*/ |
|||
private RectF getTransformedImageBounds() { |
|||
return mTransformedImageBounds; |
|||
} |
|||
|
|||
/** |
|||
* Sets the view bounds. |
|||
*/ |
|||
@Override |
|||
public void setViewBounds(RectF viewBounds) { |
|||
mViewBounds.set(viewBounds); |
|||
} |
|||
|
|||
/** |
|||
* Gets the view bounds. |
|||
*/ |
|||
public RectF getViewBounds() { |
|||
return mViewBounds; |
|||
} |
|||
|
|||
/** |
|||
* Sets the image bounds listener. |
|||
*/ |
|||
public void setImageBoundsListener(@Nullable ImageBoundsListener imageBoundsListener) { |
|||
mImageBoundsListener = imageBoundsListener; |
|||
} |
|||
|
|||
/** |
|||
* Gets the image bounds listener. |
|||
*/ |
|||
public @Nullable |
|||
ImageBoundsListener getImageBoundsListener() { |
|||
return mImageBoundsListener; |
|||
} |
|||
|
|||
/** |
|||
* Returns true if the zoomable transform is identity matrix. |
|||
*/ |
|||
@Override |
|||
public boolean isIdentity() { |
|||
return isMatrixIdentity(mActiveTransform, 1e-3f); |
|||
} |
|||
|
|||
/** |
|||
* Returns true if the transform was corrected during the last update. |
|||
* |
|||
* <p>We should rename this method to `wasTransformedWithoutCorrection` and just return the |
|||
* internal flag directly. However, this requires interface change and negation of meaning. |
|||
*/ |
|||
@Override |
|||
public boolean wasTransformCorrected() { |
|||
return mWasTransformCorrected; |
|||
} |
|||
|
|||
/** |
|||
* Gets the matrix that transforms image-absolute coordinates to view-absolute coordinates. The |
|||
* zoomable transformation is taken into account. |
|||
* |
|||
* <p>Internal matrix is exposed for performance reasons and is not to be modified by the callers. |
|||
*/ |
|||
@Override |
|||
public Matrix getTransform() { |
|||
return mActiveTransform; |
|||
} |
|||
|
|||
/** |
|||
* Gets the matrix that transforms image-relative coordinates to view-absolute coordinates. The |
|||
* zoomable transformation is taken into account. |
|||
*/ |
|||
public void getImageRelativeToViewAbsoluteTransform(Matrix outMatrix) { |
|||
outMatrix.setRectToRect(IDENTITY_RECT, mTransformedImageBounds, Matrix.ScaleToFit.FILL); |
|||
} |
|||
|
|||
/** |
|||
* Maps point from view-absolute to image-relative coordinates. This takes into account the |
|||
* zoomable transformation. |
|||
*/ |
|||
public PointF mapViewToImage(PointF viewPoint) { |
|||
float[] points = mTempValues; |
|||
points[0] = viewPoint.x; |
|||
points[1] = viewPoint.y; |
|||
mActiveTransform.invert(mActiveTransformInverse); |
|||
mActiveTransformInverse.mapPoints(points, 0, points, 0, 1); |
|||
mapAbsoluteToRelative(points, points, 1); |
|||
return new PointF(points[0], points[1]); |
|||
} |
|||
|
|||
/** |
|||
* Maps point from image-relative to view-absolute coordinates. This takes into account the |
|||
* zoomable transformation. |
|||
*/ |
|||
public PointF mapImageToView(PointF imagePoint) { |
|||
float[] points = mTempValues; |
|||
points[0] = imagePoint.x; |
|||
points[1] = imagePoint.y; |
|||
mapRelativeToAbsolute(points, points, 1); |
|||
mActiveTransform.mapPoints(points, 0, points, 0, 1); |
|||
return new PointF(points[0], points[1]); |
|||
} |
|||
|
|||
/** |
|||
* Maps array of 2D points from view-absolute to image-relative coordinates. This does NOT take |
|||
* into account the zoomable transformation. Points are represented by a float array of [x0, y0, |
|||
* x1, y1, ...]. |
|||
* |
|||
* @param destPoints destination array (may be the same as source array) |
|||
* @param srcPoints source array |
|||
* @param numPoints number of points to map |
|||
*/ |
|||
private void mapAbsoluteToRelative(float[] destPoints, float[] srcPoints, int numPoints) { |
|||
for (int i = 0; i < numPoints; i++) { |
|||
destPoints[i * 2 + 0] = (srcPoints[i * 2 + 0] - mImageBounds.left) / mImageBounds.width(); |
|||
destPoints[i * 2 + 1] = (srcPoints[i * 2 + 1] - mImageBounds.top) / mImageBounds.height(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Maps array of 2D points from image-relative to view-absolute coordinates. This does NOT take |
|||
* into account the zoomable transformation. Points are represented by float array of [x0, y0, x1, |
|||
* y1, ...]. |
|||
* |
|||
* @param destPoints destination array (may be the same as source array) |
|||
* @param srcPoints source array |
|||
* @param numPoints number of points to map |
|||
*/ |
|||
private void mapRelativeToAbsolute(float[] destPoints, float[] srcPoints, int numPoints) { |
|||
for (int i = 0; i < numPoints; i++) { |
|||
destPoints[i * 2 + 0] = srcPoints[i * 2 + 0] * mImageBounds.width() + mImageBounds.left; |
|||
destPoints[i * 2 + 1] = srcPoints[i * 2 + 1] * mImageBounds.height() + mImageBounds.top; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Zooms to the desired scale and positions the image so that the given image point corresponds to |
|||
* the given view point. |
|||
* |
|||
* @param scale desired scale, will be limited to {min, max} scale factor |
|||
* @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1) |
|||
* @param viewPoint 2D point in view's absolute coordinate system |
|||
*/ |
|||
public void zoomToPoint(float scale, PointF imagePoint, PointF viewPoint) { |
|||
FLog.v(TAG, "zoomToPoint"); |
|||
calculateZoomToPointTransform(mActiveTransform, scale, imagePoint, viewPoint, LIMIT_ALL); |
|||
onTransformChanged(); |
|||
} |
|||
|
|||
/** |
|||
* Calculates the zoom transformation that would zoom to the desired scale and position the image |
|||
* so that the given image point corresponds to the given view point. |
|||
* |
|||
* @param outTransform the matrix to store the result to |
|||
* @param scale desired scale, will be limited to {min, max} scale factor |
|||
* @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1) |
|||
* @param viewPoint 2D point in view's absolute coordinate system |
|||
* @param limitFlags whether to limit translation and/or scale. |
|||
* @return whether or not the transform has been corrected due to limitation |
|||
*/ |
|||
protected boolean calculateZoomToPointTransform( |
|||
Matrix outTransform, |
|||
float scale, |
|||
PointF imagePoint, |
|||
PointF viewPoint, |
|||
@LimitFlag int limitFlags) { |
|||
float[] viewAbsolute = mTempValues; |
|||
viewAbsolute[0] = imagePoint.x; |
|||
viewAbsolute[1] = imagePoint.y; |
|||
mapRelativeToAbsolute(viewAbsolute, viewAbsolute, 1); |
|||
float distanceX = viewPoint.x - viewAbsolute[0]; |
|||
float distanceY = viewPoint.y - viewAbsolute[1]; |
|||
boolean transformCorrected = false; |
|||
outTransform.setScale(scale, scale, viewAbsolute[0], viewAbsolute[1]); |
|||
transformCorrected |= limitScale(outTransform, viewAbsolute[0], viewAbsolute[1], limitFlags); |
|||
outTransform.postTranslate(distanceX, distanceY); |
|||
transformCorrected |= limitTranslation(outTransform, limitFlags); |
|||
return transformCorrected; |
|||
} |
|||
|
|||
/** |
|||
* Sets a new zoom transformation. |
|||
*/ |
|||
public void setTransform(Matrix newTransform) { |
|||
FLog.v(TAG, "setTransform"); |
|||
mActiveTransform.set(newTransform); |
|||
onTransformChanged(); |
|||
} |
|||
|
|||
/** |
|||
* Gets the gesture detector. |
|||
*/ |
|||
protected TransformGestureDetector getDetector() { |
|||
return mGestureDetector; |
|||
} |
|||
|
|||
/** |
|||
* Notifies controller of the received touch event. |
|||
*/ |
|||
@Override |
|||
public boolean onTouchEvent(MotionEvent event) { |
|||
FLog.v(TAG, "onTouchEvent: action: ", event.getAction()); |
|||
if (mIsEnabled && mIsGestureZoomEnabled) { |
|||
return mGestureDetector.onTouchEvent(event); |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
/* TransformGestureDetector.Listener methods */ |
|||
|
|||
@Override |
|||
public void onGestureBegin(TransformGestureDetector detector) { |
|||
FLog.v(TAG, "onGestureBegin"); |
|||
mPreviousTransform.set(mActiveTransform); |
|||
onTransformBegin(); |
|||
// We only received a touch down event so far, and so we don't know yet in which direction a |
|||
// future move event will follow. Therefore, if we can't scroll in all directions, we have to |
|||
// assume the worst case where the user tries to scroll out of edge, which would cause |
|||
// transformation to be corrected. |
|||
mWasTransformCorrected = !canScrollInAllDirection(); |
|||
} |
|||
|
|||
@Override |
|||
public void onGestureUpdate(TransformGestureDetector detector) { |
|||
FLog.v(TAG, "onGestureUpdate"); |
|||
boolean transformCorrected = calculateGestureTransform(mActiveTransform, LIMIT_ALL); |
|||
onTransformChanged(); |
|||
if (transformCorrected) { |
|||
mGestureDetector.restartGesture(); |
|||
} |
|||
// A transformation happened, but was it without correction? |
|||
mWasTransformCorrected = transformCorrected; |
|||
} |
|||
|
|||
@Override |
|||
public void onGestureEnd(TransformGestureDetector detector) { |
|||
FLog.v(TAG, "onGestureEnd"); |
|||
onTransformEnd(); |
|||
} |
|||
|
|||
/** |
|||
* Calculates the zoom transformation based on the current gesture. |
|||
* |
|||
* @param outTransform the matrix to store the result to |
|||
* @param limitTypes whether to limit translation and/or scale. |
|||
* @return whether or not the transform has been corrected due to limitation |
|||
*/ |
|||
protected boolean calculateGestureTransform(Matrix outTransform, @LimitFlag int limitTypes) { |
|||
TransformGestureDetector detector = mGestureDetector; |
|||
boolean transformCorrected = false; |
|||
outTransform.set(mPreviousTransform); |
|||
if (mIsRotationEnabled) { |
|||
float angle = detector.getRotation() * (float) (180 / Math.PI); |
|||
outTransform.postRotate(angle, detector.getPivotX(), detector.getPivotY()); |
|||
} |
|||
if (mIsScaleEnabled) { |
|||
float scale = detector.getScale(); |
|||
outTransform.postScale(scale, scale, detector.getPivotX(), detector.getPivotY()); |
|||
} |
|||
transformCorrected |= |
|||
limitScale(outTransform, detector.getPivotX(), detector.getPivotY(), limitTypes); |
|||
if (mIsTranslationEnabled) { |
|||
outTransform.postTranslate(detector.getTranslationX(), detector.getTranslationY()); |
|||
} |
|||
transformCorrected |= limitTranslation(outTransform, limitTypes); |
|||
return transformCorrected; |
|||
} |
|||
|
|||
private void onTransformBegin() { |
|||
if (mListener != null && isEnabled()) { |
|||
mListener.onTransformBegin(mActiveTransform); |
|||
} |
|||
} |
|||
|
|||
private void onTransformChanged() { |
|||
mActiveTransform.mapRect(mTransformedImageBounds, mImageBounds); |
|||
if (mListener != null && isEnabled()) { |
|||
mListener.onTransformChanged(mActiveTransform); |
|||
} |
|||
} |
|||
|
|||
private void onTransformEnd() { |
|||
if (mListener != null && isEnabled()) { |
|||
mListener.onTransformEnd(mActiveTransform); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Keeps the scaling factor within the specified limits. |
|||
* |
|||
* @param pivotX x coordinate of the pivot point |
|||
* @param pivotY y coordinate of the pivot point |
|||
* @param limitTypes whether to limit scale. |
|||
* @return whether limiting has been applied or not |
|||
*/ |
|||
private boolean limitScale( |
|||
Matrix transform, float pivotX, float pivotY, @LimitFlag int limitTypes) { |
|||
if (!shouldLimit(limitTypes, LIMIT_SCALE)) { |
|||
return false; |
|||
} |
|||
float currentScale = getMatrixScaleFactor(transform); |
|||
float targetScale = limit(currentScale, mMinScaleFactor, mMaxScaleFactor); |
|||
if (targetScale != currentScale) { |
|||
float scale = targetScale / currentScale; |
|||
transform.postScale(scale, scale, pivotX, pivotY); |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* Limits the translation so that there are no empty spaces on the sides if possible. |
|||
* |
|||
* <p>The image is attempted to be centered within the view bounds if the transformed image is |
|||
* smaller. There will be no empty spaces within the view bounds if the transformed image is |
|||
* bigger. This applies to each dimension (horizontal and vertical) independently. |
|||
* |
|||
* @param limitTypes whether to limit translation along the specific axis. |
|||
* @return whether limiting has been applied or not |
|||
*/ |
|||
private boolean limitTranslation(Matrix transform, @LimitFlag int limitTypes) { |
|||
if (!shouldLimit(limitTypes, LIMIT_TRANSLATION_X | LIMIT_TRANSLATION_Y)) { |
|||
return false; |
|||
} |
|||
RectF b = mTempRect; |
|||
b.set(mImageBounds); |
|||
transform.mapRect(b); |
|||
float offsetLeft = |
|||
shouldLimit(limitTypes, LIMIT_TRANSLATION_X) |
|||
? getOffset( |
|||
b.left, b.right, mViewBounds.left, mViewBounds.right, mImageBounds.centerX()) |
|||
: 0; |
|||
float offsetTop = |
|||
shouldLimit(limitTypes, LIMIT_TRANSLATION_Y) |
|||
? getOffset( |
|||
b.top, b.bottom, mViewBounds.top, mViewBounds.bottom, mImageBounds.centerY()) |
|||
: 0; |
|||
if (offsetLeft != 0 || offsetTop != 0) { |
|||
transform.postTranslate(offsetLeft, offsetTop); |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* Checks whether the specified limit flag is present in the limits provided. |
|||
* |
|||
* <p>If the flag contains multiple flags together using a bitwise OR, this only checks that at |
|||
* least one of the flags is included. |
|||
* |
|||
* @param limits the limits to apply |
|||
* @param flag the limit flag(s) to check for |
|||
* @return true if the flag (or one of the flags) is included in the limits |
|||
*/ |
|||
private static boolean shouldLimit(@LimitFlag int limits, @LimitFlag int flag) { |
|||
return (limits & flag) != LIMIT_NONE; |
|||
} |
|||
|
|||
/** |
|||
* Returns the offset necessary to make sure that: - the image is centered within the limit if the |
|||
* image is smaller than the limit - there is no empty space on left/right if the image is bigger |
|||
* than the limit |
|||
*/ |
|||
private float getOffset( |
|||
float imageStart, float imageEnd, float limitStart, float limitEnd, float limitCenter) { |
|||
float imageWidth = imageEnd - imageStart, limitWidth = limitEnd - limitStart; |
|||
float limitInnerWidth = Math.min(limitCenter - limitStart, limitEnd - limitCenter) * 2; |
|||
// center if smaller than limitInnerWidth |
|||
if (imageWidth < limitInnerWidth) { |
|||
return limitCenter - (imageEnd + imageStart) / 2; |
|||
} |
|||
// to the edge if in between and limitCenter is not (limitLeft + limitRight) / 2 |
|||
if (imageWidth < limitWidth) { |
|||
if (limitCenter < (limitStart + limitEnd) / 2) { |
|||
return limitStart - imageStart; |
|||
} else { |
|||
return limitEnd - imageEnd; |
|||
} |
|||
} |
|||
// to the edge if larger than limitWidth and empty space visible |
|||
if (imageStart > limitStart) { |
|||
return limitStart - imageStart; |
|||
} |
|||
if (imageEnd < limitEnd) { |
|||
return limitEnd - imageEnd; |
|||
} |
|||
return 0; |
|||
} |
|||
|
|||
/** |
|||
* Limits the value to the given min and max range. |
|||
*/ |
|||
private float limit(float value, float min, float max) { |
|||
return Math.min(Math.max(min, value), max); |
|||
} |
|||
|
|||
/** |
|||
* Gets the scale factor for the given matrix. This method assumes the equal scaling factor for X |
|||
* and Y axis. |
|||
*/ |
|||
private float getMatrixScaleFactor(Matrix transform) { |
|||
transform.getValues(mTempValues); |
|||
return mTempValues[Matrix.MSCALE_X]; |
|||
} |
|||
|
|||
/** |
|||
* Same as {@code Matrix.isIdentity()}, but with tolerance {@code eps}. |
|||
*/ |
|||
private boolean isMatrixIdentity(Matrix transform, float eps) { |
|||
// Checks whether the given matrix is close enough to the identity matrix: |
|||
// 1 0 0 |
|||
// 0 1 0 |
|||
// 0 0 1 |
|||
// Or equivalently to the zero matrix, after subtracting 1.0f from the diagonal elements: |
|||
// 0 0 0 |
|||
// 0 0 0 |
|||
// 0 0 0 |
|||
transform.getValues(mTempValues); |
|||
mTempValues[0] -= 1.0f; // m00 |
|||
mTempValues[4] -= 1.0f; // m11 |
|||
mTempValues[8] -= 1.0f; // m22 |
|||
for (int i = 0; i < 9; i++) { |
|||
if (Math.abs(mTempValues[i]) > eps) { |
|||
return false; |
|||
} |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* Returns whether the scroll can happen in all directions. I.e. the image is not on any edge. |
|||
*/ |
|||
private boolean canScrollInAllDirection() { |
|||
return mTransformedImageBounds.left < mViewBounds.left - EPS |
|||
&& mTransformedImageBounds.top < mViewBounds.top - EPS |
|||
&& mTransformedImageBounds.right > mViewBounds.right + EPS |
|||
&& mTransformedImageBounds.bottom > mViewBounds.bottom + EPS; |
|||
} |
|||
|
|||
@Override |
|||
public int computeHorizontalScrollRange() { |
|||
return (int) mTransformedImageBounds.width(); |
|||
} |
|||
|
|||
@Override |
|||
public int computeHorizontalScrollOffset() { |
|||
return (int) (mViewBounds.left - mTransformedImageBounds.left); |
|||
} |
|||
|
|||
@Override |
|||
public int computeHorizontalScrollExtent() { |
|||
return (int) mViewBounds.width(); |
|||
} |
|||
|
|||
@Override |
|||
public int computeVerticalScrollRange() { |
|||
return (int) mTransformedImageBounds.height(); |
|||
} |
|||
|
|||
@Override |
|||
public int computeVerticalScrollOffset() { |
|||
return (int) (mViewBounds.top - mTransformedImageBounds.top); |
|||
} |
|||
|
|||
@Override |
|||
public int computeVerticalScrollExtent() { |
|||
return (int) mViewBounds.height(); |
|||
} |
|||
|
|||
public Listener getListener() { |
|||
return mListener; |
|||
} |
|||
} |
@ -0,0 +1,84 @@ |
|||
/* |
|||
* Copyright (c) Facebook, Inc. and its affiliates. |
|||
* |
|||
* This source code is licensed under the MIT license found in the |
|||
* LICENSE file in the root directory of this source tree. |
|||
*/ |
|||
|
|||
package awais.instagrabber.customviews.drawee; |
|||
|
|||
import android.graphics.PointF; |
|||
import android.view.GestureDetector; |
|||
import android.view.MotionEvent; |
|||
|
|||
/** |
|||
* Tap gesture listener for double tap to zoom / unzoom and double-tap-and-drag to zoom. |
|||
* |
|||
* @see ZoomableDraweeView#setTapListener(GestureDetector.SimpleOnGestureListener) |
|||
*/ |
|||
public class DoubleTapGestureListener extends GestureDetector.SimpleOnGestureListener { |
|||
private static final int DURATION_MS = 300; |
|||
private static final int DOUBLE_TAP_SCROLL_THRESHOLD = 20; |
|||
|
|||
private final ZoomableDraweeView mDraweeView; |
|||
private final PointF mDoubleTapViewPoint = new PointF(); |
|||
private final PointF mDoubleTapImagePoint = new PointF(); |
|||
private float mDoubleTapScale = 1; |
|||
private boolean mDoubleTapScroll = false; |
|||
|
|||
public DoubleTapGestureListener(ZoomableDraweeView zoomableDraweeView) { |
|||
mDraweeView = zoomableDraweeView; |
|||
} |
|||
|
|||
@Override |
|||
public boolean onDoubleTapEvent(MotionEvent e) { |
|||
AbstractAnimatedZoomableController zc = |
|||
(AbstractAnimatedZoomableController) mDraweeView.getZoomableController(); |
|||
PointF vp = new PointF(e.getX(), e.getY()); |
|||
PointF ip = zc.mapViewToImage(vp); |
|||
switch (e.getActionMasked()) { |
|||
case MotionEvent.ACTION_DOWN: |
|||
mDoubleTapViewPoint.set(vp); |
|||
mDoubleTapImagePoint.set(ip); |
|||
mDoubleTapScale = zc.getScaleFactor(); |
|||
break; |
|||
case MotionEvent.ACTION_MOVE: |
|||
mDoubleTapScroll = mDoubleTapScroll || shouldStartDoubleTapScroll(vp); |
|||
if (mDoubleTapScroll) { |
|||
float scale = calcScale(vp); |
|||
zc.zoomToPoint(scale, mDoubleTapImagePoint, mDoubleTapViewPoint); |
|||
} |
|||
break; |
|||
case MotionEvent.ACTION_UP: |
|||
if (mDoubleTapScroll) { |
|||
float scale = calcScale(vp); |
|||
zc.zoomToPoint(scale, mDoubleTapImagePoint, mDoubleTapViewPoint); |
|||
} else { |
|||
final float maxScale = zc.getMaxScaleFactor(); |
|||
final float minScale = zc.getMinScaleFactor(); |
|||
if (zc.getScaleFactor() < (maxScale + minScale) / 2) { |
|||
zc.zoomToPoint( |
|||
maxScale, ip, vp, DefaultZoomableController.LIMIT_ALL, DURATION_MS, null); |
|||
} else { |
|||
zc.zoomToPoint( |
|||
minScale, ip, vp, DefaultZoomableController.LIMIT_ALL, DURATION_MS, null); |
|||
} |
|||
} |
|||
mDoubleTapScroll = false; |
|||
break; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
private boolean shouldStartDoubleTapScroll(PointF viewPoint) { |
|||
double dist = |
|||
Math.hypot(viewPoint.x - mDoubleTapViewPoint.x, viewPoint.y - mDoubleTapViewPoint.y); |
|||
return dist > DOUBLE_TAP_SCROLL_THRESHOLD; |
|||
} |
|||
|
|||
private float calcScale(PointF currentViewPoint) { |
|||
float dy = (currentViewPoint.y - mDoubleTapViewPoint.y); |
|||
float t = 1 + Math.abs(dy) * 0.001f; |
|||
return (dy < 0) ? mDoubleTapScale / t : mDoubleTapScale * t; |
|||
} |
|||
} |
@ -0,0 +1,72 @@ |
|||
/* |
|||
* Copyright (c) Facebook, Inc. and its affiliates. |
|||
* |
|||
* This source code is licensed under the MIT license found in the |
|||
* LICENSE file in the root directory of this source tree. |
|||
*/ |
|||
|
|||
package awais.instagrabber.customviews.drawee; |
|||
|
|||
import android.view.GestureDetector; |
|||
import android.view.MotionEvent; |
|||
|
|||
/** |
|||
* Wrapper for SimpleOnGestureListener as GestureDetector does not allow changing its listener. |
|||
*/ |
|||
public class GestureListenerWrapper extends GestureDetector.SimpleOnGestureListener { |
|||
|
|||
private GestureDetector.SimpleOnGestureListener mDelegate; |
|||
|
|||
public GestureListenerWrapper() { |
|||
mDelegate = new GestureDetector.SimpleOnGestureListener(); |
|||
} |
|||
|
|||
public void setListener(GestureDetector.SimpleOnGestureListener listener) { |
|||
mDelegate = listener; |
|||
} |
|||
|
|||
@Override |
|||
public void onLongPress(MotionEvent e) { |
|||
mDelegate.onLongPress(e); |
|||
} |
|||
|
|||
@Override |
|||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { |
|||
return mDelegate.onScroll(e1, e2, distanceX, distanceY); |
|||
} |
|||
|
|||
@Override |
|||
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { |
|||
return mDelegate.onFling(e1, e2, velocityX, velocityY); |
|||
} |
|||
|
|||
@Override |
|||
public void onShowPress(MotionEvent e) { |
|||
mDelegate.onShowPress(e); |
|||
} |
|||
|
|||
@Override |
|||
public boolean onDown(MotionEvent e) { |
|||
return mDelegate.onDown(e); |
|||
} |
|||
|
|||
@Override |
|||
public boolean onDoubleTap(MotionEvent e) { |
|||
return mDelegate.onDoubleTap(e); |
|||
} |
|||
|
|||
@Override |
|||
public boolean onDoubleTapEvent(MotionEvent e) { |
|||
return mDelegate.onDoubleTapEvent(e); |
|||
} |
|||
|
|||
@Override |
|||
public boolean onSingleTapConfirmed(MotionEvent e) { |
|||
return mDelegate.onSingleTapConfirmed(e); |
|||
} |
|||
|
|||
@Override |
|||
public boolean onSingleTapUp(MotionEvent e) { |
|||
return mDelegate.onSingleTapUp(e); |
|||
} |
|||
} |
@ -0,0 +1,156 @@ |
|||
/* |
|||
* Copyright (c) Facebook, Inc. and its affiliates. |
|||
* |
|||
* This source code is licensed under the MIT license found in the |
|||
* LICENSE file in the root directory of this source tree. |
|||
*/ |
|||
|
|||
package awais.instagrabber.customviews.drawee; |
|||
|
|||
import android.view.GestureDetector; |
|||
import android.view.MotionEvent; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* Gesture listener that allows multiple child listeners to be added and notified about gesture |
|||
* events. |
|||
* |
|||
* <p>NOTE: The order of the listeners is important. Listeners can consume gesture events. For |
|||
* example, if one of the child listeners consumes {@link #onLongPress(MotionEvent)} (the listener |
|||
* returned true), subsequent listeners will not be notified about the event any more since it has |
|||
* been consumed. |
|||
*/ |
|||
public class MultiGestureListener extends GestureDetector.SimpleOnGestureListener { |
|||
|
|||
private final List<GestureDetector.SimpleOnGestureListener> mListeners = new ArrayList<>(); |
|||
|
|||
/** |
|||
* Adds a listener to the multi gesture listener. |
|||
* |
|||
* <p>NOTE: The order of the listeners is important since gesture events can be consumed. |
|||
* |
|||
* @param listener the listener to be added |
|||
*/ |
|||
public synchronized void addListener(GestureDetector.SimpleOnGestureListener listener) { |
|||
mListeners.add(listener); |
|||
} |
|||
|
|||
/** |
|||
* Removes the given listener so that it will not be notified about future events. |
|||
* |
|||
* <p>NOTE: The order of the listeners is important since gesture events can be consumed. |
|||
* |
|||
* @param listener the listener to remove |
|||
*/ |
|||
public synchronized void removeListener(GestureDetector.SimpleOnGestureListener listener) { |
|||
mListeners.remove(listener); |
|||
} |
|||
|
|||
@Override |
|||
public synchronized boolean onSingleTapUp(MotionEvent e) { |
|||
final int size = mListeners.size(); |
|||
for (int i = 0; i < size; i++) { |
|||
if (mListeners.get(i).onSingleTapUp(e)) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public synchronized void onLongPress(MotionEvent e) { |
|||
final int size = mListeners.size(); |
|||
for (int i = 0; i < size; i++) { |
|||
mListeners.get(i).onLongPress(e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public synchronized boolean onScroll( |
|||
MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { |
|||
final int size = mListeners.size(); |
|||
for (int i = 0; i < size; i++) { |
|||
if (mListeners.get(i).onScroll(e1, e2, distanceX, distanceY)) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public synchronized boolean onFling( |
|||
MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { |
|||
final int size = mListeners.size(); |
|||
for (int i = 0; i < size; i++) { |
|||
if (mListeners.get(i).onFling(e1, e2, velocityX, velocityY)) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public synchronized void onShowPress(MotionEvent e) { |
|||
final int size = mListeners.size(); |
|||
for (int i = 0; i < size; i++) { |
|||
mListeners.get(i).onShowPress(e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public synchronized boolean onDown(MotionEvent e) { |
|||
final int size = mListeners.size(); |
|||
for (int i = 0; i < size; i++) { |
|||
if (mListeners.get(i).onDown(e)) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public synchronized boolean onDoubleTap(MotionEvent e) { |
|||
final int size = mListeners.size(); |
|||
for (int i = 0; i < size; i++) { |
|||
if (mListeners.get(i).onDoubleTap(e)) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public synchronized boolean onDoubleTapEvent(MotionEvent e) { |
|||
final int size = mListeners.size(); |
|||
for (int i = 0; i < size; i++) { |
|||
if (mListeners.get(i).onDoubleTapEvent(e)) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public synchronized boolean onSingleTapConfirmed(MotionEvent e) { |
|||
final int size = mListeners.size(); |
|||
for (int i = 0; i < size; i++) { |
|||
if (mListeners.get(i).onSingleTapConfirmed(e)) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public synchronized boolean onContextClick(MotionEvent e) { |
|||
final int size = mListeners.size(); |
|||
for (int i = 0; i < size; i++) { |
|||
if (mListeners.get(i).onContextClick(e)) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
} |
@ -0,0 +1,286 @@ |
|||
/* |
|||
* Copyright (c) Facebook, Inc. and its affiliates. |
|||
* |
|||
* This source code is licensed under the MIT license found in the |
|||
* LICENSE file in the root directory of this source tree. |
|||
*/ |
|||
|
|||
package awais.instagrabber.customviews.drawee; |
|||
|
|||
import android.view.MotionEvent; |
|||
|
|||
/** |
|||
* Component that detects and tracks multiple pointers based on touch events. |
|||
* |
|||
* <p>Each time a pointer gets pressed or released, the current gesture (if any) will end, and a new |
|||
* one will be started (if there are still pressed pointers left). It is guaranteed that the number |
|||
* of pointers within the single gesture will remain the same during the whole gesture. |
|||
*/ |
|||
public class MultiPointerGestureDetector { |
|||
|
|||
/** |
|||
* The listener for receiving notifications when gestures occur. |
|||
*/ |
|||
public interface Listener { |
|||
/** |
|||
* A callback called right before the gesture is about to start. |
|||
*/ |
|||
public void onGestureBegin(MultiPointerGestureDetector detector); |
|||
|
|||
/** |
|||
* A callback called each time the gesture gets updated. |
|||
*/ |
|||
public void onGestureUpdate(MultiPointerGestureDetector detector); |
|||
|
|||
/** |
|||
* A callback called right after the gesture has finished. |
|||
*/ |
|||
public void onGestureEnd(MultiPointerGestureDetector detector); |
|||
} |
|||
|
|||
private static final int MAX_POINTERS = 2; |
|||
|
|||
private boolean mGestureInProgress; |
|||
private int mPointerCount; |
|||
private int mNewPointerCount; |
|||
private final int mId[] = new int[MAX_POINTERS]; |
|||
private final float mStartX[] = new float[MAX_POINTERS]; |
|||
private final float mStartY[] = new float[MAX_POINTERS]; |
|||
private final float mCurrentX[] = new float[MAX_POINTERS]; |
|||
private final float mCurrentY[] = new float[MAX_POINTERS]; |
|||
|
|||
private Listener mListener = null; |
|||
|
|||
public MultiPointerGestureDetector() { |
|||
reset(); |
|||
} |
|||
|
|||
/** |
|||
* Factory method that creates a new instance of MultiPointerGestureDetector |
|||
*/ |
|||
public static MultiPointerGestureDetector newInstance() { |
|||
return new MultiPointerGestureDetector(); |
|||
} |
|||
|
|||
/** |
|||
* Sets the listener. |
|||
* |
|||
* @param listener listener to set |
|||
*/ |
|||
public void setListener(Listener listener) { |
|||
mListener = listener; |
|||
} |
|||
|
|||
/** |
|||
* Resets the component to the initial state. |
|||
*/ |
|||
public void reset() { |
|||
mGestureInProgress = false; |
|||
mPointerCount = 0; |
|||
for (int i = 0; i < MAX_POINTERS; i++) { |
|||
mId[i] = MotionEvent.INVALID_POINTER_ID; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* This method can be overridden in order to perform threshold check or something similar. |
|||
* |
|||
* @return whether or not to start a new gesture |
|||
*/ |
|||
protected boolean shouldStartGesture() { |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* Starts a new gesture and calls the listener just before starting it. |
|||
*/ |
|||
private void startGesture() { |
|||
if (!mGestureInProgress) { |
|||
if (mListener != null) { |
|||
mListener.onGestureBegin(this); |
|||
} |
|||
mGestureInProgress = true; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Stops the current gesture and calls the listener right after stopping it. |
|||
*/ |
|||
private void stopGesture() { |
|||
if (mGestureInProgress) { |
|||
mGestureInProgress = false; |
|||
if (mListener != null) { |
|||
mListener.onGestureEnd(this); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Gets the index of the i-th pressed pointer. Normally, the index will be equal to i, except in |
|||
* the case when the pointer is released. |
|||
* |
|||
* @return index of the specified pointer or -1 if not found (i.e. not enough pointers are down) |
|||
*/ |
|||
private int getPressedPointerIndex(MotionEvent event, int i) { |
|||
final int count = event.getPointerCount(); |
|||
final int action = event.getActionMasked(); |
|||
final int index = event.getActionIndex(); |
|||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { |
|||
if (i >= index) { |
|||
i++; |
|||
} |
|||
} |
|||
return (i < count) ? i : -1; |
|||
} |
|||
|
|||
/** |
|||
* Gets the number of pressed pointers (fingers down). |
|||
*/ |
|||
private static int getPressedPointerCount(MotionEvent event) { |
|||
int count = event.getPointerCount(); |
|||
int action = event.getActionMasked(); |
|||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { |
|||
count--; |
|||
} |
|||
return count; |
|||
} |
|||
|
|||
private void updatePointersOnTap(MotionEvent event) { |
|||
mPointerCount = 0; |
|||
for (int i = 0; i < MAX_POINTERS; i++) { |
|||
int index = getPressedPointerIndex(event, i); |
|||
if (index == -1) { |
|||
mId[i] = MotionEvent.INVALID_POINTER_ID; |
|||
} else { |
|||
mId[i] = event.getPointerId(index); |
|||
mCurrentX[i] = mStartX[i] = event.getX(index); |
|||
mCurrentY[i] = mStartY[i] = event.getY(index); |
|||
mPointerCount++; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void updatePointersOnMove(MotionEvent event) { |
|||
for (int i = 0; i < MAX_POINTERS; i++) { |
|||
int index = event.findPointerIndex(mId[i]); |
|||
if (index != -1) { |
|||
mCurrentX[i] = event.getX(index); |
|||
mCurrentY[i] = event.getY(index); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Handles the given motion event. |
|||
* |
|||
* @param event event to handle |
|||
* @return whether or not the event was handled |
|||
*/ |
|||
public boolean onTouchEvent(final MotionEvent event) { |
|||
switch (event.getActionMasked()) { |
|||
case MotionEvent.ACTION_MOVE: { |
|||
// update pointers |
|||
updatePointersOnMove(event); |
|||
// start a new gesture if not already started |
|||
if (!mGestureInProgress && mPointerCount > 0 && shouldStartGesture()) { |
|||
startGesture(); |
|||
} |
|||
// notify listener |
|||
if (mGestureInProgress && mListener != null) { |
|||
mListener.onGestureUpdate(this); |
|||
} |
|||
break; |
|||
} |
|||
|
|||
case MotionEvent.ACTION_DOWN: |
|||
case MotionEvent.ACTION_POINTER_DOWN: |
|||
case MotionEvent.ACTION_POINTER_UP: |
|||
case MotionEvent.ACTION_UP: { |
|||
// restart gesture whenever the number of pointers changes |
|||
mNewPointerCount = getPressedPointerCount(event); |
|||
stopGesture(); |
|||
updatePointersOnTap(event); |
|||
if (mPointerCount > 0 && shouldStartGesture()) { |
|||
startGesture(); |
|||
} |
|||
break; |
|||
} |
|||
|
|||
case MotionEvent.ACTION_CANCEL: { |
|||
mNewPointerCount = 0; |
|||
stopGesture(); |
|||
reset(); |
|||
break; |
|||
} |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* Restarts the current gesture (if any). |
|||
*/ |
|||
public void restartGesture() { |
|||
if (!mGestureInProgress) { |
|||
return; |
|||
} |
|||
stopGesture(); |
|||
for (int i = 0; i < MAX_POINTERS; i++) { |
|||
mStartX[i] = mCurrentX[i]; |
|||
mStartY[i] = mCurrentY[i]; |
|||
} |
|||
startGesture(); |
|||
} |
|||
|
|||
/** |
|||
* Gets whether there is a gesture in progress |
|||
*/ |
|||
public boolean isGestureInProgress() { |
|||
return mGestureInProgress; |
|||
} |
|||
|
|||
/** |
|||
* Gets the number of pointers after the current gesture |
|||
*/ |
|||
public int getNewPointerCount() { |
|||
return mNewPointerCount; |
|||
} |
|||
|
|||
/** |
|||
* Gets the number of pointers in the current gesture |
|||
*/ |
|||
public int getPointerCount() { |
|||
return mPointerCount; |
|||
} |
|||
|
|||
/** |
|||
* Gets the start X coordinates for the all pointers Mutable array is exposed for performance |
|||
* reasons and is not to be modified by the callers. |
|||
*/ |
|||
public float[] getStartX() { |
|||
return mStartX; |
|||
} |
|||
|
|||
/** |
|||
* Gets the start Y coordinates for the all pointers Mutable array is exposed for performance |
|||
* reasons and is not to be modified by the callers. |
|||
*/ |
|||
public float[] getStartY() { |
|||
return mStartY; |
|||
} |
|||
|
|||
/** |
|||
* Gets the current X coordinates for the all pointers Mutable array is exposed for performance |
|||
* reasons and is not to be modified by the callers. |
|||
*/ |
|||
public float[] getCurrentX() { |
|||
return mCurrentX; |
|||
} |
|||
|
|||
/** |
|||
* Gets the current Y coordinates for the all pointers Mutable array is exposed for performance |
|||
* reasons and is not to be modified by the callers. |
|||
*/ |
|||
public float[] getCurrentY() { |
|||
return mCurrentY; |
|||
} |
|||
} |
@ -0,0 +1,51 @@ |
|||
/* |
|||
* Copyright (c) Facebook, Inc. and its affiliates. |
|||
* |
|||
* This source code is licensed under the MIT license found in the |
|||
* LICENSE file in the root directory of this source tree. |
|||
*/ |
|||
|
|||
package awais.instagrabber.customviews.drawee; |
|||
|
|||
import android.graphics.Matrix; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* An implementation of {@link ZoomableController.Listener} that allows multiple child listeners to |
|||
* be added and notified about {@link ZoomableController} events. |
|||
*/ |
|||
public class MultiZoomableControllerListener implements ZoomableController.Listener { |
|||
|
|||
private final List<ZoomableController.Listener> mListeners = new ArrayList<>(); |
|||
|
|||
@Override |
|||
public synchronized void onTransformBegin(Matrix transform) { |
|||
for (ZoomableController.Listener listener : mListeners) { |
|||
listener.onTransformBegin(transform); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public synchronized void onTransformChanged(Matrix transform) { |
|||
for (ZoomableController.Listener listener : mListeners) { |
|||
listener.onTransformChanged(transform); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public synchronized void onTransformEnd(Matrix transform) { |
|||
for (ZoomableController.Listener listener : mListeners) { |
|||
listener.onTransformEnd(transform); |
|||
} |
|||
} |
|||
|
|||
public synchronized void addListener(ZoomableController.Listener listener) { |
|||
mListeners.add(listener); |
|||
} |
|||
|
|||
public synchronized void removeListener(ZoomableController.Listener listener) { |
|||
mListeners.remove(listener); |
|||
} |
|||
} |
@ -0,0 +1,203 @@ |
|||
/* |
|||
* Copyright (c) Facebook, Inc. and its affiliates. |
|||
* |
|||
* This source code is licensed under the MIT license found in the |
|||
* LICENSE file in the root directory of this source tree. |
|||
*/ |
|||
|
|||
package awais.instagrabber.customviews.drawee; |
|||
|
|||
import android.view.MotionEvent; |
|||
|
|||
/** |
|||
* Component that detects translation, scale and rotation based on touch events. |
|||
* |
|||
* <p>This class notifies its listeners whenever a gesture begins, updates or ends. The instance of |
|||
* this detector is passed to the listeners, so it can be queried for pivot, translation, scale or |
|||
* rotation. |
|||
*/ |
|||
public class TransformGestureDetector implements MultiPointerGestureDetector.Listener { |
|||
|
|||
/** |
|||
* The listener for receiving notifications when gestures occur. |
|||
*/ |
|||
public interface Listener { |
|||
/** |
|||
* A callback called right before the gesture is about to start. |
|||
*/ |
|||
public void onGestureBegin(TransformGestureDetector detector); |
|||
|
|||
/** |
|||
* A callback called each time the gesture gets updated. |
|||
*/ |
|||
public void onGestureUpdate(TransformGestureDetector detector); |
|||
|
|||
/** |
|||
* A callback called right after the gesture has finished. |
|||
*/ |
|||
public void onGestureEnd(TransformGestureDetector detector); |
|||
} |
|||
|
|||
private final MultiPointerGestureDetector mDetector; |
|||
|
|||
private Listener mListener = null; |
|||
|
|||
public TransformGestureDetector(MultiPointerGestureDetector multiPointerGestureDetector) { |
|||
mDetector = multiPointerGestureDetector; |
|||
mDetector.setListener(this); |
|||
} |
|||
|
|||
/** |
|||
* Factory method that creates a new instance of TransformGestureDetector |
|||
*/ |
|||
public static TransformGestureDetector newInstance() { |
|||
return new TransformGestureDetector(MultiPointerGestureDetector.newInstance()); |
|||
} |
|||
|
|||
/** |
|||
* Sets the listener. |
|||
* |
|||
* @param listener listener to set |
|||
*/ |
|||
public void setListener(Listener listener) { |
|||
mListener = listener; |
|||
} |
|||
|
|||
/** |
|||
* Resets the component to the initial state. |
|||
*/ |
|||
public void reset() { |
|||
mDetector.reset(); |
|||
} |
|||
|
|||
/** |
|||
* Handles the given motion event. |
|||
* |
|||
* @param event event to handle |
|||
* @return whether or not the event was handled |
|||
*/ |
|||
public boolean onTouchEvent(final MotionEvent event) { |
|||
return mDetector.onTouchEvent(event); |
|||
} |
|||
|
|||
@Override |
|||
public void onGestureBegin(MultiPointerGestureDetector detector) { |
|||
if (mListener != null) { |
|||
mListener.onGestureBegin(this); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onGestureUpdate(MultiPointerGestureDetector detector) { |
|||
if (mListener != null) { |
|||
mListener.onGestureUpdate(this); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onGestureEnd(MultiPointerGestureDetector detector) { |
|||
if (mListener != null) { |
|||
mListener.onGestureEnd(this); |
|||
} |
|||
} |
|||
|
|||
private float calcAverage(float[] arr, int len) { |
|||
float sum = 0; |
|||
for (int i = 0; i < len; i++) { |
|||
sum += arr[i]; |
|||
} |
|||
return (len > 0) ? sum / len : 0; |
|||
} |
|||
|
|||
/** |
|||
* Restarts the current gesture (if any). |
|||
*/ |
|||
public void restartGesture() { |
|||
mDetector.restartGesture(); |
|||
} |
|||
|
|||
/** |
|||
* Gets whether there is a gesture in progress |
|||
*/ |
|||
public boolean isGestureInProgress() { |
|||
return mDetector.isGestureInProgress(); |
|||
} |
|||
|
|||
/** |
|||
* Gets the number of pointers after the current gesture |
|||
*/ |
|||
public int getNewPointerCount() { |
|||
return mDetector.getNewPointerCount(); |
|||
} |
|||
|
|||
/** |
|||
* Gets the number of pointers in the current gesture |
|||
*/ |
|||
public int getPointerCount() { |
|||
return mDetector.getPointerCount(); |
|||
} |
|||
|
|||
/** |
|||
* Gets the X coordinate of the pivot point |
|||
*/ |
|||
public float getPivotX() { |
|||
return calcAverage(mDetector.getStartX(), mDetector.getPointerCount()); |
|||
} |
|||
|
|||
/** |
|||
* Gets the Y coordinate of the pivot point |
|||
*/ |
|||
public float getPivotY() { |
|||
return calcAverage(mDetector.getStartY(), mDetector.getPointerCount()); |
|||
} |
|||
|
|||
/** |
|||
* Gets the X component of the translation |
|||
*/ |
|||
public float getTranslationX() { |
|||
return calcAverage(mDetector.getCurrentX(), mDetector.getPointerCount()) |
|||
- calcAverage(mDetector.getStartX(), mDetector.getPointerCount()); |
|||
} |
|||
|
|||
/** |
|||
* Gets the Y component of the translation |
|||
*/ |
|||
public float getTranslationY() { |
|||
return calcAverage(mDetector.getCurrentY(), mDetector.getPointerCount()) |
|||
- calcAverage(mDetector.getStartY(), mDetector.getPointerCount()); |
|||
} |
|||
|
|||
/** |
|||
* Gets the scale |
|||
*/ |
|||
public float getScale() { |
|||
if (mDetector.getPointerCount() < 2) { |
|||
return 1; |
|||
} else { |
|||
float startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0]; |
|||
float startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0]; |
|||
float currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0]; |
|||
float currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0]; |
|||
float startDist = (float) Math.hypot(startDeltaX, startDeltaY); |
|||
float currentDist = (float) Math.hypot(currentDeltaX, currentDeltaY); |
|||
return currentDist / startDist; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Gets the rotation in radians |
|||
*/ |
|||
public float getRotation() { |
|||
if (mDetector.getPointerCount() < 2) { |
|||
return 0; |
|||
} else { |
|||
float startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0]; |
|||
float startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0]; |
|||
float currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0]; |
|||
float currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0]; |
|||
float startAngle = (float) Math.atan2(startDeltaY, startDeltaX); |
|||
float currentAngle = (float) Math.atan2(currentDeltaY, currentDeltaX); |
|||
return currentAngle - startAngle; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,134 @@ |
|||
/* |
|||
* Copyright (c) Facebook, Inc. and its affiliates. |
|||
* |
|||
* This source code is licensed under the MIT license found in the |
|||
* LICENSE file in the root directory of this source tree. |
|||
*/ |
|||
|
|||
package awais.instagrabber.customviews.drawee; |
|||
|
|||
import android.graphics.Matrix; |
|||
import android.graphics.RectF; |
|||
import android.view.MotionEvent; |
|||
|
|||
/** |
|||
* Interface for implementing a controller that works with {@link ZoomableDraweeView} to control the |
|||
* zoom. |
|||
*/ |
|||
public interface ZoomableController { |
|||
|
|||
/** |
|||
* Listener interface. |
|||
*/ |
|||
interface Listener { |
|||
|
|||
/** |
|||
* Notifies the view that the transform began. |
|||
* |
|||
* @param transform the current transform matrix |
|||
*/ |
|||
void onTransformBegin(Matrix transform); |
|||
|
|||
/** |
|||
* Notifies the view that the transform changed. |
|||
* |
|||
* @param transform the new matrix |
|||
*/ |
|||
void onTransformChanged(Matrix transform); |
|||
|
|||
/** |
|||
* Notifies the view that the transform ended. |
|||
* |
|||
* @param transform the current transform matrix |
|||
*/ |
|||
void onTransformEnd(Matrix transform); |
|||
} |
|||
|
|||
/** |
|||
* Enables the controller. The controller is enabled when the image has been loaded. |
|||
* |
|||
* @param enabled whether to enable the controller |
|||
*/ |
|||
void setEnabled(boolean enabled); |
|||
|
|||
/** |
|||
* Gets whether the controller is enabled. This should return the last value passed to {@link |
|||
* #setEnabled}. |
|||
* |
|||
* @return whether the controller is enabled. |
|||
*/ |
|||
boolean isEnabled(); |
|||
|
|||
/** |
|||
* Sets the listener for the controller to call back when the matrix changes. |
|||
* |
|||
* @param listener the listener |
|||
*/ |
|||
void setListener(Listener listener); |
|||
|
|||
/** |
|||
* Gets the current scale factor. A convenience method for calculating the scale from the |
|||
* transform. |
|||
* |
|||
* @return the current scale factor |
|||
*/ |
|||
float getScaleFactor(); |
|||
|
|||
/** |
|||
* Returns true if the zoomable transform is identity matrix, and the controller is idle. |
|||
*/ |
|||
boolean isIdentity(); |
|||
|
|||
/** |
|||
* Returns true if the transform was corrected during the last update. |
|||
* |
|||
* <p>This mainly happens when a gesture would cause the image to get out of limits and the |
|||
* transform gets corrected in order to prevent that. |
|||
*/ |
|||
boolean wasTransformCorrected(); |
|||
|
|||
/** |
|||
* See {@link androidx.core.view.ScrollingView}. |
|||
*/ |
|||
int computeHorizontalScrollRange(); |
|||
|
|||
int computeHorizontalScrollOffset(); |
|||
|
|||
int computeHorizontalScrollExtent(); |
|||
|
|||
int computeVerticalScrollRange(); |
|||
|
|||
int computeVerticalScrollOffset(); |
|||
|
|||
int computeVerticalScrollExtent(); |
|||
|
|||
/** |
|||
* Gets the current transform. |
|||
* |
|||
* @return the transform |
|||
*/ |
|||
Matrix getTransform(); |
|||
|
|||
/** |
|||
* Sets the bounds of the image post transform prior to application of the zoomable |
|||
* transformation. |
|||
* |
|||
* @param imageBounds the bounds of the image |
|||
*/ |
|||
void setImageBounds(RectF imageBounds); |
|||
|
|||
/** |
|||
* Sets the bounds of the view. |
|||
* |
|||
* @param viewBounds the bounds of the view |
|||
*/ |
|||
void setViewBounds(RectF viewBounds); |
|||
|
|||
/** |
|||
* Allows the controller to handle a touch event. |
|||
* |
|||
* @param event the touch event |
|||
* @return whether the controller handled the event |
|||
*/ |
|||
boolean onTouchEvent(MotionEvent event); |
|||
} |
@ -0,0 +1,416 @@ |
|||
/* |
|||
* Copyright (c) Facebook, Inc. and its affiliates. |
|||
* |
|||
* This source code is licensed under the MIT license found in the |
|||
* LICENSE file in the root directory of this source tree. |
|||
*/ |
|||
|
|||
package awais.instagrabber.customviews.drawee; |
|||
|
|||
import android.content.Context; |
|||
import android.content.res.Resources; |
|||
import android.graphics.Canvas; |
|||
import android.graphics.Matrix; |
|||
import android.graphics.RectF; |
|||
import android.graphics.drawable.Animatable; |
|||
import android.util.AttributeSet; |
|||
import android.view.GestureDetector; |
|||
import android.view.MotionEvent; |
|||
import android.view.ViewParent; |
|||
|
|||
import androidx.annotation.Nullable; |
|||
import androidx.core.view.ScrollingView; |
|||
|
|||
import com.facebook.common.internal.Preconditions; |
|||
import com.facebook.common.logging.FLog; |
|||
import com.facebook.drawee.controller.AbstractDraweeController; |
|||
import com.facebook.drawee.controller.BaseControllerListener; |
|||
import com.facebook.drawee.controller.ControllerListener; |
|||
import com.facebook.drawee.drawable.ScalingUtils; |
|||
import com.facebook.drawee.generic.GenericDraweeHierarchy; |
|||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; |
|||
import com.facebook.drawee.generic.GenericDraweeHierarchyInflater; |
|||
import com.facebook.drawee.interfaces.DraweeController; |
|||
import com.facebook.drawee.view.DraweeView; |
|||
|
|||
|
|||
/** |
|||
* DraweeView that has zoomable capabilities. |
|||
* |
|||
* <p>Once the image loads, pinch-to-zoom and translation gestures are enabled. |
|||
*/ |
|||
public class ZoomableDraweeView extends DraweeView<GenericDraweeHierarchy> |
|||
implements ScrollingView { |
|||
|
|||
private static final Class<?> TAG = ZoomableDraweeView.class; |
|||
|
|||
private static final float HUGE_IMAGE_SCALE_FACTOR_THRESHOLD = 1.1f; |
|||
|
|||
private final RectF mImageBounds = new RectF(); |
|||
private final RectF mViewBounds = new RectF(); |
|||
|
|||
private DraweeController mHugeImageController; |
|||
private ZoomableController mZoomableController; |
|||
private GestureDetector mTapGestureDetector; |
|||
private boolean mAllowTouchInterceptionWhileZoomed = true; |
|||
|
|||
private boolean mIsDialtoneEnabled = false; |
|||
private boolean mZoomingEnabled = true; |
|||
|
|||
private final ControllerListener mControllerListener = |
|||
new BaseControllerListener<Object>() { |
|||
@Override |
|||
public void onFinalImageSet( |
|||
String id, @Nullable Object imageInfo, @Nullable Animatable animatable) { |
|||
ZoomableDraweeView.this.onFinalImageSet(); |
|||
} |
|||
|
|||
@Override |
|||
public void onRelease(String id) { |
|||
ZoomableDraweeView.this.onRelease(); |
|||
} |
|||
}; |
|||
|
|||
private final ZoomableController.Listener mZoomableListener = |
|||
new ZoomableController.Listener() { |
|||
@Override |
|||
public void onTransformBegin(Matrix transform) {} |
|||
|
|||
@Override |
|||
public void onTransformChanged(Matrix transform) { |
|||
ZoomableDraweeView.this.onTransformChanged(transform); |
|||
} |
|||
|
|||
@Override |
|||
public void onTransformEnd(Matrix transform) {} |
|||
}; |
|||
|
|||
private final GestureListenerWrapper mTapListenerWrapper = new GestureListenerWrapper(); |
|||
|
|||
public ZoomableDraweeView(Context context, GenericDraweeHierarchy hierarchy) { |
|||
super(context); |
|||
setHierarchy(hierarchy); |
|||
init(); |
|||
} |
|||
|
|||
public ZoomableDraweeView(Context context) { |
|||
super(context); |
|||
inflateHierarchy(context, null); |
|||
init(); |
|||
} |
|||
|
|||
public ZoomableDraweeView(Context context, AttributeSet attrs) { |
|||
super(context, attrs); |
|||
inflateHierarchy(context, attrs); |
|||
init(); |
|||
} |
|||
|
|||
public ZoomableDraweeView(Context context, AttributeSet attrs, int defStyle) { |
|||
super(context, attrs, defStyle); |
|||
inflateHierarchy(context, attrs); |
|||
init(); |
|||
} |
|||
|
|||
protected void inflateHierarchy(Context context, @Nullable AttributeSet attrs) { |
|||
Resources resources = context.getResources(); |
|||
GenericDraweeHierarchyBuilder builder = |
|||
new GenericDraweeHierarchyBuilder(resources) |
|||
.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER); |
|||
GenericDraweeHierarchyInflater.updateBuilder(builder, context, attrs); |
|||
setAspectRatio(builder.getDesiredAspectRatio()); |
|||
setHierarchy(builder.build()); |
|||
} |
|||
|
|||
private void init() { |
|||
mZoomableController = createZoomableController(); |
|||
mZoomableController.setListener(mZoomableListener); |
|||
mTapGestureDetector = new GestureDetector(getContext(), mTapListenerWrapper); |
|||
} |
|||
|
|||
public void setIsDialtoneEnabled(boolean isDialtoneEnabled) { |
|||
mIsDialtoneEnabled = isDialtoneEnabled; |
|||
} |
|||
|
|||
/** |
|||
* Gets the original image bounds, in view-absolute coordinates. |
|||
* |
|||
* <p>The original image bounds are those reported by the hierarchy. The hierarchy itself may |
|||
* apply scaling on its own (e.g. due to scale type) so the reported bounds are not necessarily |
|||
* the same as the actual bitmap dimensions. In other words, the original image bounds correspond |
|||
* to the image bounds within this view when no zoomable transformation is applied, but including |
|||
* the potential scaling of the hierarchy. Having the actual bitmap dimensions abstracted away |
|||
* from this view greatly simplifies implementation because the actual bitmap may change (e.g. |
|||
* when a high-res image arrives and replaces the previously set low-res image). With proper |
|||
* hierarchy scaling (e.g. FIT_CENTER), this underlying change will not affect this view nor the |
|||
* zoomable transformation in any way. |
|||
*/ |
|||
protected void getImageBounds(RectF outBounds) { |
|||
getHierarchy().getActualImageBounds(outBounds); |
|||
} |
|||
|
|||
/** |
|||
* Gets the bounds used to limit the translation, in view-absolute coordinates. |
|||
* |
|||
* <p>These bounds are passed to the zoomable controller in order to limit the translation. The |
|||
* image is attempted to be centered within the limit bounds if the transformed image is smaller. |
|||
* There will be no empty spaces within the limit bounds if the transformed image is bigger. This |
|||
* applies to each dimension (horizontal and vertical) independently. |
|||
* |
|||
* <p>Unless overridden by a subclass, these bounds are same as the view bounds. |
|||
*/ |
|||
protected void getLimitBounds(RectF outBounds) { |
|||
outBounds.set(0, 0, getWidth(), getHeight()); |
|||
} |
|||
|
|||
/** |
|||
* Sets a custom zoomable controller, instead of using the default one. |
|||
*/ |
|||
public void setZoomableController(ZoomableController zoomableController) { |
|||
Preconditions.checkNotNull(zoomableController); |
|||
mZoomableController.setListener(null); |
|||
mZoomableController = zoomableController; |
|||
mZoomableController.setListener(mZoomableListener); |
|||
} |
|||
|
|||
/** |
|||
* Gets the zoomable controller. |
|||
* |
|||
* <p>Zoomable controller can be used to zoom to point, or to map point from view to image |
|||
* coordinates for instance. |
|||
*/ |
|||
public ZoomableController getZoomableController() { |
|||
return mZoomableController; |
|||
} |
|||
|
|||
/** |
|||
* Check whether the parent view can intercept touch events while zoomed. This can be used, for |
|||
* example, to swipe between images in a view pager while zoomed. |
|||
* |
|||
* @return true if touch events can be intercepted |
|||
*/ |
|||
public boolean allowsTouchInterceptionWhileZoomed() { |
|||
return mAllowTouchInterceptionWhileZoomed; |
|||
} |
|||
|
|||
/** |
|||
* If this is set to true, parent views can intercept touch events while the view is zoomed. For |
|||
* example, this can be used to swipe between images in a view pager while zoomed. |
|||
* |
|||
* @param allowTouchInterceptionWhileZoomed true if the parent needs to intercept touches |
|||
*/ |
|||
public void setAllowTouchInterceptionWhileZoomed(boolean allowTouchInterceptionWhileZoomed) { |
|||
mAllowTouchInterceptionWhileZoomed = allowTouchInterceptionWhileZoomed; |
|||
} |
|||
|
|||
/** |
|||
* Sets the tap listener. |
|||
*/ |
|||
public void setTapListener(GestureDetector.SimpleOnGestureListener tapListener) { |
|||
mTapListenerWrapper.setListener(tapListener); |
|||
} |
|||
|
|||
/** |
|||
* Sets whether long-press tap detection is enabled. Unfortunately, long-press conflicts with |
|||
* onDoubleTapEvent. |
|||
*/ |
|||
public void setIsLongpressEnabled(boolean enabled) { |
|||
mTapGestureDetector.setIsLongpressEnabled(enabled); |
|||
} |
|||
|
|||
public void setZoomingEnabled(boolean zoomingEnabled) { |
|||
mZoomingEnabled = zoomingEnabled; |
|||
mZoomableController.setEnabled(false); |
|||
} |
|||
|
|||
/** |
|||
* Sets the image controller. |
|||
*/ |
|||
@Override |
|||
public void setController(@Nullable DraweeController controller) { |
|||
setControllers(controller, null); |
|||
} |
|||
|
|||
/** |
|||
* Sets the controllers for the normal and huge image. |
|||
* |
|||
* <p>The huge image controller is used after the image gets scaled above a certain threshold. |
|||
* |
|||
* <p>IMPORTANT: in order to avoid a flicker when switching to the huge image, the huge image |
|||
* controller should have the normal-image-uri set as its low-res-uri. |
|||
* |
|||
* @param controller controller to be initially used |
|||
* @param hugeImageController controller to be used after the client starts zooming-in |
|||
*/ |
|||
public void setControllers( |
|||
@Nullable DraweeController controller, @Nullable DraweeController hugeImageController) { |
|||
setControllersInternal(null, null); |
|||
mZoomableController.setEnabled(false); |
|||
setControllersInternal(controller, hugeImageController); |
|||
} |
|||
|
|||
private void setControllersInternal( |
|||
@Nullable DraweeController controller, @Nullable DraweeController hugeImageController) { |
|||
removeControllerListener(getController()); |
|||
addControllerListener(controller); |
|||
mHugeImageController = hugeImageController; |
|||
super.setController(controller); |
|||
} |
|||
|
|||
private void maybeSetHugeImageController() { |
|||
if (mHugeImageController != null |
|||
&& mZoomableController.getScaleFactor() > HUGE_IMAGE_SCALE_FACTOR_THRESHOLD) { |
|||
setControllersInternal(mHugeImageController, null); |
|||
} |
|||
} |
|||
|
|||
private void removeControllerListener(DraweeController controller) { |
|||
if (controller instanceof AbstractDraweeController) { |
|||
((AbstractDraweeController) controller).removeControllerListener(mControllerListener); |
|||
} |
|||
} |
|||
|
|||
private void addControllerListener(DraweeController controller) { |
|||
if (controller instanceof AbstractDraweeController) { |
|||
((AbstractDraweeController) controller).addControllerListener(mControllerListener); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
protected void onDraw(Canvas canvas) { |
|||
int saveCount = canvas.save(); |
|||
canvas.concat(mZoomableController.getTransform()); |
|||
try { |
|||
super.onDraw(canvas); |
|||
} catch (Exception e) { |
|||
DraweeController controller = getController(); |
|||
if (controller != null && controller instanceof AbstractDraweeController) { |
|||
Object callerContext = ((AbstractDraweeController) controller).getCallerContext(); |
|||
if (callerContext != null) { |
|||
throw new RuntimeException( |
|||
String.format("Exception in onDraw, callerContext=%s", callerContext.toString()), e); |
|||
} |
|||
} |
|||
throw e; |
|||
} |
|||
canvas.restoreToCount(saveCount); |
|||
} |
|||
|
|||
@Override |
|||
public boolean onTouchEvent(MotionEvent event) { |
|||
int a = event.getActionMasked(); |
|||
FLog.v(getLogTag(), "onTouchEvent: %d, view %x, received", a, this.hashCode()); |
|||
if (!mIsDialtoneEnabled && mTapGestureDetector.onTouchEvent(event)) { |
|||
FLog.v( |
|||
getLogTag(), |
|||
"onTouchEvent: %d, view %x, handled by tap gesture detector", |
|||
a, |
|||
this.hashCode()); |
|||
return true; |
|||
} |
|||
|
|||
if (!mIsDialtoneEnabled && mZoomableController.onTouchEvent(event)) { |
|||
FLog.v( |
|||
getLogTag(), |
|||
"onTouchEvent: %d, view %x, handled by zoomable controller", |
|||
a, |
|||
this.hashCode()); |
|||
if (!mAllowTouchInterceptionWhileZoomed && !mZoomableController.isIdentity()) { |
|||
final ViewParent parent = getParent(); |
|||
parent.requestDisallowInterceptTouchEvent(true); |
|||
} |
|||
return true; |
|||
} |
|||
if (super.onTouchEvent(event)) { |
|||
FLog.v(getLogTag(), "onTouchEvent: %d, view %x, handled by the super", a, this.hashCode()); |
|||
return true; |
|||
} |
|||
// None of our components reported that they handled the touch event. Upon returning false |
|||
// from this method, our parent won't send us any more events for this gesture. Unfortunately, |
|||
// some components may have started a delayed action, such as a long-press timer, and since we |
|||
// won't receive an ACTION_UP that would cancel that timer, a false event may be triggered. |
|||
// To prevent that we explicitly send one last cancel event when returning false. |
|||
MotionEvent cancelEvent = MotionEvent.obtain(event); |
|||
cancelEvent.setAction(MotionEvent.ACTION_CANCEL); |
|||
mTapGestureDetector.onTouchEvent(cancelEvent); |
|||
mZoomableController.onTouchEvent(cancelEvent); |
|||
cancelEvent.recycle(); |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public int computeHorizontalScrollRange() { |
|||
return mZoomableController.computeHorizontalScrollRange(); |
|||
} |
|||
|
|||
@Override |
|||
public int computeHorizontalScrollOffset() { |
|||
return mZoomableController.computeHorizontalScrollOffset(); |
|||
} |
|||
|
|||
@Override |
|||
public int computeHorizontalScrollExtent() { |
|||
return mZoomableController.computeHorizontalScrollExtent(); |
|||
} |
|||
|
|||
@Override |
|||
public int computeVerticalScrollRange() { |
|||
return mZoomableController.computeVerticalScrollRange(); |
|||
} |
|||
|
|||
@Override |
|||
public int computeVerticalScrollOffset() { |
|||
return mZoomableController.computeVerticalScrollOffset(); |
|||
} |
|||
|
|||
@Override |
|||
public int computeVerticalScrollExtent() { |
|||
return mZoomableController.computeVerticalScrollExtent(); |
|||
} |
|||
|
|||
@Override |
|||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
|||
FLog.v(getLogTag(), "onLayout: view %x", this.hashCode()); |
|||
super.onLayout(changed, left, top, right, bottom); |
|||
updateZoomableControllerBounds(); |
|||
} |
|||
|
|||
private void onFinalImageSet() { |
|||
FLog.v(getLogTag(), "onFinalImageSet: view %x", this.hashCode()); |
|||
if (!mZoomableController.isEnabled() && mZoomingEnabled) { |
|||
mZoomableController.setEnabled(true); |
|||
updateZoomableControllerBounds(); |
|||
} |
|||
} |
|||
|
|||
private void onRelease() { |
|||
FLog.v(getLogTag(), "onRelease: view %x", this.hashCode()); |
|||
mZoomableController.setEnabled(false); |
|||
} |
|||
|
|||
protected void onTransformChanged(Matrix transform) { |
|||
FLog.v(getLogTag(), "onTransformChanged: view %x, transform: %s", this.hashCode(), transform); |
|||
maybeSetHugeImageController(); |
|||
invalidate(); |
|||
} |
|||
|
|||
protected void updateZoomableControllerBounds() { |
|||
getImageBounds(mImageBounds); |
|||
getLimitBounds(mViewBounds); |
|||
mZoomableController.setImageBounds(mImageBounds); |
|||
mZoomableController.setViewBounds(mViewBounds); |
|||
FLog.v( |
|||
getLogTag(), |
|||
"updateZoomableControllerBounds: view %x, view bounds: %s, image bounds: %s", |
|||
this.hashCode(), |
|||
mViewBounds, |
|||
mImageBounds); |
|||
} |
|||
|
|||
protected Class<?> getLogTag() { |
|||
return TAG; |
|||
} |
|||
|
|||
protected ZoomableController createZoomableController() { |
|||
return AnimatedZoomableController.newInstance(); |
|||
} |
|||
} |
@ -0,0 +1,43 @@ |
|||
package awais.instagrabber.customviews.helpers; |
|||
|
|||
import android.graphics.drawable.Animatable; |
|||
import android.view.ViewGroup; |
|||
|
|||
import com.facebook.drawee.controller.BaseControllerListener; |
|||
import com.facebook.drawee.generic.GenericDraweeHierarchy; |
|||
import com.facebook.drawee.view.DraweeView; |
|||
import com.facebook.imagepipeline.image.ImageInfo; |
|||
|
|||
import awais.instagrabber.utils.Utils; |
|||
|
|||
public class ImageResizingControllerListener<T extends DraweeView<GenericDraweeHierarchy>> extends BaseControllerListener<ImageInfo> { |
|||
private static final String TAG = "ImageResizingController"; |
|||
|
|||
private T imageView; |
|||
private final int requiredWidth; |
|||
|
|||
public ImageResizingControllerListener(final T imageView, final int requiredWidth) { |
|||
this.imageView = imageView; |
|||
this.requiredWidth = requiredWidth; |
|||
} |
|||
|
|||
@Override |
|||
public void onIntermediateImageSet(final String id, final ImageInfo imageInfo) { |
|||
super.onIntermediateImageSet(id, imageInfo); |
|||
} |
|||
|
|||
public void onFinalImageSet(String id, ImageInfo imageInfo, Animatable animatable) { |
|||
if (imageInfo != null) { |
|||
// updateViewSize(imageInfo); |
|||
final int height = imageInfo.getHeight(); |
|||
final int width = imageInfo.getWidth(); |
|||
// final float aspectRatio = ((float) width) / height; |
|||
final ViewGroup.LayoutParams layoutParams = imageView.getLayoutParams(); |
|||
// final int deviceWidth = Utils.displayMetrics.widthPixels; |
|||
final int resultingHeight = Utils.getResultingHeight(requiredWidth, height, width); |
|||
layoutParams.width = requiredWidth; |
|||
layoutParams.height = resultingHeight; |
|||
imageView.requestLayout(); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,45 @@ |
|||
package awais.instagrabber.customviews.helpers; |
|||
|
|||
import androidx.recyclerview.widget.RecyclerView; |
|||
|
|||
import com.bumptech.glide.RequestManager; |
|||
|
|||
import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_DRAGGING; |
|||
import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE; |
|||
|
|||
public class PauseGlideOnFlingScrollListener extends RecyclerView.OnScrollListener { |
|||
private static final int FLING_JUMP_LOW_THRESHOLD = 80; |
|||
private static final int FLING_JUMP_HIGH_THRESHOLD = 120; |
|||
|
|||
private final RequestManager glide; |
|||
private boolean dragging = false; |
|||
|
|||
public PauseGlideOnFlingScrollListener(final RequestManager glide) { |
|||
this.glide = glide; |
|||
} |
|||
|
|||
@Override |
|||
public void onScrollStateChanged(RecyclerView recyclerView, int newState) { |
|||
dragging = newState == SCROLL_STATE_DRAGGING; |
|||
if (glide.isPaused()) { |
|||
if (newState == SCROLL_STATE_DRAGGING || newState == SCROLL_STATE_IDLE) { |
|||
// user is touchy or the scroll finished, show images |
|||
glide.resumeRequests(); |
|||
} // settling means the user let the screen go, but it can still be flinging |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) { |
|||
if (!dragging) { |
|||
// TODO can be made better by a rolling average of last N calls to smooth out patterns like a,b,a |
|||
int currentSpeed = Math.abs(dy); |
|||
boolean paused = glide.isPaused(); |
|||
if (paused && currentSpeed < FLING_JUMP_LOW_THRESHOLD) { |
|||
glide.resumeRequests(); |
|||
} else if (!paused && FLING_JUMP_HIGH_THRESHOLD < currentSpeed) { |
|||
glide.pauseRequests(); |
|||
} |
|||
} |
|||
} |
|||
} |
@ -1,297 +1,318 @@ |
|||
package awais.instagrabber.customviews.helpers; |
|||
|
|||
import android.app.Activity; |
|||
import android.content.Context; |
|||
import android.content.Intent; |
|||
import android.graphics.Point; |
|||
import android.graphics.Rect; |
|||
import android.net.Uri; |
|||
import android.view.View; |
|||
import android.widget.ImageView; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.recyclerview.widget.LinearLayoutManager; |
|||
import androidx.recyclerview.widget.RecyclerView; |
|||
|
|||
import com.google.android.exoplayer2.Player; |
|||
import com.google.android.exoplayer2.SimpleExoPlayer; |
|||
import com.google.android.exoplayer2.source.ProgressiveMediaSource; |
|||
import com.google.android.exoplayer2.ui.PlayerView; |
|||
import com.google.android.exoplayer2.upstream.DataSource; |
|||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; |
|||
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; |
|||
import com.google.android.exoplayer2.upstream.cache.SimpleCache; |
|||
|
|||
import java.util.List; |
|||
|
|||
import awais.instagrabber.R; |
|||
import awais.instagrabber.activities.CommentsViewer; |
|||
import awais.instagrabber.adapters.FeedAdapter; |
|||
import awais.instagrabber.adapters.viewholder.feed.FeedVideoViewHolder; |
|||
import awais.instagrabber.models.FeedModel; |
|||
import awais.instagrabber.utils.Constants; |
|||
import awais.instagrabber.utils.Utils; |
|||
|
|||
import static awais.instagrabber.utils.Utils.settingsHelper; |
|||
import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_DRAGGING; |
|||
import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE; |
|||
|
|||
public class VideoAwareRecyclerScroller extends RecyclerView.OnScrollListener { |
|||
private static final String TAG = "VideoAwareRecScroll"; |
|||
private static final int FLING_JUMP_LOW_THRESHOLD = 80; |
|||
private static final int FLING_JUMP_HIGH_THRESHOLD = 120; |
|||
private static final Object LOCK = new Object(); |
|||
|
|||
private LinearLayoutManager layoutManager; |
|||
private View firstItemView, lastItemView; |
|||
private int videoPosShown = -1, lastVideoPos = -1, lastChangedVideoPos, lastStoppedVideoPos, lastPlayedVideoPos; |
|||
private boolean videoAttached = false; |
|||
private SimpleExoPlayer player; |
|||
private ImageView btnMute; |
|||
private CacheDataSourceFactory cacheDataSourceFactory; |
|||
|
|||
private final List<FeedModel> feedModels; |
|||
private final Context context; |
|||
private final VideoChangeCallback videoChangeCallback; |
|||
private final DefaultDataSourceFactory dataSourceFactory; |
|||
|
|||
private final View.OnClickListener commentClickListener = new View.OnClickListener() { |
|||
@Override |
|||
public void onClick(@NonNull final View v) { |
|||
final Object tag = v.getTag(); |
|||
if (tag instanceof FeedModel && context instanceof Activity) { |
|||
if (player != null) player.setPlayWhenReady(false); |
|||
((Activity) context).startActivityForResult(new Intent(context, CommentsViewer.class) |
|||
.putExtra(Constants.EXTRAS_SHORTCODE, ((FeedModel) tag).getShortCode()) |
|||
.putExtra(Constants.EXTRAS_POST, ((FeedModel) tag).getPostId()) |
|||
.putExtra(Constants.EXTRAS_POST, ((FeedModel) tag).getProfileModel().getId()), 6969); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
private final View.OnClickListener muteClickListener = v -> { |
|||
if (player == null) return; |
|||
final float intVol = player.getVolume() == 0f ? 1f : 0f; |
|||
player.setVolume(intVol); |
|||
if (btnMute != null) |
|||
btnMute.setImageResource(intVol == 0f ? R.drawable.mute : R.drawable.vol); |
|||
Utils.sessionVolumeFull = intVol == 1f; |
|||
}; |
|||
private boolean dragging; |
|||
private boolean isLoadingPaused = false; |
|||
private FeedVideoViewHolder currentlyPlayingViewHolder; |
|||
|
|||
public VideoAwareRecyclerScroller(final Context context, final List<FeedModel> feedModels, |
|||
final VideoChangeCallback videoChangeCallback) { |
|||
this.context = context; |
|||
this.feedModels = feedModels; |
|||
this.videoChangeCallback = videoChangeCallback; |
|||
dataSourceFactory = new DefaultDataSourceFactory(context, "instagram"); |
|||
final SimpleCache simpleCache = Utils.getSimpleCacheInstance(context); |
|||
if (simpleCache != null) { |
|||
cacheDataSourceFactory = new CacheDataSourceFactory(simpleCache, dataSourceFactory); |
|||
@Override |
|||
public void onScrollStateChanged(@NonNull final RecyclerView recyclerView, final int newState) { |
|||
dragging = newState == SCROLL_STATE_DRAGGING; |
|||
if (isLoadingPaused) { |
|||
if (newState == SCROLL_STATE_DRAGGING || newState == SCROLL_STATE_IDLE) { |
|||
// user is touchy or the scroll finished, show videos |
|||
isLoadingPaused = false; |
|||
} // settling means the user let the screen go, but it can still be flinging |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) { |
|||
if (!dragging) { |
|||
// TODO can be made better by a rolling average of last N calls to smooth out patterns like a,b,a |
|||
int currentSpeed = Math.abs(dy); |
|||
if (isLoadingPaused && currentSpeed < FLING_JUMP_LOW_THRESHOLD) { |
|||
isLoadingPaused = false; |
|||
} else if (!isLoadingPaused && FLING_JUMP_HIGH_THRESHOLD < currentSpeed) { |
|||
isLoadingPaused = true; |
|||
// stop playing video |
|||
} |
|||
} |
|||
if (isLoadingPaused) return; |
|||
if (layoutManager == null) { |
|||
final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); |
|||
if (layoutManager instanceof LinearLayoutManager) |
|||
this.layoutManager = (LinearLayoutManager) layoutManager; |
|||
} |
|||
if (feedModels.size() == 0 || layoutManager == null) { |
|||
if (layoutManager == null) { |
|||
return; |
|||
} |
|||
int firstVisibleItemPos = layoutManager.findFirstCompletelyVisibleItemPosition(); |
|||
int lastVisibleItemPos = layoutManager.findLastCompletelyVisibleItemPosition(); |
|||
|
|||
if (firstVisibleItemPos == -1 && lastVisibleItemPos == -1) { |
|||
firstVisibleItemPos = layoutManager.findFirstVisibleItemPosition(); |
|||
lastVisibleItemPos = layoutManager.findLastVisibleItemPosition(); |
|||
} |
|||
|
|||
boolean processFirstItem = false, processLastItem = false; |
|||
View currView; |
|||
if (firstVisibleItemPos != -1) { |
|||
currView = layoutManager.findViewByPosition(firstVisibleItemPos); |
|||
if (currView != null && currView.getId() == R.id.videoHolder) { |
|||
firstItemView = currView; |
|||
processFirstItem = true; |
|||
} |
|||
} |
|||
if (lastVisibleItemPos != -1) { |
|||
currView = layoutManager.findViewByPosition(lastVisibleItemPos); |
|||
if (currView != null && currView.getId() == R.id.videoHolder) { |
|||
lastItemView = currView; |
|||
processLastItem = true; |
|||
} |
|||
} |
|||
|
|||
final Rect visibleItemRect = new Rect(); |
|||
|
|||
int firstVisibleItemHeight = 0, lastVisibleItemHeight = 0; |
|||
|
|||
final boolean isFirstItemVideoHolder = firstItemView != null && firstItemView.getId() == R.id.videoHolder; |
|||
if (isFirstItemVideoHolder) { |
|||
firstItemView.getGlobalVisibleRect(visibleItemRect); |
|||
firstVisibleItemHeight = visibleItemRect.height(); |
|||
} |
|||
final boolean isLastItemVideoHolder = lastItemView != null && lastItemView.getId() == R.id.videoHolder; |
|||
if (isLastItemVideoHolder) { |
|||
lastItemView.getGlobalVisibleRect(visibleItemRect); |
|||
lastVisibleItemHeight = visibleItemRect.height(); |
|||
} |
|||
|
|||
if (processFirstItem && firstVisibleItemHeight > lastVisibleItemHeight) |
|||
videoPosShown = firstVisibleItemPos; |
|||
else if (processLastItem && lastVisibleItemHeight != 0) videoPosShown = lastVisibleItemPos; |
|||
|
|||
if (firstItemView != lastItemView) { |
|||
final int mox = lastVisibleItemHeight - firstVisibleItemHeight; |
|||
if (processLastItem && lastVisibleItemHeight > firstVisibleItemHeight) |
|||
videoPosShown = lastVisibleItemPos; |
|||
if ((processFirstItem || processLastItem) && mox >= 0) |
|||
videoPosShown = lastVisibleItemPos; |
|||
} |
|||
|
|||
if (lastChangedVideoPos != -1 && lastVideoPos != -1) { |
|||
currView = layoutManager.findViewByPosition(lastChangedVideoPos); |
|||
if (currView != null && currView.getId() == R.id.videoHolder && |
|||
lastStoppedVideoPos != lastChangedVideoPos && lastPlayedVideoPos != lastChangedVideoPos) { |
|||
lastStoppedVideoPos = lastChangedVideoPos; |
|||
stopVideo(lastChangedVideoPos, recyclerView, currView); |
|||
} |
|||
|
|||
currView = layoutManager.findViewByPosition(lastVideoPos); |
|||
if (currView != null && currView.getId() == R.id.videoHolder) { |
|||
final Rect rect = new Rect(); |
|||
currView.getGlobalVisibleRect(rect); |
|||
|
|||
final int holderTop = currView.getTop(); |
|||
final int holderHeight = currView.getBottom() - holderTop; |
|||
final int halfHeight = holderHeight / 2; |
|||
//halfHeight -= halfHeight / 5; |
|||
|
|||
if (rect.height() < halfHeight) { |
|||
if (lastStoppedVideoPos != lastVideoPos) { |
|||
lastStoppedVideoPos = lastVideoPos; |
|||
stopVideo(lastVideoPos, recyclerView, currView); |
|||
} |
|||
} else if (lastPlayedVideoPos != lastVideoPos) { |
|||
lastPlayedVideoPos = lastVideoPos; |
|||
playVideo(lastVideoPos, recyclerView, currView); |
|||
synchronized (LOCK) { |
|||
final FeedVideoViewHolder videoHolder = getFirstVideoHolder(recyclerView, firstVisibleItemPos, lastVisibleItemPos); |
|||
if (videoHolder == null || videoHolder.getCurrentFeedModel() == null) { |
|||
if (currentlyPlayingViewHolder != null) { |
|||
currentlyPlayingViewHolder.stopPlaying(); |
|||
currentlyPlayingViewHolder = null; |
|||
} |
|||
return; |
|||
} |
|||
|
|||
if (lastChangedVideoPos != lastVideoPos) lastChangedVideoPos = lastVideoPos; |
|||
} |
|||
|
|||
if (lastVideoPos != -1 && lastVideoPos != videoPosShown) { |
|||
if (videoAttached) { |
|||
//if ((currView = layoutManager.findViewByPosition(lastVideoPos)) != null && currView.getId() == R.id.videoHolder) |
|||
releaseVideo(lastVideoPos, recyclerView, null); |
|||
videoAttached = false; |
|||
if (currentlyPlayingViewHolder != null && currentlyPlayingViewHolder.getCurrentFeedModel().getPostId() |
|||
.equals(videoHolder.getCurrentFeedModel().getPostId())) { |
|||
return; |
|||
} |
|||
} |
|||
if (videoPosShown != -1) { |
|||
lastVideoPos = videoPosShown; |
|||
if (!videoAttached) { |
|||
if ((currView = layoutManager.findViewByPosition(videoPosShown)) != null && currView.getId() == R.id.videoHolder) |
|||
attachVideo(videoPosShown, recyclerView, currView); |
|||
videoAttached = true; |
|||
if (currentlyPlayingViewHolder != null) { |
|||
currentlyPlayingViewHolder.stopPlaying(); |
|||
} |
|||
videoHolder.startPlaying(); |
|||
currentlyPlayingViewHolder = videoHolder; |
|||
} |
|||
// boolean processFirstItem = false, processLastItem = false; |
|||
// View currView; |
|||
// if (firstVisibleItemPos != -1) { |
|||
// currView = layoutManager.findViewByPosition(firstVisibleItemPos); |
|||
// if (currView != null && currView.getId() == R.id.videoHolder) { |
|||
// firstItemView = currView; |
|||
// // processFirstItem = true; |
|||
// } |
|||
// } |
|||
// if (lastVisibleItemPos != -1) { |
|||
// currView = layoutManager.findViewByPosition(lastVisibleItemPos); |
|||
// if (currView != null && currView.getId() == R.id.videoHolder) { |
|||
// lastItemView = currView; |
|||
// // processLastItem = true; |
|||
// } |
|||
// } |
|||
// if (firstItemView == null && lastItemView == null) { |
|||
// return; |
|||
// } |
|||
// if (firstItemView != null) { |
|||
// |
|||
// Log.d(TAG, "view" + viewHolder); |
|||
// } |
|||
// if (lastItemView != null) { |
|||
// final FeedVideoViewHolder viewHolder = (FeedVideoViewHolder) recyclerView.getChildViewHolder(lastItemView); |
|||
// Log.d(TAG, "view" + viewHolder); |
|||
// } |
|||
// Log.d(TAG, firstItemView + " " + lastItemView); |
|||
|
|||
// final Rect visibleItemRect = new Rect(); |
|||
|
|||
// int firstVisibleItemHeight = 0, lastVisibleItemHeight = 0; |
|||
|
|||
// final boolean isFirstItemVideoHolder = firstItemView != null && firstItemView.getId() == R.id.videoHolder; |
|||
// if (isFirstItemVideoHolder) { |
|||
// firstItemView.getGlobalVisibleRect(visibleItemRect); |
|||
// firstVisibleItemHeight = visibleItemRect.height(); |
|||
// } |
|||
// final boolean isLastItemVideoHolder = lastItemView != null && lastItemView.getId() == R.id.videoHolder; |
|||
// if (isLastItemVideoHolder) { |
|||
// lastItemView.getGlobalVisibleRect(visibleItemRect); |
|||
// lastVisibleItemHeight = visibleItemRect.height(); |
|||
// } |
|||
// |
|||
// if (processFirstItem && firstVisibleItemHeight > lastVisibleItemHeight) |
|||
// videoPosShown = firstVisibleItemPos; |
|||
// else if (processLastItem && lastVisibleItemHeight != 0) videoPosShown = lastVisibleItemPos; |
|||
// |
|||
// if (firstItemView != lastItemView) { |
|||
// final int mox = lastVisibleItemHeight - firstVisibleItemHeight; |
|||
// if (processLastItem && lastVisibleItemHeight > firstVisibleItemHeight) |
|||
// videoPosShown = lastVisibleItemPos; |
|||
// if ((processFirstItem || processLastItem) && mox >= 0) |
|||
// videoPosShown = lastVisibleItemPos; |
|||
// } |
|||
// |
|||
// if (lastChangedVideoPos != -1 && lastVideoPos != -1) { |
|||
// currView = layoutManager.findViewByPosition(lastChangedVideoPos); |
|||
// if (currView != null && currView.getId() == R.id.videoHolder && |
|||
// lastStoppedVideoPos != lastChangedVideoPos && lastPlayedVideoPos != lastChangedVideoPos) { |
|||
// lastStoppedVideoPos = lastChangedVideoPos; |
|||
// stopVideo(lastChangedVideoPos, recyclerView, currView); |
|||
// } |
|||
// |
|||
// currView = layoutManager.findViewByPosition(lastVideoPos); |
|||
// if (currView != null && currView.getId() == R.id.videoHolder) { |
|||
// final Rect rect = new Rect(); |
|||
// currView.getGlobalVisibleRect(rect); |
|||
// |
|||
// final int holderTop = currView.getTop(); |
|||
// final int holderHeight = currView.getBottom() - holderTop; |
|||
// final int halfHeight = holderHeight / 2; |
|||
// //halfHeight -= halfHeight / 5; |
|||
// |
|||
// if (rect.height() < halfHeight) { |
|||
// if (lastStoppedVideoPos != lastVideoPos) { |
|||
// lastStoppedVideoPos = lastVideoPos; |
|||
// stopVideo(lastVideoPos, recyclerView, currView); |
|||
// } |
|||
// } else if (lastPlayedVideoPos != lastVideoPos) { |
|||
// lastPlayedVideoPos = lastVideoPos; |
|||
// playVideo(lastVideoPos, recyclerView, currView); |
|||
// } |
|||
// } |
|||
// |
|||
// if (lastChangedVideoPos != lastVideoPos) lastChangedVideoPos = lastVideoPos; |
|||
// } |
|||
// |
|||
// if (lastVideoPos != -1 && lastVideoPos != videoPosShown) { |
|||
// if (videoAttached) { |
|||
// //if ((currView = layoutManager.findViewByPosition(lastVideoPos)) != null && currView.getId() == R.id.videoHolder) |
|||
// releaseVideo(lastVideoPos, recyclerView, null); |
|||
// videoAttached = false; |
|||
// } |
|||
// } |
|||
// if (videoPosShown != -1) { |
|||
// lastVideoPos = videoPosShown; |
|||
// if (!videoAttached) { |
|||
// if ((currView = layoutManager.findViewByPosition(videoPosShown)) != null && currView.getId() == R.id.videoHolder) |
|||
// attachVideo(videoPosShown, recyclerView, currView); |
|||
// videoAttached = true; |
|||
// } |
|||
// } |
|||
} |
|||
|
|||
private synchronized void attachVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) { |
|||
synchronized (LOCK) { |
|||
if (recyclerView != null) { |
|||
final RecyclerView.Adapter<?> adapter = recyclerView.getAdapter(); |
|||
if (adapter instanceof FeedAdapter) { |
|||
final SimpleExoPlayer pagerPlayer = ((FeedAdapter) adapter).pagerPlayer; |
|||
if (pagerPlayer != null) pagerPlayer.setPlayWhenReady(false); |
|||
} |
|||
} |
|||
|
|||
if (player != null) { |
|||
player.stop(true); |
|||
player.release(); |
|||
player = null; |
|||
} |
|||
|
|||
final boolean shouldAutoplay = settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS); |
|||
player = new SimpleExoPlayer.Builder(context) |
|||
.setUseLazyPreparation(!shouldAutoplay) |
|||
.build(); |
|||
player.setPlayWhenReady(shouldAutoplay); |
|||
|
|||
if (itemView != null) { |
|||
final Object tag = itemView.getTag(); |
|||
|
|||
final View btnComments = itemView.findViewById(R.id.btnComments); |
|||
if (btnComments != null && tag instanceof FeedModel) { |
|||
final FeedModel feedModel = (FeedModel) tag; |
|||
|
|||
if (feedModel.getCommentsCount() <= 0) btnComments.setEnabled(false); |
|||
else { |
|||
btnComments.setTag(feedModel); |
|||
btnComments.setEnabled(true); |
|||
btnComments.setOnClickListener(commentClickListener); |
|||
} |
|||
private FeedVideoViewHolder getFirstVideoHolder(final RecyclerView recyclerView, final int firstVisibleItemPos, final int lastVisibleItemPos) { |
|||
final Rect visibleItemRect = new Rect(); |
|||
final Point offset = new Point(); |
|||
for (int pos = firstVisibleItemPos; pos <= lastVisibleItemPos; pos++) { |
|||
final View view = layoutManager.findViewByPosition(pos); |
|||
if (view != null && view.getId() == R.id.videoHolder) { |
|||
final View viewSwitcher = view.findViewById(R.id.view_switcher); |
|||
if (viewSwitcher == null) { |
|||
continue; |
|||
} |
|||
|
|||
final PlayerView playerView = itemView.findViewById(R.id.playerView); |
|||
if (playerView == null) return; |
|||
playerView.setPlayer(player); |
|||
|
|||
btnMute = itemView.findViewById(R.id.btnMute); |
|||
|
|||
float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f; |
|||
if (vol == 0f && Utils.sessionVolumeFull) vol = 1f; |
|||
player.setVolume(vol); |
|||
|
|||
if (btnMute != null) { |
|||
btnMute.setVisibility(View.VISIBLE); |
|||
btnMute.setImageResource(vol == 0f ? R.drawable.vol : R.drawable.mute); |
|||
btnMute.setOnClickListener(muteClickListener); |
|||
final boolean result = viewSwitcher.getGlobalVisibleRect(visibleItemRect, offset); |
|||
if (!result) continue; |
|||
final FeedVideoViewHolder viewHolder = (FeedVideoViewHolder) recyclerView.getChildViewHolder(view); |
|||
final FeedModel currentFeedModel = viewHolder.getCurrentFeedModel(); |
|||
visibleItemRect.offset(-offset.x, -offset.y); |
|||
final int visibleHeight = visibleItemRect.height(); |
|||
if (visibleHeight < currentFeedModel.getImageHeight()) { |
|||
continue; |
|||
} |
|||
final DataSource.Factory factory = cacheDataSourceFactory != null ? cacheDataSourceFactory : dataSourceFactory; |
|||
final ProgressiveMediaSource.Factory sourceFactory = new ProgressiveMediaSource.Factory(factory); |
|||
final ProgressiveMediaSource mediaSource = sourceFactory.createMediaSource(Uri.parse(feedModels.get(itemPos).getDisplayUrl())); |
|||
|
|||
player.setRepeatMode(Player.REPEAT_MODE_ALL); |
|||
player.prepare(mediaSource); |
|||
player.setVolume(vol); |
|||
|
|||
playerView.setOnClickListener(v -> player.setPlayWhenReady(!player.getPlayWhenReady())); |
|||
// Log.d(TAG, "post:" + currentFeedModel.getPostId() + ", visibleHeight: " + visibleHeight + ", post height: " + currentFeedModel.getImageHeight()); |
|||
return viewHolder; |
|||
} |
|||
|
|||
if (videoChangeCallback != null) videoChangeCallback.playerChanged(itemPos, player); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
private void releaseVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) { |
|||
// Log.d("AWAISKING_APP", "release: " + itemPos); |
|||
// private synchronized void attachVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) { |
|||
// synchronized (LOCK) { |
|||
// if (recyclerView != null) { |
|||
// final RecyclerView.Adapter<?> adapter = recyclerView.getAdapter(); |
|||
// if (adapter instanceof FeedAdapter) { |
|||
// final SimpleExoPlayer pagerPlayer = ((FeedAdapter) adapter).pagerPlayer; |
|||
// if (pagerPlayer != null) pagerPlayer.setPlayWhenReady(false); |
|||
// } |
|||
// } |
|||
// if (itemView == null) { |
|||
// return; |
|||
// } |
|||
// final boolean shouldAutoplay = settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS); |
|||
// final FeedModel feedModel = feedModels.get(itemPos); |
|||
// // loadVideo(itemPos, itemView, shouldAutoplay, feedModel); |
|||
// } |
|||
// } |
|||
// |
|||
// private void loadVideo(final int itemPos, final View itemView, final boolean shouldAutoplay, final FeedModel feedModel) { |
|||
// final PlayerView playerView = itemView.findViewById(R.id.playerView); |
|||
// if (playerView == null) { |
|||
// return; |
|||
// } |
|||
// if (player != null) { |
|||
// player.stop(true); |
|||
// player.release(); |
|||
// player = null; |
|||
// } |
|||
// player = null; |
|||
} |
|||
|
|||
private void playVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) { |
|||
// if (player != null) { |
|||
// final int playbackState = player.getPlaybackState(); |
|||
// if (!player.isPlaying() |
|||
// || playbackState == Player.STATE_READY || playbackState == Player.STATE_ENDED |
|||
// ) { |
|||
// player.setPlayWhenReady(true); |
|||
// } |
|||
// } |
|||
// if (player != null) { |
|||
// player.setPlayWhenReady(true); |
|||
// player.getPlaybackState(); |
|||
// } |
|||
} |
|||
|
|||
private void stopVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) { |
|||
if (player != null) { |
|||
player.setPlayWhenReady(false); |
|||
player.getPlaybackState(); |
|||
} |
|||
} |
|||
// |
|||
// player = new SimpleExoPlayer.Builder(context) |
|||
// .setUseLazyPreparation(!shouldAutoplay) |
|||
// .build(); |
|||
// player.setPlayWhenReady(shouldAutoplay); |
|||
// |
|||
// final View btnComments = itemView.findViewById(R.id.btnComments); |
|||
// if (btnComments != null) { |
|||
// if (feedModel.getCommentsCount() <= 0) btnComments.setEnabled(false); |
|||
// else { |
|||
// btnComments.setTag(feedModel); |
|||
// btnComments.setEnabled(true); |
|||
// btnComments.setOnClickListener(commentClickListener); |
|||
// } |
|||
// } |
|||
// playerView.setPlayer(player); |
|||
// btnMute = itemView.findViewById(R.id.btnMute); |
|||
// float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f; |
|||
// if (vol == 0f && Utils.sessionVolumeFull) vol = 1f; |
|||
// player.setVolume(vol); |
|||
// |
|||
// if (btnMute != null) { |
|||
// btnMute.setVisibility(View.VISIBLE); |
|||
// btnMute.setImageResource(vol == 0f ? R.drawable.vol : R.drawable.mute); |
|||
// btnMute.setOnClickListener(muteClickListener); |
|||
// } |
|||
// final DataSource.Factory factory = cacheDataSourceFactory != null ? cacheDataSourceFactory : dataSourceFactory; |
|||
// final ProgressiveMediaSource.Factory sourceFactory = new ProgressiveMediaSource.Factory(factory); |
|||
// final ProgressiveMediaSource mediaSource = sourceFactory.createMediaSource(Uri.parse(feedModel.getDisplayUrl())); |
|||
// |
|||
// player.setRepeatMode(Player.REPEAT_MODE_ALL); |
|||
// player.prepare(mediaSource); |
|||
// player.setVolume(vol); |
|||
// |
|||
// playerView.setOnClickListener(v -> player.setPlayWhenReady(!player.getPlayWhenReady())); |
|||
// |
|||
// if (videoChangeCallback != null) videoChangeCallback.playerChanged(itemPos, player); |
|||
// } |
|||
// |
|||
// private void releaseVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) { |
|||
// // Log.d("AWAISKING_APP", "release: " + itemPos); |
|||
// // if (player != null) { |
|||
// // player.stop(true); |
|||
// // player.release(); |
|||
// // } |
|||
// // player = null; |
|||
// } |
|||
// |
|||
// private void playVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) { |
|||
// // if (player != null) { |
|||
// // final int playbackState = player.getPlaybackState(); |
|||
// // if (!player.isPlaying() |
|||
// // || playbackState == Player.STATE_READY || playbackState == Player.STATE_ENDED |
|||
// // ) { |
|||
// // player.setPlayWhenReady(true); |
|||
// // } |
|||
// // } |
|||
// // if (player != null) { |
|||
// // player.setPlayWhenReady(true); |
|||
// // player.getPlaybackState(); |
|||
// // } |
|||
// } |
|||
// |
|||
// private void stopVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) { |
|||
// if (player != null) { |
|||
// player.setPlayWhenReady(false); |
|||
// player.getPlaybackState(); |
|||
// } |
|||
// } |
|||
|
|||
public interface VideoChangeCallback { |
|||
void playerChanged(final int itemPos, final SimpleExoPlayer player); |
|||
|
@ -1,10 +1,33 @@ |
|||
package awais.instagrabber.models.enums; |
|||
|
|||
import java.io.Serializable; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
public enum MediaItemType implements Serializable { |
|||
MEDIA_TYPE_IMAGE, |
|||
MEDIA_TYPE_VIDEO, |
|||
MEDIA_TYPE_SLIDER, |
|||
MEDIA_TYPE_VOICE, |
|||
MEDIA_TYPE_IMAGE(1), |
|||
MEDIA_TYPE_VIDEO(2), |
|||
MEDIA_TYPE_SLIDER(3), |
|||
MEDIA_TYPE_VOICE(4); |
|||
|
|||
private final int id; |
|||
private static Map<Integer, MediaItemType> map = new HashMap<>(); |
|||
|
|||
static { |
|||
for (MediaItemType type : MediaItemType.values()) { |
|||
map.put(type.id, type); |
|||
} |
|||
} |
|||
|
|||
MediaItemType(final int id) { |
|||
this.id = id; |
|||
} |
|||
|
|||
public int getId() { |
|||
return id; |
|||
} |
|||
|
|||
public static MediaItemType valueOf(final int id) { |
|||
return map.get(id); |
|||
} |
|||
} |
@ -0,0 +1,11 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<shape xmlns:android="http://schemas.android.com/apk/res/android" > |
|||
<solid android:color="@color/semi_transparent_black" /> |
|||
<padding |
|||
android:left="2dp" |
|||
android:right="2dp" |
|||
android:bottom="2dp" |
|||
android:top="2dp" /> |
|||
|
|||
<corners android:radius="5dp" /> |
|||
</shape> |
@ -1,17 +0,0 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|||
xmlns:tools="http://schemas.android.com/tools" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:orientation="vertical" |
|||
tools:viewBindingIgnore="true"> |
|||
|
|||
<include layout="@layout/item_feed_top" /> |
|||
|
|||
<com.github.chrisbanes.photoview.PhotoView |
|||
android:id="@+id/imageViewer" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" /> |
|||
|
|||
<include layout="@layout/item_feed_bottom" /> |
|||
</LinearLayout> |
@ -0,0 +1,25 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:orientation="vertical"> |
|||
|
|||
<include |
|||
android:id="@+id/item_feed_top" |
|||
layout="@layout/item_feed_top" /> |
|||
|
|||
<!--<com.github.chrisbanes.photoview.PhotoView--> |
|||
<!-- android:id="@+id/imageViewer"--> |
|||
<!-- android:layout_width="match_parent"--> |
|||
<!-- android:layout_height="wrap_content" />--> |
|||
|
|||
<awais.instagrabber.customviews.drawee.ZoomableDraweeView |
|||
android:id="@+id/imageViewer" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="200dp" |
|||
android:background="@android:color/transparent" /> |
|||
|
|||
<include |
|||
android:id="@+id/item_feed_bottom" |
|||
layout="@layout/item_feed_bottom" /> |
|||
</LinearLayout> |
@ -1,23 +1,52 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|||
xmlns:app="http://schemas.android.com/apk/res-auto" |
|||
xmlns:tools="http://schemas.android.com/tools" |
|||
android:id="@+id/videoHolder" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:orientation="vertical" |
|||
tools:viewBindingIgnore="true"> |
|||
android:orientation="vertical"> |
|||
|
|||
<include layout="@layout/item_feed_top" /> |
|||
<include |
|||
android:id="@+id/item_feed_top" |
|||
layout="@layout/item_feed_top" /> |
|||
|
|||
<com.google.android.exoplayer2.ui.PlayerView |
|||
android:id="@+id/playerView" |
|||
<ViewSwitcher |
|||
android:id="@+id/view_switcher" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="240dp" |
|||
app:auto_show="true" |
|||
app:controller_layout_id="@layout/layout_controls" |
|||
app:repeat_toggle_modes="all" |
|||
app:use_controller="true" /> |
|||
android:layout_height="wrap_content"> |
|||
|
|||
<include layout="@layout/item_feed_bottom" /> |
|||
<!--<androidx.appcompat.widget.AppCompatImageView--> |
|||
<!-- android:id="@+id/thumbnail"--> |
|||
<!-- android:layout_width="match_parent"--> |
|||
<!-- android:layout_height="wrap_content"--> |
|||
<!-- android:adjustViewBounds="true"--> |
|||
<!-- android:scaleType="fitCenter"/>--> |
|||
<FrameLayout |
|||
android:id="@+id/thumbnail_parent" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content"> |
|||
|
|||
<com.facebook.drawee.view.SimpleDraweeView |
|||
android:id="@+id/thumbnail" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="match_parent" |
|||
app:actualImageScaleType="fitCenter" /> |
|||
|
|||
<androidx.appcompat.widget.AppCompatImageView |
|||
android:layout_width="50dp" |
|||
android:layout_height="50dp" |
|||
android:layout_gravity="center" |
|||
app:srcCompat="@drawable/exo_controls_play" /> |
|||
</FrameLayout> |
|||
|
|||
<com.google.android.exoplayer2.ui.PlayerView |
|||
android:id="@+id/playerView" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="match_parent" |
|||
app:show_timeout="1000" /> |
|||
</ViewSwitcher> |
|||
|
|||
<include |
|||
android:id="@+id/item_feed_bottom" |
|||
layout="@layout/item_feed_bottom" /> |
|||
</LinearLayout> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue