@@ -121,7 +125,7 @@ This app's predecessor, InstaGrabber, was originally made by [@AwaisKing](https:
Barinsta
Copyright (C) 2020-2021 Austin Huang
- Ammar Githam
+ Ammar Githam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
diff --git a/app/build.gradle b/app/build.gradle
index bb42f14d..9a4fe62b 100755
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -12,7 +12,7 @@ def getGitHash = { ->
}
android {
- compileSdkVersion 29
+ compileSdkVersion 30
defaultConfig {
applicationId 'me.austinhuang.instagrabber'
@@ -82,6 +82,27 @@ android {
}
}
+ splits {
+ // Configures multiple APKs based on ABI.
+ abi {
+ // Enables building multiple APKs per ABI.
+ enable project.hasProperty("split") && !gradle.startParameter.taskNames.isEmpty() && gradle.startParameter.taskNames.get(0).contains('Release')
+
+ // By default all ABIs are included, so use reset() and include to specify that we only
+ // want APKs for x86 and x86_64.
+
+ // Resets the list of ABIs that Gradle should create APKs for to none.
+ reset()
+
+ // Specifies a list of ABIs that Gradle should create APKs for.
+ include "x86", "x86_64", "arm64-v8a", "armeabi-v7a"
+
+ // Specifies that we want to also generate a universal APK that includes all ABIs.
+ universalApk true
+ }
+ }
+
+
android.applicationVariants.all { variant ->
if (variant.flavorName != "github") return
variant.outputs.all { output ->
@@ -90,15 +111,32 @@ android {
// def versionCode = variant.versionCode
def flavor = variant.flavorName
- def suffix = "${versionName}-${flavor}_${builtType}" // eg. 19.1.0-github_debug or release
+ def flavorBuiltType = "${flavor}_${builtType}"
+ def suffix
+ // For x86 and x86_64, the versionNames are already overridden
+ if (versionName.contains(flavorBuiltType)) {
+ suffix = "${versionName}"
+ } else {
+ suffix = "${versionName}-${flavorBuiltType}" // eg. 19.1.0-github_debug or release
+ }
if (builtType.toString() == 'release' && project.hasProperty("pre")) {
buildConfigField("boolean", "isPre", "true")
- // append latest commit short hash for pre-release
- suffix = "${versionName}.${getGitHash()}-${flavor}" // eg. 19.1.0.b123456-github
+
+ flavorBuiltType = "${getGitHash()}-${flavor}"
+
+ // For x86 and x86_64, the versionNames are already overridden
+ if (versionName.contains(flavorBuiltType)) {
+ suffix = "${versionName}"
+ } else {
+ // append latest commit short hash for pre-release
+ suffix = "${versionName}.${flavorBuiltType}" // eg. 19.1.0.b123456-github
+ }
}
output.versionNameOverride = suffix
- outputFileName = "barinsta_${suffix}.apk"
+ def abi = output.getFilter(com.android.build.OutputFile.ABI)
+ // println(abi + ", " + versionName + ", " + flavor + ", " + builtType + ", " + suffix)
+ outputFileName = abi == null ? "barinsta_${suffix}.apk" : "barinsta_${suffix}_${abi}.apk"
}
}
@@ -118,18 +156,16 @@ dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
def appcompat_version = "1.2.0"
- def nav_version = '2.3.4'
+ def nav_version = '2.3.5'
def exoplayer_version = '2.13.3'
- implementation 'com.google.android.material:material:1.4.0-alpha02'
+ implementation 'com.google.android.material:material:1.4.0-beta01'
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-dash:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
- implementation "androidx.appcompat:appcompat:$appcompat_version"
- implementation "androidx.appcompat:appcompat-resources:$appcompat_version"
- implementation "androidx.recyclerview:recyclerview:1.2.0-rc01"
+ implementation "androidx.recyclerview:recyclerview:1.2.0"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation "androidx.navigation:navigation-fragment:$nav_version"
@@ -142,6 +178,9 @@ dependencies {
implementation 'com.google.guava:guava:27.0.1-android'
+ def core_version = "1.6.0-alpha03"
+ implementation "androidx.core:core:$core_version"
+
// Room
def room_version = "2.2.6"
implementation "androidx.room:room-runtime:$room_version"
@@ -169,6 +208,8 @@ dependencies {
implementation 'org.apache.commons:commons-imaging:1.0-alpha2'
+ implementation 'com.github.skydoves:balloon:1.3.4'
+
implementation 'com.github.ammargitham:AutoLinkTextViewV2:v3.1.0'
implementation 'com.github.ammargitham:uCrop:2.3-native-beta-2'
implementation 'com.github.ammargitham:android-gpuimage:2.1.1-beta4'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ed170657..f90a4aea 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -27,8 +27,7 @@
+ android:taskAffinity=".Main">
diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.java b/app/src/main/java/awais/instagrabber/activities/MainActivity.java
index deeeb232..00bc847d 100644
--- a/app/src/main/java/awais/instagrabber/activities/MainActivity.java
+++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.java
@@ -31,6 +31,9 @@ import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.provider.FontRequest;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowCompat;
+import androidx.core.view.WindowInsetsCompat;
import androidx.emoji.text.EmojiCompat;
import androidx.emoji.text.FontRequestEmojiCompatConfig;
import androidx.fragment.app.FragmentManager;
@@ -61,6 +64,7 @@ import awais.instagrabber.BuildConfig;
import awais.instagrabber.R;
import awais.instagrabber.asyncs.PostFetcher;
import awais.instagrabber.customviews.emoji.EmojiVariantManager;
+import awais.instagrabber.customviews.helpers.RootViewDeferringInsetsCallback;
import awais.instagrabber.customviews.helpers.TextWatcherAdapter;
import awais.instagrabber.databinding.ActivityMainBinding;
import awais.instagrabber.fragments.PostViewV2Fragment;
@@ -137,11 +141,19 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
instance = this;
binding = ActivityMainBinding.inflate(getLayoutInflater());
setupCookie();
- if (settingsHelper.getBoolean(Constants.FLAG_SECURE))
+ if (settingsHelper.getBoolean(Constants.FLAG_SECURE)) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
+ }
setContentView(binding.getRoot());
final Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
+ final RootViewDeferringInsetsCallback deferringInsetsCallback = new RootViewDeferringInsetsCallback(
+ WindowInsetsCompat.Type.systemBars(),
+ WindowInsetsCompat.Type.ime()
+ );
+ ViewCompat.setWindowInsetsAnimationCallback(binding.getRoot(), deferringInsetsCallback);
+ ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), deferringInsetsCallback);
+ WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
createNotificationChannels();
try {
final CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) binding.bottomNavView.getLayoutParams();
@@ -335,6 +347,7 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
} catch (Exception e) {
Log.e(TAG, "onDestroy: ", e);
}
+ instance = null;
}
@Override
@@ -504,11 +517,12 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
@SuppressLint("RestrictedApi") final Deque backStack = navController.getBackStack();
setupMenu(backStack.size(), destinationId);
final boolean contains = showBottomViewDestinations.contains(destinationId);
- binding.bottomNavView.setVisibility(contains ? View.VISIBLE : View.GONE);
- if (contains && behavior != null) {
- behavior.slideUp(binding.bottomNavView);
- }
-
+ binding.getRoot().post(() -> {
+ binding.bottomNavView.setVisibility(contains ? View.VISIBLE : View.GONE);
+ if (contains && behavior != null) {
+ behavior.slideUp(binding.bottomNavView);
+ }
+ });
// explicitly hide keyboard when we navigate
final View view = getCurrentFocus();
Utils.hideKeyboard(view);
@@ -651,7 +665,11 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
if (navController == null) return;
final Bundle bundle = new Bundle();
bundle.putString("username", "@" + username);
- navController.navigate(R.id.action_global_profileFragment, bundle);
+ try {
+ navController.navigate(R.id.action_global_profileFragment, bundle);
+ } catch (Exception e) {
+ Log.e(TAG, "showProfileView: ", e);
+ }
}
private void showPostView(@NonNull final IntentModel intentModel) {
@@ -664,11 +682,16 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
alertDialog.show();
new PostFetcher(shortCode, feedModel -> {
if (feedModel != null) {
- final PostViewV2Fragment fragment = PostViewV2Fragment
- .builder(feedModel)
- .build();
- fragment.setOnShowListener(dialog -> alertDialog.dismiss());
- fragment.show(getSupportFragmentManager(), "post_view");
+ if (currentNavControllerLiveData == null) return;
+ final NavController navController = currentNavControllerLiveData.getValue();
+ if (navController == null) return;
+ final Bundle bundle = new Bundle();
+ bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, feedModel);
+ try {
+ navController.navigate(R.id.action_global_post_view, bundle);
+ } catch (Exception e) {
+ Log.e(TAG, "showPostView: ", e);
+ }
return;
}
Toast.makeText(getApplicationContext(), R.string.post_not_found, Toast.LENGTH_SHORT).show();
@@ -724,11 +747,19 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
}
public void setCollapsingView(@NonNull final View view) {
- binding.collapsingToolbarLayout.addView(view, 0);
+ try {
+ binding.collapsingToolbarLayout.addView(view, 0);
+ } catch (Exception e) {
+ Log.e(TAG, "setCollapsingView: ", e);
+ }
}
public void removeCollapsingView(@NonNull final View view) {
- binding.collapsingToolbarLayout.removeView(view);
+ try {
+ binding.collapsingToolbarLayout.removeView(view);
+ } catch (Exception e) {
+ Log.e(TAG, "removeCollapsingView: ", e);
+ }
}
public void setToolbar(final Toolbar toolbar) {
diff --git a/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java
index 2b1f410e..2673d787 100644
--- a/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java
+++ b/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java
@@ -406,6 +406,8 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter callback);
+
+ void onAddReactionListener(DirectItem item);
}
public interface DirectItemInternalLongClickListener {
diff --git a/app/src/main/java/awais/instagrabber/adapters/FeedStoriesListAdapter.java b/app/src/main/java/awais/instagrabber/adapters/FeedStoriesListAdapter.java
index 9d7401e0..146904d0 100755
--- a/app/src/main/java/awais/instagrabber/adapters/FeedStoriesListAdapter.java
+++ b/app/src/main/java/awais/instagrabber/adapters/FeedStoriesListAdapter.java
@@ -23,23 +23,29 @@ public final class FeedStoriesListAdapter extends ListAdapter list;
private final Filter filter = new Filter() {
- @Nullable
+ @NonNull
@Override
protected FilterResults performFiltering(final CharSequence filter) {
- final boolean isFilterEmpty = TextUtils.isEmpty(filter);
- final String query = isFilterEmpty ? null : filter.toString().toLowerCase();
-
- for (FeedStoryModel item : list) {
- if (isFilterEmpty) item.setShown(true);
- else item.setShown(item.getProfileModel().getUsername().toLowerCase().contains(query));
+ final String query = TextUtils.isEmpty(filter) ? null : filter.toString().toLowerCase();
+ List filteredList = list;
+ if (list != null && query != null) {
+ filteredList = list.stream()
+ .filter(feedStoryModel -> feedStoryModel.getProfileModel()
+ .getUsername()
+ .toLowerCase()
+ .contains(query))
+ .collect(Collectors.toList());
}
- return null;
+ final FilterResults filterResults = new FilterResults();
+ filterResults.count = filteredList != null ? filteredList.size() : 0;
+ filterResults.values = filteredList;
+ return filterResults;
}
@Override
protected void publishResults(final CharSequence constraint, final FilterResults results) {
- submitList(list);
- notifyDataSetChanged();
+ //noinspection unchecked
+ submitList((List) results.values, true);
}
};
@@ -65,10 +71,16 @@ public final class FeedStoriesListAdapter extends ListAdapter list, final boolean isFiltered) {
+ if (!isFiltered) {
+ this.list = list;
+ }
+ super.submitList(list);
+ }
+
@Override
public void submitList(final List list) {
- super.submitList(list.stream().filter(i -> i.isShown()).collect(Collectors.toList()));
- this.list = list;
+ submitList(list, false);
}
@NonNull
@@ -82,11 +94,11 @@ public final class FeedStoriesListAdapter extends ListAdapter {
- private final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener;
private final boolean loadVideoOnItemClick;
private final SliderCallback sliderCallback;
- // private final LayoutExoCustomControlsBinding controlsBinding;
private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
@@ -35,15 +35,11 @@ public final class SliderItemsAdapter extends ListAdapter {
- // if (sliderCallback != null) {
- // sliderCallback.onItemClicked(position);
- // }
- // });
- binding.getRoot().setTapListener(new GestureDetector.SimpleOnGestureListener() {
+ final DoubleTapGestureListener tapListener = new DoubleTapGestureListener(binding.getRoot()) {
@Override
- public boolean onSingleTapUp(final MotionEvent e) {
+ public boolean onSingleTapConfirmed(final MotionEvent e) {
if (sliderCallback != null) {
- sliderCallback.onItemClicked(position);
- return true;
+ sliderCallback.onItemClicked(position, model, binding.getRoot());
}
- return false;
+ return super.onSingleTapConfirmed(e);
}
- });
+ };
+ binding.getRoot().setTapListener(tapListener);
final AnimatedZoomableController zoomableController = AnimatedZoomableController.newInstance();
zoomableController.setMaxScaleFactor(3f);
binding.getRoot().setZoomableController(zoomableController);
- if (onVerticalDragListener != null) {
- binding.getRoot().setOnVerticalDragListener(onVerticalDragListener);
- }
+ binding.getRoot().setZoomingEnabled(true);
}
-
- // private void setDimensions(final FeedModel feedModel, final int spanCount, final boolean animate) {
- // final ViewGroup.LayoutParams layoutParams = binding.imageViewer.getLayoutParams();
- // final int deviceWidth = Utils.displayMetrics.widthPixels;
- // final int spanWidth = deviceWidth / spanCount;
- // final int spanHeight = NumberUtils.getResultingHeight(spanWidth, feedModel.getImageHeight(), feedModel.getImageWidth());
- // final int width = spanWidth == 0 ? deviceWidth : spanWidth;
- // final int height = spanHeight == 0 ? deviceWidth + 1 : spanHeight;
- // if (animate) {
- // Animation animation = AnimationUtils.expand(
- // binding.imageViewer,
- // layoutParams.width,
- // layoutParams.height,
- // width,
- // height,
- // new Animation.AnimationListener() {
- // @Override
- // public void onAnimationStart(final Animation animation) {
- // showOrHideDetails(spanCount);
- // }
- //
- // @Override
- // public void onAnimationEnd(final Animation animation) {
- // // showOrHideDetails(spanCount);
- // }
- //
- // @Override
- // public void onAnimationRepeat(final Animation animation) {
- //
- // }
- // });
- // binding.imageViewer.startAnimation(animation);
- // } else {
- // layoutParams.width = width;
- // layoutParams.height = height;
- // binding.imageViewer.requestLayout();
- // }
- // }
- //
- // private void showOrHideDetails(final int spanCount) {
- // if (spanCount == 1) {
- // binding.itemFeedTop.getRoot().setVisibility(View.VISIBLE);
- // binding.itemFeedBottom.getRoot().setVisibility(View.VISIBLE);
- // } else {
- // binding.itemFeedTop.getRoot().setVisibility(View.GONE);
- // binding.itemFeedBottom.getRoot().setVisibility(View.GONE);
- // }
- // }
}
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java
index d22afacd..3624d7ef 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java
@@ -7,10 +7,11 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
+import com.google.android.exoplayer2.ui.StyledPlayerView;
+
import java.util.List;
import awais.instagrabber.adapters.SliderItemsAdapter;
-import awais.instagrabber.customviews.VerticalDragHelper;
import awais.instagrabber.customviews.VideoPlayerCallbackAdapter;
import awais.instagrabber.customviews.VideoPlayerViewHelper;
import awais.instagrabber.databinding.LayoutVideoPlayerWithThumbnailBinding;
@@ -27,40 +28,23 @@ public class SliderVideoViewHolder extends SliderItemViewHolder {
private static final String TAG = "SliderVideoViewHolder";
private final LayoutVideoPlayerWithThumbnailBinding binding;
- // private final LayoutExoCustomControlsBinding controlsBinding;
private final boolean loadVideoOnItemClick;
- private final GestureDetector.OnGestureListener videoPlayerViewGestureListener = new GestureDetector.SimpleOnGestureListener() {
- @Override
- public boolean onSingleTapConfirmed(final MotionEvent e) {
- binding.playerView.performClick();
- return true;
- }
- };
private VideoPlayerViewHelper videoPlayerViewHelper;
@SuppressLint("ClickableViewAccessibility")
public SliderVideoViewHolder(@NonNull final LayoutVideoPlayerWithThumbnailBinding binding,
- final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener,
- // final LayoutExoCustomControlsBinding controlsBinding,
final boolean loadVideoOnItemClick) {
super(binding.getRoot());
this.binding = binding;
- // this.controlsBinding = controlsBinding;
this.loadVideoOnItemClick = loadVideoOnItemClick;
- // if (onVerticalDragListener != null) {
- // final VerticalDragHelper thumbnailVerticalDragHelper = new VerticalDragHelper(binding.thumbnailParent);
- // final VerticalDragHelper playerVerticalDragHelper = new VerticalDragHelper(binding.playerView);
- // thumbnailVerticalDragHelper.setOnVerticalDragListener(onVerticalDragListener);
- // playerVerticalDragHelper.setOnVerticalDragListener(onVerticalDragListener);
- // binding.thumbnailParent.setOnTouchListener((v, event) -> {
- // final boolean onDragTouch = thumbnailVerticalDragHelper.onDragTouch(event);
- // if (onDragTouch) {
- // return true;
- // }
- // return thumbnailVerticalDragHelper.onGestureTouchEvent(event);
- // });
- // }
+ final GestureDetector.OnGestureListener videoPlayerViewGestureListener = new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onSingleTapConfirmed(final MotionEvent e) {
+ binding.playerView.performClick();
+ return true;
+ }
+ };
final GestureDetector gestureDetector = new GestureDetector(itemView.getContext(), videoPlayerViewGestureListener);
binding.playerView.setOnTouchListener((v, event) -> {
gestureDetector.onTouchEvent(event);
@@ -77,7 +61,7 @@ public class SliderVideoViewHolder extends SliderItemViewHolder {
@Override
public void onThumbnailClick() {
if (sliderCallback != null) {
- sliderCallback.onItemClicked(position);
+ sliderCallback.onItemClicked(position, media, binding.getRoot());
}
}
@@ -120,6 +104,21 @@ public class SliderVideoViewHolder extends SliderItemViewHolder {
sliderCallback.onPlayerRelease(position);
}
}
+
+ @Override
+ public void onFullScreenModeChanged(final boolean isFullScreen, final StyledPlayerView playerView) {
+ if (sliderCallback != null) {
+ sliderCallback.onFullScreenModeChanged(isFullScreen, playerView);
+ }
+ }
+
+ @Override
+ public boolean isInFullScreen() {
+ if (sliderCallback != null) {
+ return sliderCallback.isInFullScreen();
+ }
+ return false;
+ }
};
final float aspectRatio = (float) media.getOriginalWidth() / media.getOriginalHeight();
String videoUrl = null;
@@ -138,16 +137,10 @@ public class SliderVideoViewHolder extends SliderItemViewHolder {
aspectRatio,
ResponseBodyUtils.getThumbUrl(media),
loadVideoOnItemClick,
- // controlsBinding,
videoPlayerCallback);
- // binding.itemFeedBottom.btnMute.setOnClickListener(v -> {
- // final float newVol = videoPlayerViewHelper.toggleMute();
- // setMuteIcon(newVol);
- // Utils.sessionVolumeFull = newVol == 1f;
- // });
binding.playerView.setOnClickListener(v -> {
if (sliderCallback != null) {
- sliderCallback.onItemClicked(position);
+ sliderCallback.onItemClicked(position, media, binding.getRoot());
}
});
}
@@ -161,62 +154,4 @@ public class SliderVideoViewHolder extends SliderItemViewHolder {
if (videoPlayerViewHelper == null) return;
videoPlayerViewHelper.releasePlayer();
}
-
- // public void resetPlayerTimeline() {
- // if (videoPlayerViewHelper == null) return;
- // videoPlayerViewHelper.resetTimeline();
- // }
- //
- // public void removeCallbacks() {
- // if (videoPlayerViewHelper == null) return;
- // videoPlayerViewHelper.removeCallbacks();
- // }
-
- // private void setDimensions(final FeedModel feedModel, final int spanCount, final boolean animate) {
- // final ViewGroup.LayoutParams layoutParams = binding.imageViewer.getLayoutParams();
- // final int deviceWidth = Utils.displayMetrics.widthPixels;
- // final int spanWidth = deviceWidth / spanCount;
- // final int spanHeight = NumberUtils.getResultingHeight(spanWidth, feedModel.getImageHeight(), feedModel.getImageWidth());
- // final int width = spanWidth == 0 ? deviceWidth : spanWidth;
- // final int height = spanHeight == 0 ? deviceWidth + 1 : spanHeight;
- // if (animate) {
- // Animation animation = AnimationUtils.expand(
- // binding.imageViewer,
- // layoutParams.width,
- // layoutParams.height,
- // width,
- // height,
- // new Animation.AnimationListener() {
- // @Override
- // public void onAnimationStart(final Animation animation) {
- // showOrHideDetails(spanCount);
- // }
- //
- // @Override
- // public void onAnimationEnd(final Animation animation) {
- // // showOrHideDetails(spanCount);
- // }
- //
- // @Override
- // public void onAnimationRepeat(final Animation animation) {
- //
- // }
- // });
- // binding.imageViewer.startAnimation(animation);
- // } else {
- // layoutParams.width = width;
- // layoutParams.height = height;
- // binding.imageViewer.requestLayout();
- // }
- // }
- //
- // private void showOrHideDetails(final int spanCount) {
- // if (spanCount == 1) {
- // binding.itemFeedTop.getRoot().setVisibility(View.VISIBLE);
- // binding.itemFeedBottom.getRoot().setVisibility(View.VISIBLE);
- // } else {
- // binding.itemFeedTop.getRoot().setVisibility(View.GONE);
- // binding.itemFeedBottom.getRoot().setVisibility(View.GONE);
- // }
- // }
}
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/StoryListViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/StoryListViewHolder.java
index 9633e549..a421e98c 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/StoryListViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/StoryListViewHolder.java
@@ -20,7 +20,6 @@ public final class StoryListViewHolder extends RecyclerView.ViewHolder {
}
public void bind(final FeedStoryModel model,
- final int position,
final OnFeedStoryClickListener notificationClickListener) {
if (model == null) return;
@@ -53,7 +52,7 @@ public final class StoryListViewHolder extends RecyclerView.ViewHolder {
itemView.setOnClickListener(v -> {
if (notificationClickListener == null) return;
- notificationClickListener.onFeedStoryClick(model, position);
+ notificationClickListener.onFeedStoryClick(model);
});
}
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemAnimatedMediaViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemAnimatedMediaViewHolder.java
index 7bc5d173..5b3b1e33 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemAnimatedMediaViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemAnimatedMediaViewHolder.java
@@ -4,7 +4,6 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import androidx.core.util.Pair;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.backends.pipeline.Fresco;
@@ -23,6 +22,7 @@ import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemAnimatedMedia;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
+import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.Utils;
@@ -48,7 +48,7 @@ public class DirectItemAnimatedMediaViewHolder extends DirectItemViewHolder {
final AnimatedMediaFixedHeight fixedHeight = images.getFixedHeight();
if (fixedHeight == null) return;
final String url = fixedHeight.getWebp();
- final Pair widthHeight = NumberUtils.calculateWidthHeight(
+ final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(
fixedHeight.getHeight(),
fixedHeight.getWidth(),
mediaImageMaxHeight,
@@ -56,8 +56,8 @@ public class DirectItemAnimatedMediaViewHolder extends DirectItemViewHolder {
);
binding.ivAnimatedMessage.setVisibility(View.VISIBLE);
final ViewGroup.LayoutParams layoutParams = binding.ivAnimatedMessage.getLayoutParams();
- final int width = widthHeight.first != null ? widthHeight.first : 0;
- final int height = widthHeight.second != null ? widthHeight.second : 0;
+ final int width = widthHeight.first;
+ final int height = widthHeight.second;
layoutParams.width = width;
layoutParams.height = height;
binding.ivAnimatedMessage.requestLayout();
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaShareViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaShareViewHolder.java
index dddd9315..82c07ab7 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaShareViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaShareViewHolder.java
@@ -6,7 +6,6 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.core.util.Pair;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.drawable.ScalingUtils;
@@ -31,6 +30,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemClip;
import awais.instagrabber.repositories.responses.directmessages.DirectItemFelixShare;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
+import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.Utils;
@@ -103,15 +103,15 @@ public class DirectItemMediaShareViewHolder extends DirectItemViewHolder {
.setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP)
.setRoundingParams(roundingParams)
.build());
- final Pair widthHeight = NumberUtils.calculateWidthHeight(
+ final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(
media.getOriginalHeight(),
media.getOriginalWidth(),
mediaImageMaxHeight,
mediaImageMaxWidth
);
final ViewGroup.LayoutParams layoutParams = binding.mediaPreview.getLayoutParams();
- layoutParams.width = widthHeight.first != null ? widthHeight.first : 0;
- layoutParams.height = widthHeight.second != null ? widthHeight.second : 0;
+ layoutParams.width = widthHeight.first;
+ layoutParams.height = widthHeight.second;
binding.mediaPreview.requestLayout();
binding.mediaPreview.setTag(url);
binding.mediaPreview.setImageURI(url);
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaViewHolder.java
index 24a9e62e..769548a8 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaViewHolder.java
@@ -4,7 +4,6 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import androidx.core.util.Pair;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
@@ -14,11 +13,11 @@ import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmMediaBinding;
import awais.instagrabber.models.enums.MediaItemType;
-import awais.instagrabber.repositories.responses.ImageVersions2;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
+import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
@@ -53,16 +52,16 @@ public class DirectItemMediaViewHolder extends DirectItemViewHolder {
binding.typeIcon.setVisibility(modelMediaType == MediaItemType.MEDIA_TYPE_VIDEO || modelMediaType == MediaItemType.MEDIA_TYPE_SLIDER
? View.VISIBLE
: View.GONE);
- final Pair widthHeight = NumberUtils.calculateWidthHeight(
+ final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(
media.getOriginalHeight(),
media.getOriginalWidth(),
mediaImageMaxHeight,
mediaImageMaxWidth
);
final ViewGroup.LayoutParams layoutParams = binding.mediaPreview.getLayoutParams();
- final int width = widthHeight.first != null ? widthHeight.first : 0;
+ final int width = widthHeight.first;
layoutParams.width = width;
- layoutParams.height = widthHeight.second != null ? widthHeight.second : 0;
+ layoutParams.height = widthHeight.second;
binding.mediaPreview.requestLayout();
binding.bgTime.getLayoutParams().width = width;
binding.bgTime.requestLayout();
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemRavenMediaViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemRavenMediaViewHolder.java
index 6198faf1..9778d046 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemRavenMediaViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemRavenMediaViewHolder.java
@@ -4,7 +4,6 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import androidx.core.util.Pair;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
@@ -21,6 +20,7 @@ import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemVisualMedia;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
+import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
@@ -170,15 +170,15 @@ public class DirectItemRavenMediaViewHolder extends DirectItemViewHolder {
binding.typeIcon.setVisibility(modelMediaType == MediaItemType.MEDIA_TYPE_VIDEO || modelMediaType == MediaItemType.MEDIA_TYPE_SLIDER
? View.VISIBLE
: View.GONE);
- final Pair widthHeight = NumberUtils.calculateWidthHeight(
+ final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(
media.getOriginalHeight(),
media.getOriginalWidth(),
mediaImageMaxHeight,
maxWidth
);
final ViewGroup.LayoutParams layoutParams = binding.preview.getLayoutParams();
- layoutParams.width = widthHeight.first != null ? widthHeight.first : 0;
- layoutParams.height = widthHeight.second != null ? widthHeight.second : 0;
+ layoutParams.width = widthHeight.first;
+ layoutParams.height = widthHeight.second;
binding.preview.requestLayout();
final String thumbUrl = ResponseBodyUtils.getThumbUrl(media);
binding.preview.setImageURI(thumbUrl);
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemStoryShareViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemStoryShareViewHolder.java
index b6b7bf68..45a6a8d4 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemStoryShareViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemStoryShareViewHolder.java
@@ -5,7 +5,6 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import androidx.core.util.Pair;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.drawable.ScalingUtils;
@@ -17,12 +16,12 @@ import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmStoryShareBinding;
import awais.instagrabber.models.enums.MediaItemType;
-import awais.instagrabber.repositories.responses.ImageVersions2;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemStoryShare;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
+import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
@@ -76,15 +75,15 @@ public class DirectItemStoryShareViewHolder extends DirectItemViewHolder {
.setRoundingParams(roundingParams)
.setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP)
.build());
- final Pair widthHeight = NumberUtils.calculateWidthHeight(
+ final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(
storyShareMedia.getOriginalHeight(),
storyShareMedia.getOriginalWidth(),
mediaImageMaxHeight,
mediaImageMaxWidth
);
final ViewGroup.LayoutParams layoutParams = binding.ivMediaPreview.getLayoutParams();
- layoutParams.width = widthHeight.first != null ? widthHeight.first : 0;
- layoutParams.height = widthHeight.second != null ? widthHeight.second : 0;
+ layoutParams.width = widthHeight.first;
+ layoutParams.height = widthHeight.second;
binding.ivMediaPreview.requestLayout();
final String thumbUrl = ResponseBodyUtils.getThumbUrl(storyShareMedia);
binding.ivMediaPreview.setImageURI(thumbUrl);
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java
index 17a0956c..61e6c5cc 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java
@@ -551,6 +551,10 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple
menu.setOnDismissListener(() -> setSelected(false));
menu.setOnReactionClickListener(emoji -> callback.onReaction(item, emoji));
menu.setOnOptionSelectListener((itemId, cb) -> callback.onOptionSelect(item, itemId, cb));
+ menu.setOnAddReactionListener(() -> {
+ menu.dismiss();
+ itemView.postDelayed(() -> callback.onAddReactionListener(item), 300);
+ });
menu.show(itemView, location);
}
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemXmaViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemXmaViewHolder.java
index 1b1a9fda..adee0226 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemXmaViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemXmaViewHolder.java
@@ -4,7 +4,6 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import androidx.core.util.Pair;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.backends.pipeline.Fresco;
@@ -16,6 +15,7 @@ import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemXma;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
+import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
public class DirectItemXmaViewHolder extends DirectItemViewHolder {
@@ -43,7 +43,7 @@ public class DirectItemXmaViewHolder extends DirectItemViewHolder {
}
final DirectItemXma.XmaUrlInfo urlInfo = playableUrlInfo != null ? playableUrlInfo : previewUrlInfo;
final String url = urlInfo.getUrl();
- final Pair widthHeight = NumberUtils.calculateWidthHeight(
+ final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(
urlInfo.getHeight(),
urlInfo.getWidth(),
mediaImageMaxHeight,
@@ -51,8 +51,8 @@ public class DirectItemXmaViewHolder extends DirectItemViewHolder {
);
binding.ivAnimatedMessage.setVisibility(View.VISIBLE);
final ViewGroup.LayoutParams layoutParams = binding.ivAnimatedMessage.getLayoutParams();
- final int width = widthHeight.first != null ? widthHeight.first : 0;
- final int height = widthHeight.second != null ? widthHeight.second : 0;
+ final int width = widthHeight.first;
+ final int height = widthHeight.second;
layoutParams.width = width;
layoutParams.height = height;
binding.ivAnimatedMessage.requestLayout();
diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java
index 3d5b5587..6931c851 100644
--- a/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java
+++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java
@@ -45,9 +45,9 @@ public class FeedSliderViewHolder extends FeedItemViewHolder {
final String text = "1/" + sliderItemLen;
binding.mediaCounter.setText(text);
binding.mediaList.setOffscreenPageLimit(1);
- final SliderItemsAdapter adapter = new SliderItemsAdapter(null, false, new SliderCallbackAdapter() {
+ final SliderItemsAdapter adapter = new SliderItemsAdapter(false, new SliderCallbackAdapter() {
@Override
- public void onItemClicked(final int position) {
+ public void onItemClicked(final int position, final Media media, final View view) {
feedItemCallback.onSliderClick(feedModel, position);
}
});
diff --git a/app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java b/app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java
index 1731dbf3..c0568a70 100644
--- a/app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java
+++ b/app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java
@@ -105,12 +105,15 @@ public class ChatMessageLayout extends FrameLayout {
viewPartMainLastLineWidth = viewPartMainLineCount > 0
? ((TextView) firstChild).getLayout().getLineWidth(viewPartMainLineCount - 1)
: 0;
+ // also include start left padding
+ viewPartMainLastLineWidth += firstChild.getPaddingLeft();
}
- if (viewPartMainLineCount > 1 && !(viewPartMainLastLineWidth + viewPartInfoWidth > viewPartMain.getMeasuredWidth())) {
+ final float lastLineWithInfoWidth = viewPartMainLastLineWidth + viewPartInfoWidth;
+ if (viewPartMainLineCount > 1 && lastLineWithInfoWidth <= viewPartMain.getMeasuredWidth()) {
widthSize += viewPartMainWidth;
heightSize += viewPartMainHeight;
- } else if (viewPartMainLineCount > 1 && (viewPartMainLastLineWidth + viewPartInfoWidth > availableWidth)) {
+ } else if (viewPartMainLineCount > 1 && (lastLineWithInfoWidth > availableWidth)) {
widthSize += viewPartMainWidth;
heightSize += viewPartMainHeight + viewPartInfoHeight;
} else if (viewPartMainLineCount == 1 && (viewPartMainWidth + viewPartInfoWidth > availableWidth)) {
@@ -120,6 +123,16 @@ public class ChatMessageLayout extends FrameLayout {
heightSize += viewPartMainHeight;
widthSize += viewPartMainWidth + viewPartInfoWidth;
}
+
+ // if (isInEditMode()) {
+ // TextView wDebugView = (TextView) ((ViewGroup) this.getParent()).findViewWithTag("debug");
+ // wDebugView.setText(lastLineWithInfoWidth
+ // + "\n" + availableWidth
+ // + "\n" + viewPartMain.getMeasuredWidth()
+ // + "\n" + (lastLineWithInfoWidth <= viewPartMain.getMeasuredWidth())
+ // + "\n" + (lastLineWithInfoWidth > availableWidth)
+ // + "\n" + (viewPartMainWidth + viewPartInfoWidth > availableWidth));
+ // }
}
setMeasuredDimension(widthSize, heightSize);
super.onMeasure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY));
diff --git a/app/src/main/java/awais/instagrabber/customviews/FormattedNumberTextView.java b/app/src/main/java/awais/instagrabber/customviews/FormattedNumberTextView.java
new file mode 100644
index 00000000..99c2a216
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/FormattedNumberTextView.java
@@ -0,0 +1,165 @@
+package awais.instagrabber.customviews;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.transition.ChangeBounds;
+import androidx.transition.Transition;
+import androidx.transition.TransitionManager;
+import androidx.transition.TransitionSet;
+
+import java.time.Duration;
+
+import awais.instagrabber.customviews.helpers.ChangeText;
+import awais.instagrabber.utils.NumberUtils;
+
+public class FormattedNumberTextView extends AppCompatTextView {
+ private static final String TAG = FormattedNumberTextView.class.getSimpleName();
+ private static final Transition TRANSITION;
+
+ private long number = Long.MIN_VALUE;
+ private boolean showAbbreviation = true;
+ private boolean animateChanges = false;
+ private boolean toggleOnClick = true;
+ private boolean autoToggleToAbbreviation = true;
+ private long autoToggleTimeoutMs = Duration.ofSeconds(2).toMillis();
+ private boolean initDone = false;
+
+ static {
+ final TransitionSet transitionSet = new TransitionSet();
+ final ChangeText changeText = new ChangeText().setChangeBehavior(ChangeText.CHANGE_BEHAVIOR_OUT_IN);
+ transitionSet.addTransition(changeText).addTransition(new ChangeBounds());
+ TRANSITION = transitionSet;
+ }
+
+
+ public FormattedNumberTextView(@NonNull final Context context) {
+ super(context);
+ init();
+ }
+
+ public FormattedNumberTextView(@NonNull final Context context, @Nullable final AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public FormattedNumberTextView(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ private void init() {
+ if (initDone) return;
+ setupClickToggle();
+ initDone = true;
+ }
+
+ private void setupClickToggle() {
+ setOnClickListener(null);
+ }
+
+ private OnClickListener getWrappedClickListener(@Nullable final OnClickListener l) {
+ if (!toggleOnClick) {
+ return l;
+ }
+ return v -> {
+ toggleAbbreviation();
+ if (l != null) {
+ l.onClick(this);
+ }
+ };
+ }
+
+ public void setNumber(final long number) {
+ if (this.number == number) return;
+ this.number = number;
+ format();
+ }
+
+ public void clearNumber() {
+ if (number == Long.MIN_VALUE) return;
+ number = Long.MIN_VALUE;
+ format();
+ }
+
+ public void setShowAbbreviation(final boolean showAbbreviation) {
+ if (this.showAbbreviation && showAbbreviation) return;
+ this.showAbbreviation = showAbbreviation;
+ format();
+ }
+
+ public boolean isShowAbbreviation() {
+ return showAbbreviation;
+ }
+
+ private void toggleAbbreviation() {
+ if (number == Long.MIN_VALUE) return;
+ setShowAbbreviation(!showAbbreviation);
+ }
+
+ public void setToggleOnClick(final boolean toggleOnClick) {
+ this.toggleOnClick = toggleOnClick;
+ }
+
+ public boolean isToggleOnClick() {
+ return toggleOnClick;
+ }
+
+ public void setAutoToggleToAbbreviation(final boolean autoToggleToAbbreviation) {
+ this.autoToggleToAbbreviation = autoToggleToAbbreviation;
+ }
+
+ public boolean isAutoToggleToAbbreviation() {
+ return autoToggleToAbbreviation;
+ }
+
+ public void setAutoToggleTimeoutMs(final long autoToggleTimeoutMs) {
+ this.autoToggleTimeoutMs = autoToggleTimeoutMs;
+ }
+
+ public long getAutoToggleTimeoutMs() {
+ return autoToggleTimeoutMs;
+ }
+
+ public void setAnimateChanges(final boolean animateChanges) {
+ this.animateChanges = animateChanges;
+ }
+
+ public boolean isAnimateChanges() {
+ return animateChanges;
+ }
+
+ @Override
+ public void setOnClickListener(@Nullable final OnClickListener l) {
+ super.setOnClickListener(getWrappedClickListener(l));
+ }
+
+ private void format() {
+ post(() -> {
+ if (animateChanges) {
+ try {
+ TransitionManager.beginDelayedTransition((ViewGroup) getParent(), TRANSITION);
+ } catch (Exception e) {
+ Log.e(TAG, "format: ", e);
+ }
+ }
+ if (number == Long.MIN_VALUE) {
+ setText(null);
+ return;
+ }
+ if (showAbbreviation) {
+ setText(NumberUtils.abbreviate(number));
+ return;
+ }
+ setText(String.valueOf(number));
+ if (autoToggleToAbbreviation) {
+ getHandler().postDelayed(() -> setShowAbbreviation(true), autoToggleTimeoutMs);
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/FragmentNavigatorWithDefaultAnimations.java b/app/src/main/java/awais/instagrabber/customviews/FragmentNavigatorWithDefaultAnimations.java
new file mode 100644
index 00000000..358e34d8
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/FragmentNavigatorWithDefaultAnimations.java
@@ -0,0 +1,75 @@
+package awais.instagrabber.customviews;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentManager;
+import androidx.navigation.NavDestination;
+import androidx.navigation.NavOptions;
+import androidx.navigation.Navigator;
+import androidx.navigation.fragment.FragmentNavigator;
+
+import awais.instagrabber.R;
+
+@Navigator.Name("fragment")
+public class FragmentNavigatorWithDefaultAnimations extends FragmentNavigator {
+
+ private final NavOptions emptyNavOptions = new NavOptions.Builder().build();
+ // private final NavOptions defaultNavOptions = new NavOptions.Builder()
+ // .setEnterAnim(R.animator.nav_default_enter_anim)
+ // .setExitAnim(R.animator.nav_default_exit_anim)
+ // .setPopEnterAnim(R.animator.nav_default_pop_enter_anim)
+ // .setPopExitAnim(R.animator.nav_default_pop_exit_anim)
+ // .build();
+
+ private final NavOptions defaultNavOptions = new NavOptions.Builder()
+ .setEnterAnim(R.anim.slide_in_right)
+ .setExitAnim(R.anim.slide_out_left)
+ .setPopEnterAnim(android.R.anim.slide_in_left)
+ .setPopExitAnim(android.R.anim.slide_out_right)
+ .build();
+
+ public FragmentNavigatorWithDefaultAnimations(@NonNull final Context context,
+ @NonNull final FragmentManager manager,
+ final int containerId) {
+ super(context, manager, containerId);
+ }
+
+ @Nullable
+ @Override
+ public NavDestination navigate(@NonNull final Destination destination,
+ @Nullable final Bundle args,
+ @Nullable final NavOptions navOptions,
+ @Nullable final Navigator.Extras navigatorExtras) {
+ // this will try to fill in empty animations with defaults when no shared element transitions are set
+ // https://developer.android.com/guide/navigation/navigation-animate-transitions#shared-element
+ final boolean shouldUseTransitionsInstead = navigatorExtras != null;
+ final NavOptions navOptions1 = shouldUseTransitionsInstead ? navOptions : fillEmptyAnimationsWithDefaults(navOptions);
+ return super.navigate(destination, args, navOptions1, navigatorExtras);
+ }
+
+ private NavOptions fillEmptyAnimationsWithDefaults(@Nullable final NavOptions navOptions) {
+ if (navOptions == null) {
+ return defaultNavOptions;
+ }
+ return copyNavOptionsWithDefaultAnimations(navOptions);
+ }
+
+ @NonNull
+ private NavOptions copyNavOptionsWithDefaultAnimations(@NonNull final NavOptions navOptions) {
+ return new NavOptions.Builder()
+ .setLaunchSingleTop(navOptions.shouldLaunchSingleTop())
+ .setPopUpTo(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive())
+ .setEnterAnim(navOptions.getEnterAnim() == emptyNavOptions.getEnterAnim()
+ ? defaultNavOptions.getEnterAnim() : navOptions.getEnterAnim())
+ .setExitAnim(navOptions.getExitAnim() == emptyNavOptions.getExitAnim()
+ ? defaultNavOptions.getExitAnim() : navOptions.getExitAnim())
+ .setPopEnterAnim(navOptions.getPopEnterAnim() == emptyNavOptions.getPopEnterAnim()
+ ? defaultNavOptions.getPopEnterAnim() : navOptions.getPopEnterAnim())
+ .setPopExitAnim(navOptions.getPopExitAnim() == emptyNavOptions.getPopExitAnim()
+ ? defaultNavOptions.getPopExitAnim() : navOptions.getPopExitAnim())
+ .build();
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/InsetsAnimationLinearLayout.java b/app/src/main/java/awais/instagrabber/customviews/InsetsAnimationLinearLayout.java
new file mode 100644
index 00000000..3e08924c
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/InsetsAnimationLinearLayout.java
@@ -0,0 +1,246 @@
+package awais.instagrabber.customviews;
+
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.WindowInsetsAnimation;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.view.NestedScrollingParent3;
+import androidx.core.view.NestedScrollingParentHelper;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+
+import java.util.Arrays;
+
+import awais.instagrabber.customviews.helpers.SimpleImeAnimationController;
+import awais.instagrabber.utils.ViewUtils;
+
+import static androidx.core.view.ViewCompat.TYPE_TOUCH;
+
+public final class InsetsAnimationLinearLayout extends LinearLayout implements NestedScrollingParent3 {
+ private final NestedScrollingParentHelper nestedScrollingParentHelper = new NestedScrollingParentHelper(this);
+ private final SimpleImeAnimationController imeAnimController = new SimpleImeAnimationController();
+ private final int[] tempIntArray2 = new int[2];
+ private final int[] startViewLocation = new int[2];
+
+ private View currentNestedScrollingChild;
+ private int dropNextY;
+ private boolean scrollImeOffScreenWhenVisible = true;
+ private boolean scrollImeOnScreenWhenNotVisible = true;
+ private boolean scrollImeOffScreenWhenVisibleOnFling = false;
+ private boolean scrollImeOnScreenWhenNotVisibleOnFling = false;
+
+ public InsetsAnimationLinearLayout(final Context context, @Nullable final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public InsetsAnimationLinearLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public final boolean getScrollImeOffScreenWhenVisible() {
+ return scrollImeOffScreenWhenVisible;
+ }
+
+ public final void setScrollImeOffScreenWhenVisible(boolean scrollImeOffScreenWhenVisible) {
+ this.scrollImeOffScreenWhenVisible = scrollImeOffScreenWhenVisible;
+ }
+
+ public final boolean getScrollImeOnScreenWhenNotVisible() {
+ return scrollImeOnScreenWhenNotVisible;
+ }
+
+ public final void setScrollImeOnScreenWhenNotVisible(boolean scrollImeOnScreenWhenNotVisible) {
+ this.scrollImeOnScreenWhenNotVisible = scrollImeOnScreenWhenNotVisible;
+ }
+
+ public boolean getScrollImeOffScreenWhenVisibleOnFling() {
+ return scrollImeOffScreenWhenVisibleOnFling;
+ }
+
+ public void setScrollImeOffScreenWhenVisibleOnFling(final boolean scrollImeOffScreenWhenVisibleOnFling) {
+ this.scrollImeOffScreenWhenVisibleOnFling = scrollImeOffScreenWhenVisibleOnFling;
+ }
+
+ public boolean getScrollImeOnScreenWhenNotVisibleOnFling() {
+ return scrollImeOnScreenWhenNotVisibleOnFling;
+ }
+
+ public void setScrollImeOnScreenWhenNotVisibleOnFling(final boolean scrollImeOnScreenWhenNotVisibleOnFling) {
+ this.scrollImeOnScreenWhenNotVisibleOnFling = scrollImeOnScreenWhenNotVisibleOnFling;
+ }
+
+ public SimpleImeAnimationController getImeAnimController() {
+ return imeAnimController;
+ }
+
+ @Override
+ public boolean onStartNestedScroll(@NonNull final View child,
+ @NonNull final View target,
+ final int axes,
+ final int type) {
+ return (axes & SCROLL_AXIS_VERTICAL) != 0 && type == TYPE_TOUCH;
+ }
+
+ @Override
+ public void onNestedScrollAccepted(@NonNull final View child,
+ @NonNull final View target,
+ final int axes,
+ final int type) {
+ nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type);
+ currentNestedScrollingChild = child;
+ }
+
+ @Override
+ public void onNestedPreScroll(@NonNull final View target,
+ final int dx,
+ final int dy,
+ @NonNull final int[] consumed,
+ final int type) {
+ if (imeAnimController.isInsetAnimationRequestPending()) {
+ consumed[0] = dx;
+ consumed[1] = dy;
+ } else {
+ int deltaY = dy;
+ if (dropNextY != 0) {
+ consumed[1] = dropNextY;
+ deltaY = dy - dropNextY;
+ dropNextY = 0;
+ }
+
+ if (deltaY < 0) {
+ if (imeAnimController.isInsetAnimationInProgress()) {
+ consumed[1] -= imeAnimController.insetBy(-deltaY);
+ } else if (scrollImeOffScreenWhenVisible && !imeAnimController.isInsetAnimationRequestPending()) {
+ WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(this);
+ if (rootWindowInsets != null) {
+ if (rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime())) {
+ startControlRequest();
+ consumed[1] = deltaY;
+ }
+ }
+ }
+ }
+
+ }
+ }
+
+ @Override
+ public void onNestedScroll(@NonNull final View target,
+ final int dxConsumed,
+ final int dyConsumed,
+ final int dxUnconsumed,
+ final int dyUnconsumed,
+ final int type,
+ @NonNull final int[] consumed) {
+ if (dyUnconsumed > 0) {
+ if (imeAnimController.isInsetAnimationInProgress()) {
+ consumed[1] = -imeAnimController.insetBy(-dyUnconsumed);
+ } else if (scrollImeOnScreenWhenNotVisible && !imeAnimController.isInsetAnimationRequestPending()) {
+ WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(this);
+ if (rootWindowInsets != null) {
+ if (!rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime())) {
+ startControlRequest();
+ consumed[1] = dyUnconsumed;
+ }
+ }
+ }
+ }
+
+ }
+
+ @Override
+ public boolean onNestedFling(@NonNull final View target,
+ final float velocityX,
+ final float velocityY,
+ final boolean consumed) {
+ if (imeAnimController.isInsetAnimationInProgress()) {
+ imeAnimController.animateToFinish(velocityY);
+ return true;
+ } else {
+ boolean imeVisible = false;
+ final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(this);
+ if (rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime())) {
+ imeVisible = true;
+ }
+ if (velocityY > 0 && scrollImeOnScreenWhenNotVisibleOnFling && !imeVisible) {
+ imeAnimController.startAndFling(this, velocityY);
+ return true;
+ } else if (velocityY < 0 && scrollImeOffScreenWhenVisibleOnFling && imeVisible) {
+ imeAnimController.startAndFling(this, velocityY);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ @Override
+ public void onStopNestedScroll(@NonNull final View target, final int type) {
+ nestedScrollingParentHelper.onStopNestedScroll(target, type);
+ if (imeAnimController.isInsetAnimationInProgress() && !imeAnimController.isInsetAnimationFinishing()) {
+ imeAnimController.animateToFinish(null);
+ }
+ reset();
+ }
+
+ @Override
+ public void dispatchWindowInsetsAnimationPrepare(@NonNull final WindowInsetsAnimation animation) {
+ super.dispatchWindowInsetsAnimationPrepare(animation);
+ ViewUtils.suppressLayoutCompat(this, false);
+ }
+
+ private void startControlRequest() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ return;
+ }
+ ViewUtils.suppressLayoutCompat(this, true);
+ if (currentNestedScrollingChild != null) {
+ currentNestedScrollingChild.getLocationInWindow(startViewLocation);
+ }
+ imeAnimController.startControlRequest(this, windowInsetsAnimationControllerCompat -> onControllerReady());
+ }
+
+ private void onControllerReady() {
+ if (currentNestedScrollingChild != null) {
+ imeAnimController.insetBy(0);
+ int[] location = tempIntArray2;
+ currentNestedScrollingChild.getLocationInWindow(location);
+ dropNextY = location[1] - startViewLocation[1];
+ }
+
+ }
+
+ private void reset() {
+ dropNextY = 0;
+ Arrays.fill(startViewLocation, 0);
+ ViewUtils.suppressLayoutCompat(this, false);
+ }
+
+ @Override
+ public void onNestedScrollAccepted(@NonNull final View child,
+ @NonNull final View target,
+ final int axes) {
+ onNestedScrollAccepted(child, target, axes, TYPE_TOUCH);
+ }
+
+ @Override
+ public void onNestedScroll(@NonNull final View target,
+ final int dxConsumed,
+ final int dyConsumed,
+ final int dxUnconsumed,
+ final int dyUnconsumed,
+ final int type) {
+ onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, tempIntArray2);
+ }
+
+ @Override
+ public void onStopNestedScroll(@NonNull final View target) {
+ onStopNestedScroll(target, TYPE_TOUCH);
+ }
+}
+
diff --git a/app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingCoordinatorLayout.java b/app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingCoordinatorLayout.java
new file mode 100644
index 00000000..13a93e43
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingCoordinatorLayout.java
@@ -0,0 +1,33 @@
+package awais.instagrabber.customviews;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.WindowInsets;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+
+public class InsetsNotifyingCoordinatorLayout extends CoordinatorLayout {
+
+ public InsetsNotifyingCoordinatorLayout(@NonNull final Context context) {
+ super(context);
+ }
+
+ public InsetsNotifyingCoordinatorLayout(@NonNull final Context context, @Nullable final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public InsetsNotifyingCoordinatorLayout(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ int childCount = getChildCount();
+ for (int index = 0; index < childCount; index++) {
+ getChildAt(index).dispatchApplyWindowInsets(insets);
+ }
+ return insets;
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingLinearLayout.java b/app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingLinearLayout.java
new file mode 100644
index 00000000..b2faa4e1
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingLinearLayout.java
@@ -0,0 +1,35 @@
+package awais.instagrabber.customviews;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.WindowInsets;
+import android.widget.LinearLayout;
+
+import androidx.annotation.Nullable;
+
+public class InsetsNotifyingLinearLayout extends LinearLayout {
+ public InsetsNotifyingLinearLayout(final Context context) {
+ super(context);
+ }
+
+ public InsetsNotifyingLinearLayout(final Context context, @Nullable final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public InsetsNotifyingLinearLayout(final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public InsetsNotifyingLinearLayout(final Context context, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ int childCount = getChildCount();
+ for (int index = 0; index < childCount; index++) {
+ getChildAt(index).dispatchApplyWindowInsets(insets);
+ }
+ return insets;
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/NavHostFragmentWithDefaultAnimations.java b/app/src/main/java/awais/instagrabber/customviews/NavHostFragmentWithDefaultAnimations.java
new file mode 100644
index 00000000..11621a6c
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/NavHostFragmentWithDefaultAnimations.java
@@ -0,0 +1,60 @@
+package awais.instagrabber.customviews;
+
+import android.os.Bundle;
+
+import androidx.annotation.NavigationRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.navigation.NavController;
+import androidx.navigation.Navigator;
+import androidx.navigation.fragment.FragmentNavigator;
+import androidx.navigation.fragment.NavHostFragment;
+
+public class NavHostFragmentWithDefaultAnimations extends NavHostFragment {
+ private static final String KEY_GRAPH_ID = "android-support-nav:fragment:graphId";
+ private static final String KEY_START_DESTINATION_ARGS =
+ "android-support-nav:fragment:startDestinationArgs";
+ private static final String KEY_NAV_CONTROLLER_STATE =
+ "android-support-nav:fragment:navControllerState";
+ private static final String KEY_DEFAULT_NAV_HOST = "android-support-nav:fragment:defaultHost";
+
+ @NonNull
+ public static NavHostFragment create(@NavigationRes int graphResId) {
+ return create(graphResId, null);
+ }
+
+ @NonNull
+ public static NavHostFragment create(@NavigationRes int graphResId,
+ @Nullable Bundle startDestinationArgs) {
+ Bundle b = null;
+ if (graphResId != 0) {
+ b = new Bundle();
+ b.putInt(KEY_GRAPH_ID, graphResId);
+ }
+ if (startDestinationArgs != null) {
+ if (b == null) {
+ b = new Bundle();
+ }
+ b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs);
+ }
+
+ final NavHostFragmentWithDefaultAnimations result = new NavHostFragmentWithDefaultAnimations();
+ if (b != null) {
+ result.setArguments(b);
+ }
+ return result;
+ }
+
+ @NonNull
+ @Override
+ protected Navigator extends FragmentNavigator.Destination> createFragmentNavigator() {
+ return new FragmentNavigatorWithDefaultAnimations(requireContext(), getChildFragmentManager(), getId());
+ }
+
+ @Override
+ protected void onCreateNavController(@NonNull final NavController navController) {
+ super.onCreateNavController(navController);
+ navController.getNavigatorProvider()
+ .addNavigator(new FragmentNavigatorWithDefaultAnimations(requireContext(), getChildFragmentManager(), getId()));
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java b/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java
index f01a5528..2e008a18 100644
--- a/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java
+++ b/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java
@@ -3,6 +3,8 @@ package awais.instagrabber.customviews;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -25,6 +27,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.function.Function;
import awais.instagrabber.adapters.FeedAdapterV2;
import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration;
@@ -60,14 +63,17 @@ public class PostsRecyclerView extends RecyclerView {
private FeedAdapterV2.FeedItemCallback feedItemCallback;
private boolean shouldScrollToTop;
private FeedAdapterV2.SelectionModeCallback selectionModeCallback;
+ private Function headerViewCreator;
+ private Function headerBinder;
+ private boolean refresh = true;
private final List fetchStatusChangeListeners = new ArrayList<>();
private final FetchListener> fetchListener = new FetchListener>() {
@Override
public void onResult(final List result) {
- final int currentPage = lazyLoader.getCurrentPage();
- if (currentPage == 0) {
+ if (refresh) {
+ refresh = false;
mediaViewModel.getList().postValue(result);
shouldScrollToTop = true;
dispatchFetchStatus();
@@ -198,21 +204,19 @@ public class PostsRecyclerView extends RecyclerView {
Log.e(TAG, "initSelf: ", e);
}
if (mediaViewModel == null) return;
- mediaViewModel.getList().observe(lifeCycleOwner, list -> {
- if (list.size() <= 0) return;
- feedAdapter.submitList(list, () -> {
- // postDelayed(this::fetchMoreIfPossible, 1000);
- if (!shouldScrollToTop) return;
- smoothScrollToPosition(0);
- shouldScrollToTop = false;
- });
- });
+ mediaViewModel.getList().observe(lifeCycleOwner, list -> feedAdapter.submitList(list, () -> {
+ // postDelayed(this::fetchMoreIfPossible, 1000);
+ if (!shouldScrollToTop) return;
+ shouldScrollToTop = false;
+ post(() -> smoothScrollToPosition(0));
+ }));
postFetcher = new PostFetcher(postFetchService, fetchListener);
if (layoutPreferences.getHasGap()) {
addItemDecoration(gridSpacingItemDecoration);
}
setHasFixedSize(true);
setNestedScrollingEnabled(true);
+ setItemAnimator(null);
lazyLoader = new RecyclerLazyLoaderAtEdge(layoutManager, (page) -> {
if (postFetcher.hasMore()) {
postFetcher.fetch();
@@ -316,11 +320,12 @@ public class PostsRecyclerView extends RecyclerView {
}
public void refresh() {
+ refresh = true;
if (lazyLoader != null) {
lazyLoader.resetState();
}
if (postFetcher != null) {
- mediaViewModel.getList().postValue(Collections.emptyList());
+ // mediaViewModel.getList().postValue(Collections.emptyList());
postFetcher.reset();
postFetcher.fetch();
}
diff --git a/app/src/main/java/awais/instagrabber/customviews/Tooltip.java b/app/src/main/java/awais/instagrabber/customviews/Tooltip.java
index 91a07e42..42bbbb6e 100644
--- a/app/src/main/java/awais/instagrabber/customviews/Tooltip.java
+++ b/app/src/main/java/awais/instagrabber/customviews/Tooltip.java
@@ -9,20 +9,20 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
+import androidx.annotation.NonNull;
import androidx.appcompat.widget.AppCompatTextView;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.utils.ViewUtils;
-
public class Tooltip extends AppCompatTextView {
private View anchor;
private ViewPropertyAnimator animator;
private boolean showing;
- private final AppExecutors appExecutors;
+ private final AppExecutors appExecutors = AppExecutors.getInstance();
private final Runnable dismissRunnable = () -> {
animator = animate().alpha(0).setListener(new AnimatorListenerAdapter() {
@Override
@@ -33,7 +33,7 @@ public class Tooltip extends AppCompatTextView {
animator.start();
};
- public Tooltip(Context context, ViewGroup parentView, int backgroundColor, int textColor) {
+ public Tooltip(@NonNull Context context, @NonNull ViewGroup parentView, int backgroundColor, int textColor) {
super(context);
setBackgroundDrawable(ViewUtils.createRoundRectDrawable(Utils.convertDpToPx(3), backgroundColor));
setTextColor(textColor);
@@ -43,7 +43,6 @@ public class Tooltip extends AppCompatTextView {
parentView.addView(this, ViewUtils.createFrame(
ViewUtils.WRAP_CONTENT, ViewUtils.WRAP_CONTENT, Gravity.START | Gravity.TOP, 5, 0, 5, 3));
setVisibility(GONE);
- appExecutors = AppExecutors.getInstance();
}
@Override
diff --git a/app/src/main/java/awais/instagrabber/customviews/VideoPlayerCallbackAdapter.java b/app/src/main/java/awais/instagrabber/customviews/VideoPlayerCallbackAdapter.java
index 30b9bbca..dd75c15f 100644
--- a/app/src/main/java/awais/instagrabber/customviews/VideoPlayerCallbackAdapter.java
+++ b/app/src/main/java/awais/instagrabber/customviews/VideoPlayerCallbackAdapter.java
@@ -1,5 +1,7 @@
package awais.instagrabber.customviews;
+import com.google.android.exoplayer2.ui.StyledPlayerView;
+
public class VideoPlayerCallbackAdapter implements VideoPlayerViewHelper.VideoPlayerCallback {
@Override
public void onThumbnailLoaded() {}
@@ -18,4 +20,12 @@ public class VideoPlayerCallbackAdapter implements VideoPlayerViewHelper.VideoPl
@Override
public void onRelease() {}
+
+ @Override
+ public void onFullScreenModeChanged(final boolean isFullScreen, final StyledPlayerView playerView) {}
+
+ @Override
+ public boolean isInFullScreen() {
+ return false;
+ }
}
diff --git a/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java b/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java
index 56ed8976..6ce21a77 100644
--- a/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java
+++ b/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java
@@ -1,14 +1,19 @@
package awais.instagrabber.customviews;
import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
import android.graphics.drawable.Animatable;
import android.net.Uri;
import android.os.Looper;
import android.util.Log;
import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
import androidx.annotation.NonNull;
-import androidx.appcompat.widget.PopupMenu;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatImageButton;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.backends.pipeline.PipelineDraweeControllerBuilder;
@@ -22,16 +27,23 @@ import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioListener;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
+import com.google.android.exoplayer2.ui.StyledPlayerControlView;
+import com.google.android.exoplayer2.ui.StyledPlayerView;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
-import com.google.android.material.button.MaterialButton;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import awais.instagrabber.R;
import awais.instagrabber.databinding.LayoutVideoPlayerWithThumbnailBinding;
+import awais.instagrabber.utils.Utils;
public class VideoPlayerViewHelper implements Player.EventListener {
- private static final String TAG = "VideoPlayerViewHelper";
- // private static final long INITIAL_DELAY = 0;
- // private static final long RECURRING_DELAY = 60;
+ private static final String TAG = VideoPlayerViewHelper.class.getSimpleName();
private final Context context;
private final LayoutVideoPlayerWithThumbnailBinding binding;
@@ -39,74 +51,20 @@ public class VideoPlayerViewHelper implements Player.EventListener {
private final float thumbnailAspectRatio;
private final String thumbnailUrl;
private final boolean loadPlayerOnClick;
- // private final LayoutExoCustomControlsBinding controlsBinding;
private final VideoPlayerCallback videoPlayerCallback;
private final String videoUrl;
private final DefaultDataSourceFactory dataSourceFactory;
private SimpleExoPlayer player;
- private PopupMenu speedPopup;
- // private PositionCheckRunnable positionChecker;
- // private Handler positionUpdateHandler;
+ private AppCompatImageButton mute;
- // private final Player.EventListener listener = new Player.EventListener() {
- // @Override
- // public void onPlaybackStateChanged(final int state) {
- // // switch (state) {
- // // case Player.STATE_BUFFERING:
- // // case STATE_IDLE:
- // // case STATE_ENDED:
- // // positionUpdateHandler.removeCallbacks(positionChecker);
- // // return;
- // // case STATE_READY:
- // // setupTimeline();
- // // positionUpdateHandler.postDelayed(positionChecker, INITIAL_DELAY);
- // // break;
- // // }
- // }
- //
- // @Override
- // public void onPlayWhenReadyChanged(final boolean playWhenReady, final int reason) {
- // // updatePlayPauseDrawable(playWhenReady);
- // // if (positionUpdateHandler == null || positionChecker == null) return;
- // // if (playWhenReady) {
- // // positionUpdateHandler.removeCallbacks(positionChecker);
- // // positionUpdateHandler.postDelayed(positionChecker, INITIAL_DELAY);
- // // }
- // }
- // };
private final AudioListener audioListener = new AudioListener() {
@Override
public void onVolumeChanged(final float volume) {
updateMuteIcon(volume);
}
};
- // private final Slider.OnChangeListener onChangeListener = (slider, value, fromUser) -> {
- // if (!fromUser) return;
- // long actualValue = (long) value;
- // if (actualValue < 0) {
- // actualValue = 0;
- // } else if (actualValue > player.getDuration()) {
- // actualValue = player.getDuration();
- // }
- // player.seekTo(actualValue);
- // };
- // private final View.OnClickListener onClickListener = v -> player.setPlayWhenReady(!player.getPlayWhenReady());
- // // private final LabelFormatter labelFormatter = value -> TextUtils.millisToTimeString((long) value);
private final View.OnClickListener muteOnClickListener = v -> toggleMute();
- private MaterialButton mute;
- // private final View.OnClickListener rewOnClickListener = v -> {
- // final long positionMs = player.getCurrentPosition() - 5000;
- // player.seekTo(positionMs < 0 ? 0 : positionMs);
- // };
- // private final View.OnClickListener ffOnClickListener = v -> {
- // long positionMs = player.getCurrentPosition() + 5000;
- // long duration = player.getDuration();
- // if (duration == TIME_UNSET) {
- // duration = 0;
- // }
- // player.seekTo(Math.min(positionMs, duration));
- // };
- // private final View.OnClickListener showMenu = this::showMenu;
+ private Object layoutManager;
public VideoPlayerViewHelper(@NonNull final Context context,
@NonNull final LayoutVideoPlayerWithThumbnailBinding binding,
@@ -115,7 +73,6 @@ public class VideoPlayerViewHelper implements Player.EventListener {
final float thumbnailAspectRatio,
final String thumbnailUrl,
final boolean loadPlayerOnClick,
- // final LayoutExoCustomControlsBinding controlsBinding,
final VideoPlayerCallback videoPlayerCallback) {
this.context = context;
this.binding = binding;
@@ -123,7 +80,6 @@ public class VideoPlayerViewHelper implements Player.EventListener {
this.thumbnailAspectRatio = thumbnailAspectRatio;
this.thumbnailUrl = thumbnailUrl;
this.loadPlayerOnClick = loadPlayerOnClick;
- // this.controlsBinding = controlsBinding;
this.videoPlayerCallback = videoPlayerCallback;
this.videoUrl = videoUrl;
this.dataSourceFactory = new DefaultDataSourceFactory(binding.getRoot().getContext(), "instagram");
@@ -140,7 +96,6 @@ public class VideoPlayerViewHelper implements Player.EventListener {
}
});
setThumbnail();
- // setupControls();
}
private void setThumbnail() {
@@ -149,25 +104,25 @@ public class VideoPlayerViewHelper implements Player.EventListener {
if (thumbnailUrl != null) {
thumbnailRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(thumbnailUrl)).build();
}
- final PipelineDraweeControllerBuilder builder = Fresco.newDraweeControllerBuilder()
- .setControllerListener(new BaseControllerListener() {
- @Override
- public void onFailure(final String id,
- final Throwable throwable) {
- if (videoPlayerCallback != null) {
- videoPlayerCallback.onThumbnailLoaded();
- }
- }
+ final PipelineDraweeControllerBuilder builder = Fresco
+ .newDraweeControllerBuilder()
+ .setControllerListener(new BaseControllerListener() {
+ @Override
+ public void onFailure(final String id, final Throwable throwable) {
+ if (videoPlayerCallback != null) {
+ videoPlayerCallback.onThumbnailLoaded();
+ }
+ }
- @Override
- public void onFinalImageSet(final String id,
- final ImageInfo imageInfo,
- final Animatable animatable) {
- if (videoPlayerCallback != null) {
- videoPlayerCallback.onThumbnailLoaded();
- }
- }
- });
+ @Override
+ public void onFinalImageSet(final String id,
+ final ImageInfo imageInfo,
+ final Animatable animatable) {
+ if (videoPlayerCallback != null) {
+ videoPlayerCallback.onThumbnailLoaded();
+ }
+ }
+ });
if (thumbnailRequest != null) {
builder.setImageRequest(thumbnailRequest);
}
@@ -176,8 +131,8 @@ public class VideoPlayerViewHelper implements Player.EventListener {
private void loadPlayer() {
if (videoUrl == null) return;
- if (binding.root.getDisplayedChild() == 0) {
- binding.root.showNext();
+ if (binding.getRoot().getDisplayedChild() == 0) {
+ binding.getRoot().showNext();
}
if (videoPlayerCallback != null) {
videoPlayerCallback.onPlayerViewLoaded();
@@ -186,14 +141,13 @@ public class VideoPlayerViewHelper implements Player.EventListener {
if (player != null) {
player.release();
}
+ final ViewGroup.LayoutParams playerViewLayoutParams = binding.playerView.getLayoutParams();
+ if (playerViewLayoutParams.height > Utils.displayMetrics.heightPixels * 0.8) {
+ playerViewLayoutParams.height = (int) (Utils.displayMetrics.heightPixels * 0.8);
+ }
player = new SimpleExoPlayer.Builder(context)
.setLooper(Looper.getMainLooper())
.build();
- // positionUpdateHandler = new Handler();
- // positionChecker = new PositionCheckRunnable(positionUpdateHandler,
- // player,
- // controlsBinding.timeline,
- // controlsBinding.fromTime);
player.addListener(this);
player.addAudioListener(audioListener);
player.setVolume(initialVolume);
@@ -203,135 +157,118 @@ public class VideoPlayerViewHelper implements Player.EventListener {
final MediaItem mediaItem = MediaItem.fromUri(videoUrl);
final ProgressiveMediaSource mediaSource = sourceFactory.createMediaSource(mediaItem);
player.setMediaSource(mediaSource);
- // setupControls();
player.prepare();
binding.playerView.setPlayer(player);
- binding.playerView.setShowFastForwardButton(false);
- binding.playerView.setShowRewindButton(false);
- // binding.controls.setPlayer(player);
- mute = binding.playerView.findViewById(R.id.mute);
- // mute = binding.controls.findViewById(R.id.mute);
- if (mute != null) {
- mute.setOnClickListener(muteOnClickListener);
- }
- updateMuteIcon(player.getVolume());
+ binding.playerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
+ binding.playerView.setShowNextButton(false);
+ binding.playerView.setShowPreviousButton(false);
+ binding.playerView.setControllerOnFullScreenModeChangedListener(isFullScreen -> {
+ if (videoPlayerCallback == null) return;
+ videoPlayerCallback.onFullScreenModeChanged(isFullScreen, binding.playerView);
+ });
+ setupControllerView();
}
- // private void setupControls() {
- // if (controlsBinding == null) return;
- // // binding.playerView.setUseController(false);
- // if (player == null) {
- // // enableControls(false);
- // // controlsBinding.playPause.setEnabled(true);
- // // controlsBinding.playPause.setOnClickListener(new NoPlayerPlayPauseClickListener(binding.thumbnailParent));
- // return;
- // }
- // // enableControls(true);
- // // updatePlayPauseDrawable(player.getPlayWhenReady());
- // // updateMuteIcon(player.getVolume());
- // player.addListener(listener);
- // // player.addAudioListener(audioListener);
- // // controlsBinding.timeline.addOnChangeListener(onChangeListener);
- // // controlsBinding.timeline.setLabelFormatter(labelFormatter);
- // // controlsBinding.playPause.setOnClickListener(onClickListener);
- // // controlsBinding.mute.setOnClickListener(muteOnClickListener);
- // // controlsBinding.rewWithAmount.setOnClickListener(rewOnClickListener);
- // // controlsBinding.ffWithAmount.setOnClickListener(ffOnClickListener);
- // // controlsBinding.speed.setOnClickListener(showMenu);
- // }
+ private void setupControllerView() {
+ try {
+ final StyledPlayerControlView controllerView = getStyledPlayerControlView();
+ if (controllerView == null) return;
+ layoutManager = setControlViewLayoutManager(controllerView);
+ if (videoPlayerCallback != null && videoPlayerCallback.isInFullScreen()) {
+ setControllerViewToFullScreenMode(controllerView);
+ }
+ final ViewGroup exoBasicControls = controllerView.findViewById(R.id.exo_basic_controls);
+ if (exoBasicControls == null) return;
+ mute = new AppCompatImageButton(context);
+ final Resources resources = context.getResources();
+ if (resources == null) return;
+ final int width = resources.getDimensionPixelSize(R.dimen.exo_small_icon_width);
+ final int height = resources.getDimensionPixelSize(R.dimen.exo_small_icon_height);
+ final int margin = resources.getDimensionPixelSize(R.dimen.exo_small_icon_horizontal_margin);
+ final int paddingHorizontal = resources.getDimensionPixelSize(R.dimen.exo_small_icon_padding_horizontal);
+ final int paddingVertical = resources.getDimensionPixelSize(R.dimen.exo_small_icon_padding_vertical);
+ final ViewGroup.MarginLayoutParams layoutParams = new ViewGroup.MarginLayoutParams(width, height);
+ layoutParams.setMargins(margin, 0, margin, 0);
+ mute.setLayoutParams(layoutParams);
+ mute.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical);
+ mute.setScaleType(ImageView.ScaleType.FIT_XY);
+ mute.setBackgroundResource(Utils.getAttrResId(context, android.R.attr.selectableItemBackground));
+ mute.setImageTintList(ColorStateList.valueOf(resources.getColor(R.color.white)));
+ updateMuteIcon(player.getVolume());
+ exoBasicControls.addView(mute, 0);
+ mute.setOnClickListener(muteOnClickListener);
+ } catch (Exception e) {
+ Log.e(TAG, "loadPlayer: ", e);
+ }
+ }
- // private void setupTimeline() {
- // final long duration = player.getDuration();
- // controlsBinding.timeline.setEnabled(true);
- // controlsBinding.timeline.setValueFrom(0);
- // controlsBinding.timeline.setValueTo(duration);
- // controlsBinding.fromTime.setText(TextUtils.millisToTimeString(0));
- // controlsBinding.toTime.setText(TextUtils.millisToTimeString(duration));
- // }
+ @Nullable
+ private Object setControlViewLayoutManager(@NonNull final StyledPlayerControlView controllerView)
+ throws NoSuchFieldException, IllegalAccessException {
+ final Field controlViewLayoutManagerField = controllerView.getClass().getDeclaredField("controlViewLayoutManager");
+ controlViewLayoutManagerField.setAccessible(true);
+ return controlViewLayoutManagerField.get(controllerView);
+ }
- // private void enableControls(final boolean enable) {
- // controlsBinding.speed.setEnabled(enable);
- // controlsBinding.speed.setClickable(enable);
- // controlsBinding.mute.setEnabled(enable);
- // controlsBinding.mute.setClickable(enable);
- // controlsBinding.ffWithAmount.setEnabled(enable);
- // controlsBinding.ffWithAmount.setClickable(enable);
- // controlsBinding.rewWithAmount.setEnabled(enable);
- // controlsBinding.rewWithAmount.setClickable(enable);
- // // controlsBinding.fromTime.setEnabled(enable);
- // // controlsBinding.toTime.setEnabled(enable);
- // controlsBinding.playPause.setEnabled(enable);
- // controlsBinding.playPause.setClickable(enable);
- // // controlsBinding.timeline.setEnabled(enable);
- // }
+ private void setControllerViewToFullScreenMode(@NonNull final StyledPlayerControlView controllerView)
+ throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
+ // Exoplayer doesn't expose the fullscreen state, so using reflection
+ final Field fullScreenButtonField = controllerView.getClass().getDeclaredField("fullScreenButton");
+ fullScreenButtonField.setAccessible(true);
+ final ImageView fullScreenButton = (ImageView) fullScreenButtonField.get(controllerView);
+ final Field isFullScreen = controllerView.getClass().getDeclaredField("isFullScreen");
+ isFullScreen.setAccessible(true);
+ isFullScreen.set(controllerView, true);
+ final Method updateFullScreenButtonForState = controllerView
+ .getClass()
+ .getDeclaredMethod("updateFullScreenButtonForState", ImageView.class, boolean.class);
+ updateFullScreenButtonForState.setAccessible(true);
+ updateFullScreenButtonForState.invoke(controllerView, fullScreenButton, true);
- // public void showMenu(View anchor) {
- // PopupMenu popup = getPopupMenu(anchor);
- // popup.show();
- // }
+ }
- // @NonNull
- // private PopupMenu getPopupMenu(final View anchor) {
- // if (speedPopup != null) {
- // return speedPopup;
- // }
- // final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(context, R.style.popupMenuStyle);
- // // final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(context, R.style.Widget_MaterialComponents_PopupMenu_Exoplayer);
- // speedPopup = new PopupMenu(themeWrapper, anchor);
- // speedPopup.getMenuInflater().inflate(R.menu.speed_menu, speedPopup.getMenu());
- // speedPopup.setOnMenuItemClickListener(item -> {
- // float nextSpeed;
- // int textResId;
- // int itemId = item.getItemId();
- // if (itemId == R.id.pt_two_five_x) {
- // nextSpeed = 0.25f;
- // textResId = R.string.pt_two_five_x;
- // } else if (itemId == R.id.pt_five_x) {
- // nextSpeed = 0.5f;
- // textResId = R.string.pt_five_x;
- // } else if (itemId == R.id.pt_seven_five_x) {
- // nextSpeed = 0.75f;
- // textResId = R.string.pt_seven_five_x;
- // } else if (itemId == R.id.one_x) {
- // nextSpeed = 1f;
- // textResId = R.string.one_x;
- // } else if (itemId == R.id.one_pt_two_five_x) {
- // nextSpeed = 1.25f;
- // textResId = R.string.one_pt_two_five_x;
- // } else if (itemId == R.id.one_pt_five_x) {
- // nextSpeed = 1.5f;
- // textResId = R.string.one_pt_five_x;
- // } else if (itemId == R.id.two_x) {
- // nextSpeed = 2f;
- // textResId = R.string.two_x;
- // } else {
- // nextSpeed = 1;
- // textResId = R.string.one_x;
- // }
- // player.setPlaybackParameters(new PlaybackParameters(nextSpeed));
- // controlsBinding.speed.setText(textResId);
- // return true;
- // });
- // return speedPopup;
- // }
+ @Nullable
+ private StyledPlayerControlView getStyledPlayerControlView() throws NoSuchFieldException, IllegalAccessException {
+ final Field controller = binding.playerView.getClass().getDeclaredField("controller");
+ controller.setAccessible(true);
+ return (StyledPlayerControlView) controller.get(binding.playerView);
+ }
+
+ @Override
+ public void onTracksChanged(@NonNull TrackGroupArray trackGroups, @NonNull TrackSelectionArray trackSelections) {
+ if (trackGroups.isEmpty()) {
+ setHasAudio(false);
+ return;
+ }
+ boolean hasAudio = false;
+ for (int i = 0; i < trackGroups.length; i++) {
+ for (int g = 0; g < trackGroups.get(i).length; g++) {
+ final String sampleMimeType = trackGroups.get(i).getFormat(g).sampleMimeType;
+ if (sampleMimeType != null && sampleMimeType.contains("audio")) {
+ hasAudio = true;
+ break;
+ }
+ }
+ }
+ setHasAudio(hasAudio);
+ }
+
+ private void setHasAudio(final boolean hasAudio) {
+ if (mute == null) return;
+ mute.setEnabled(hasAudio);
+ mute.setAlpha(hasAudio ? 1f : 0.5f);
+ updateMuteIcon(hasAudio ? 1f : 0f);
+ }
private void updateMuteIcon(final float volume) {
if (mute == null) return;
if (volume == 0) {
- mute.setIconResource(R.drawable.ic_volume_off_24_states);
+ mute.setImageResource(R.drawable.ic_volume_off_24);
return;
}
- mute.setIconResource(R.drawable.ic_volume_up_24_states);
+ mute.setImageResource(R.drawable.ic_volume_up_24);
}
- // private void updatePlayPauseDrawable(final boolean playWhenReady) {
- // if (playWhenReady) {
- // controlsBinding.playPause.setIconResource(R.drawable.ic_pause_24);
- // return;
- // }
- // controlsBinding.playPause.setIconResource(R.drawable.ic_play_states);
- // }
-
@Override
public void onPlayWhenReadyChanged(final boolean playWhenReady, final int reason) {
if (videoPlayerCallback == null) return;
@@ -349,18 +286,18 @@ public class VideoPlayerViewHelper implements Player.EventListener {
private void toggleMute() {
if (player == null) return;
+ if (layoutManager != null) {
+ try {
+ final Method resetHideCallbacks = layoutManager.getClass().getDeclaredMethod("resetHideCallbacks");
+ resetHideCallbacks.invoke(layoutManager);
+ } catch (Exception e) {
+ Log.e(TAG, "toggleMute: ", e);
+ }
+ }
final float vol = player.getVolume() == 0f ? 1f : 0f;
player.setVolume(vol);
}
- // public void togglePlayback() {
- // if (player == null) return;
- // final int playbackState = player.getPlaybackState();
- // if (playbackState == STATE_IDLE || playbackState == STATE_ENDED) return;
- // final boolean playWhenReady = player.getPlayWhenReady();
- // player.setPlayWhenReady(!playWhenReady);
- // }
-
public void releasePlayer() {
if (videoPlayerCallback != null) {
videoPlayerCallback.onRelease();
@@ -369,86 +306,14 @@ public class VideoPlayerViewHelper implements Player.EventListener {
player.release();
player = null;
}
- // if (positionUpdateHandler != null) {
- // if (positionChecker != null) {
- // positionUpdateHandler.removeCallbacks(positionChecker);
- // positionChecker = null;
- // }
- // positionUpdateHandler = null;
- // }
}
public void pause() {
if (player != null) {
player.pause();
}
- // if (positionUpdateHandler != null) {
- // if (positionChecker != null) {
- // positionUpdateHandler.removeCallbacks(positionChecker);
- // }
- // }
}
- // public void resetTimeline() {
- // if (player == null) {
- // enableControls(false);
- // return;
- // }
- // setupTimeline();
- // final long currentPosition = player.getCurrentPosition();
- // controlsBinding.timeline.setValue(Math.min(currentPosition, player.getDuration()));
- // setupControls();
- // }
-
- // public void removeCallbacks() {
- // if (player != null) {
- // player.removeListener(listener);
- // player.removeAudioListener(audioListener);
- // }
- // controlsBinding.timeline.removeOnChangeListener(onChangeListener);
- // controlsBinding.timeline.setLabelFormatter(null);
- // controlsBinding.playPause.setOnClickListener(null);
- // controlsBinding.mute.setOnClickListener(null);
- // controlsBinding.rewWithAmount.setOnClickListener(null);
- // controlsBinding.ffWithAmount.setOnClickListener(null);
- // controlsBinding.speed.setOnClickListener(null);
- // }
-
- // private static class PositionCheckRunnable implements Runnable {
- // private final Handler positionUpdateHandler;
- // private final SimpleExoPlayer player;
- // private final Slider timeline;
- // private final AppCompatTextView fromTime;
- //
- // public PositionCheckRunnable(final Handler positionUpdateHandler,
- // final SimpleExoPlayer simpleExoPlayer,
- // final Slider slider,
- // final AppCompatTextView fromTime) {
- // this.positionUpdateHandler = positionUpdateHandler;
- // this.player = simpleExoPlayer;
- // this.timeline = slider;
- // this.fromTime = fromTime;
- // }
- //
- // @Override
- // public void run() {
- // if (positionUpdateHandler == null) return;
- // positionUpdateHandler.removeCallbacks(this);
- // if (player == null) return;
- // final long currentPosition = player.getCurrentPosition();
- // final long duration = player.getDuration();
- // if (duration == TIME_UNSET) {
- // timeline.setValueFrom(0);
- // timeline.setValueTo(0);
- // timeline.setEnabled(false);
- // return;
- // }
- // timeline.setValue(Math.min(currentPosition, duration));
- // fromTime.setText(TextUtils.millisToTimeString(currentPosition));
- // positionUpdateHandler.postDelayed(this, RECURRING_DELAY);
- // }
- // }
-
public interface VideoPlayerCallback {
void onThumbnailLoaded();
@@ -461,5 +326,9 @@ public class VideoPlayerViewHelper implements Player.EventListener {
void onPause();
void onRelease();
+
+ void onFullScreenModeChanged(boolean isFullScreen, final StyledPlayerView playerView);
+
+ boolean isInFullScreen();
}
}
diff --git a/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java b/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java
index 5a7d55ad..d8b7b8f9 100644
--- a/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java
+++ b/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java
@@ -228,7 +228,7 @@ public class ZoomableDraweeView extends DraweeView
public void setZoomingEnabled(boolean zoomingEnabled) {
mZoomingEnabled = zoomingEnabled;
- mZoomableController.setEnabled(false);
+ mZoomableController.setEnabled(zoomingEnabled);
}
/**
diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiBottomSheetDialog.java b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiBottomSheetDialog.java
new file mode 100644
index 00000000..cdf5767c
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiBottomSheetDialog.java
@@ -0,0 +1,100 @@
+package awais.instagrabber.customviews.emoji;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.bottomsheet.BottomSheetDialog;
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+
+import awais.instagrabber.R;
+import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration;
+import awais.instagrabber.utils.Utils;
+
+public class EmojiBottomSheetDialog extends BottomSheetDialogFragment {
+ public static final String TAG = EmojiBottomSheetDialog.class.getSimpleName();
+
+ private RecyclerView grid;
+ private EmojiPicker.OnEmojiClickListener callback;
+
+ @NonNull
+ public static EmojiBottomSheetDialog newInstance() {
+ // Bundle args = new Bundle();
+ // fragment.setArguments(args);
+ return new EmojiBottomSheetDialog();
+ }
+
+ @Override
+ public void onCreate(@Nullable final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setStyle(DialogFragment.STYLE_NORMAL, R.style.ThemeOverlay_Rounded_BottomSheetDialog);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) {
+ final Context context = getContext();
+ if (context == null) return null;
+ grid = new RecyclerView(context);
+ return grid;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
+ init();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ final Dialog dialog = getDialog();
+ if (dialog == null) return;
+ final BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialog;
+ final View bottomSheetInternal = bottomSheetDialog.findViewById(com.google.android.material.R.id.design_bottom_sheet);
+ if (bottomSheetInternal == null) return;
+ bottomSheetInternal.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
+ bottomSheetInternal.requestLayout();
+ }
+
+ @Override
+ public void onAttach(@NonNull final Context context) {
+ super.onAttach(context);
+ final Fragment parentFragment = getParentFragment();
+ if (parentFragment instanceof EmojiPicker.OnEmojiClickListener) {
+ callback = (EmojiPicker.OnEmojiClickListener) parentFragment;
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ grid = null;
+ super.onDestroyView();
+ }
+
+ private void init() {
+ final Context context = getContext();
+ if (context == null) return;
+ final GridLayoutManager gridLayoutManager = new GridLayoutManager(context, 9);
+ grid.setLayoutManager(gridLayoutManager);
+ grid.setHasFixedSize(true);
+ grid.setClipToPadding(false);
+ grid.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(8)));
+ final EmojiGridAdapter adapter = new EmojiGridAdapter(null, (view, emoji) -> {
+ if (callback != null) {
+ callback.onClick(view, emoji);
+ }
+ dismiss();
+ }, null);
+ grid.setAdapter(adapter);
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiGridAdapter.java b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiGridAdapter.java
index 5aff4472..1f0a7c7f 100644
--- a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiGridAdapter.java
+++ b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiGridAdapter.java
@@ -43,7 +43,7 @@ public class EmojiGridAdapter extends RecyclerView.Adapter {
- binding.image.setImageDrawable(emoji.getDrawable());
- final boolean hasVariants = !parent.getVariants().isEmpty();
- binding.indicator.setVisibility(hasVariants ? View.VISIBLE : View.GONE);
- if (onEmojiClickListener != null) {
- itemView.setOnClickListener(v -> onEmojiClickListener.onClick(v, emoji));
- }
- if (hasVariants && onEmojiLongClickListener != null) {
- itemView.setOnLongClickListener(v -> onEmojiLongClickListener.onLongClick(position, v, parent));
- }
- });
+ // itemView.post(() -> {
+ binding.image.setImageDrawable(emoji.getDrawable());
+ final boolean hasVariants = !parent.getVariants().isEmpty();
+ binding.indicator.setVisibility(hasVariants ? View.VISIBLE : View.GONE);
+ if (onEmojiClickListener != null) {
+ itemView.setOnClickListener(v -> onEmojiClickListener.onClick(v, emoji));
+ }
+ if (hasVariants && onEmojiLongClickListener != null) {
+ itemView.setOnLongClickListener(v -> onEmojiLongClickListener.onLongClick(position, v, parent));
+ }
+ // });
}
}
diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPopupWindow.java b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPopupWindow.java
deleted file mode 100644
index 76673590..00000000
--- a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPopupWindow.java
+++ /dev/null
@@ -1,163 +0,0 @@
-package awais.instagrabber.customviews.emoji;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.view.Gravity;
-import android.view.View;
-import android.view.WindowManager.LayoutParams;
-import android.widget.PopupWindow;
-
-import awais.instagrabber.R;
-import awais.instagrabber.customviews.emoji.EmojiPicker.OnBackspaceClickListener;
-import awais.instagrabber.customviews.emoji.EmojiPicker.OnEmojiClickListener;
-import awais.instagrabber.utils.Utils;
-
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-
-/**
- * https://stackoverflow.com/a/33897583/1436766
- */
-public class EmojiPopupWindow extends PopupWindow {
-
- private int keyBoardHeight = 0;
- private Boolean pendingOpen = false;
- private Boolean isOpened = false;
- private final View rootView;
- private final Context context;
- private final OnEmojiClickListener onEmojiClickListener;
- private final OnBackspaceClickListener onBackspaceClickListener;
-
- private OnSoftKeyboardOpenCloseListener onSoftKeyboardOpenCloseListener;
-
-
- /**
- * Constructor
- *
- * @param rootView The top most layout in your view hierarchy. The difference of this view and the screen height will be used to calculate the keyboard height.
- */
- public EmojiPopupWindow(final View rootView,
- final OnEmojiClickListener onEmojiClickListener,
- final OnBackspaceClickListener onBackspaceClickListener) {
- super(rootView.getContext());
- this.rootView = rootView;
- this.context = rootView.getContext();
- this.onEmojiClickListener = onEmojiClickListener;
- this.onBackspaceClickListener = onBackspaceClickListener;
- View customView = createCustomView();
- setContentView(customView);
- setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
- //default size
- setSize((int) context.getResources().getDimension(R.dimen.keyboard_height), MATCH_PARENT);
- }
-
- /**
- * Set the listener for the event of keyboard opening or closing.
- */
- public void setOnSoftKeyboardOpenCloseListener(OnSoftKeyboardOpenCloseListener listener) {
- this.onSoftKeyboardOpenCloseListener = listener;
- }
-
- /**
- * Use this function to show the emoji popup.
- * NOTE: Since, the soft keyboard sizes are variable on different android devices, the
- * library needs you to open the soft keyboard atleast once before calling this function.
- * If that is not possible see showAtBottomPending() function.
- */
- public void showAtBottom() {
- showAtLocation(rootView, Gravity.BOTTOM, 0, 0);
- }
-
- /**
- * Use this function when the soft keyboard has not been opened yet. This
- * will show the emoji popup after the keyboard is up next time.
- * Generally, you will be calling InputMethodManager.showSoftInput function after
- * calling this function.
- */
- public void showAtBottomPending() {
- if (isKeyBoardOpen())
- showAtBottom();
- else
- pendingOpen = true;
- }
-
- /**
- * @return Returns true if the soft keyboard is open, false otherwise.
- */
- public Boolean isKeyBoardOpen() {
- return isOpened;
- }
-
- /**
- * Dismiss the popup
- */
- @Override
- public void dismiss() {
- super.dismiss();
- }
-
- /**
- * Call this function to resize the emoji popup according to your soft keyboard size
- */
- public void setSizeForSoftKeyboard() {
- rootView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
- Rect r = new Rect();
- rootView.getWindowVisibleDisplayFrame(r);
-
- int screenHeight = getUsableScreenHeight();
- int heightDifference = screenHeight - (r.bottom - r.top);
- int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
- if (resourceId > 0) {
- heightDifference -= context.getResources()
- .getDimensionPixelSize(resourceId);
- }
- if (heightDifference > 100) {
- keyBoardHeight = heightDifference;
- setSize(MATCH_PARENT, keyBoardHeight);
- if (!isOpened) {
- if (onSoftKeyboardOpenCloseListener != null)
- onSoftKeyboardOpenCloseListener.onKeyboardOpen(keyBoardHeight);
- }
- isOpened = true;
- if (pendingOpen) {
- showAtBottom();
- pendingOpen = false;
- }
- } else {
- isOpened = false;
- if (onSoftKeyboardOpenCloseListener != null)
- onSoftKeyboardOpenCloseListener.onKeyboardClose();
- }
- });
- }
-
- private int getUsableScreenHeight() {
- return Utils.displayMetrics.heightPixels;
- }
-
- /**
- * Manually set the popup window size
- *
- * @param width Width of the popup
- * @param height Height of the popup
- */
- public void setSize(int width, int height) {
- setWidth(width);
- setHeight(height);
- }
-
- private View createCustomView() {
- final EmojiPicker emojiPicker = new EmojiPicker(context);
- final LayoutParams layoutParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT);
- emojiPicker.setLayoutParams(layoutParams);
- emojiPicker.init(rootView, onEmojiClickListener, onBackspaceClickListener);
- return emojiPicker;
- }
-
-
- public interface OnSoftKeyboardOpenCloseListener {
- void onKeyboardOpen(int keyBoardHeight);
-
- void onKeyboardClose();
- }
-}
-
diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/GoogleCompatEmojiDrawable.java b/app/src/main/java/awais/instagrabber/customviews/emoji/GoogleCompatEmojiDrawable.java
index f2bec738..ae1aff00 100644
--- a/app/src/main/java/awais/instagrabber/customviews/emoji/GoogleCompatEmojiDrawable.java
+++ b/app/src/main/java/awais/instagrabber/customviews/emoji/GoogleCompatEmojiDrawable.java
@@ -25,7 +25,6 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.Spanned;
import android.text.TextPaint;
-import android.util.Log;
import androidx.annotation.NonNull;
import androidx.emoji.text.EmojiCompat;
diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/ChangeText.java b/app/src/main/java/awais/instagrabber/customviews/helpers/ChangeText.java
new file mode 100644
index 00000000..bd3613ec
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/helpers/ChangeText.java
@@ -0,0 +1,320 @@
+package awais.instagrabber.customviews.helpers;
+
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.graphics.Color;
+import android.util.Log;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.transition.Transition;
+import androidx.transition.TransitionListenerAdapter;
+import androidx.transition.TransitionValues;
+
+import java.util.Map;
+import java.util.Objects;
+
+import awais.instagrabber.BuildConfig;
+
+/**
+ * This transition tracks changes to the text in TextView targets. If the text
+ * changes between the start and end scenes, the transition ensures that the
+ * starting text stays until the transition ends, at which point it changes
+ * to the end text. This is useful in situations where you want to resize a
+ * text view to its new size before displaying the text that goes there.
+ */
+public class ChangeText extends Transition {
+ private static final String LOG_TAG = "TextChange";
+ private static final String PROPNAME_TEXT = "android:textchange:text";
+ private static final String PROPNAME_TEXT_SELECTION_START =
+ "android:textchange:textSelectionStart";
+ private static final String PROPNAME_TEXT_SELECTION_END =
+ "android:textchange:textSelectionEnd";
+ private static final String PROPNAME_TEXT_COLOR = "android:textchange:textColor";
+ private int mChangeBehavior = CHANGE_BEHAVIOR_KEEP;
+ private boolean crossFade;
+ /**
+ * Flag specifying that the text in affected/changing TextView targets will keep
+ * their original text during the transition, setting it to the final text when
+ * the transition ends. This is the default behavior.
+ *
+ * @see #setChangeBehavior(int)
+ */
+ public static final int CHANGE_BEHAVIOR_KEEP = 0;
+ /**
+ * Flag specifying that the text changing animation should first fade
+ * out the original text completely. The new text is set on the target
+ * view at the end of the fade-out animation. This transition is typically
+ * used with a later {@link #CHANGE_BEHAVIOR_IN} transition, allowing more
+ * flexibility than the {@link #CHANGE_BEHAVIOR_OUT_IN} by allowing other
+ * transitions to be run sequentially or in parallel with these fades.
+ *
+ * @see #setChangeBehavior(int)
+ */
+ public static final int CHANGE_BEHAVIOR_OUT = 1;
+ /**
+ * Flag specifying that the text changing animation should fade in the
+ * end text into the affected target view(s). This transition is typically
+ * used in conjunction with an earlier {@link #CHANGE_BEHAVIOR_OUT}
+ * transition, possibly with other transitions running as well, such as
+ * a sequence to fade out, then resize the view, then fade in.
+ *
+ * @see #setChangeBehavior(int)
+ */
+ public static final int CHANGE_BEHAVIOR_IN = 2;
+ /**
+ * Flag specifying that the text changing animation should first fade
+ * out the original text completely and then fade in the
+ * new text.
+ *
+ * @see #setChangeBehavior(int)
+ */
+ public static final int CHANGE_BEHAVIOR_OUT_IN = 3;
+ private static final String[] sTransitionProperties = {
+ PROPNAME_TEXT,
+ PROPNAME_TEXT_SELECTION_START,
+ PROPNAME_TEXT_SELECTION_END
+ };
+
+ /**
+ * Sets the type of changing animation that will be run, one of
+ * {@link #CHANGE_BEHAVIOR_KEEP}, {@link #CHANGE_BEHAVIOR_OUT},
+ * {@link #CHANGE_BEHAVIOR_IN}, and {@link #CHANGE_BEHAVIOR_OUT_IN}.
+ *
+ * @param changeBehavior The type of fading animation to use when this
+ * transition is run.
+ * @return this textChange object.
+ */
+ public ChangeText setChangeBehavior(int changeBehavior) {
+ if (changeBehavior >= CHANGE_BEHAVIOR_KEEP && changeBehavior <= CHANGE_BEHAVIOR_OUT_IN) {
+ mChangeBehavior = changeBehavior;
+ }
+ return this;
+ }
+
+ public ChangeText setCrossFade(final boolean crossFade) {
+ this.crossFade = crossFade;
+ return this;
+ }
+
+ @Override
+ public String[] getTransitionProperties() {
+ return sTransitionProperties;
+ }
+
+ /**
+ * Returns the type of changing animation that will be run.
+ *
+ * @return either {@link #CHANGE_BEHAVIOR_KEEP}, {@link #CHANGE_BEHAVIOR_OUT},
+ * {@link #CHANGE_BEHAVIOR_IN}, or {@link #CHANGE_BEHAVIOR_OUT_IN}.
+ */
+ public int getChangeBehavior() {
+ return mChangeBehavior;
+ }
+
+ private void captureValues(TransitionValues transitionValues) {
+ if (transitionValues.view instanceof TextView) {
+ TextView textview = (TextView) transitionValues.view;
+ transitionValues.values.put(PROPNAME_TEXT, textview.getText());
+ if (textview instanceof EditText) {
+ transitionValues.values.put(PROPNAME_TEXT_SELECTION_START,
+ textview.getSelectionStart());
+ transitionValues.values.put(PROPNAME_TEXT_SELECTION_END,
+ textview.getSelectionEnd());
+ }
+ if (mChangeBehavior > CHANGE_BEHAVIOR_KEEP) {
+ transitionValues.values.put(PROPNAME_TEXT_COLOR, textview.getCurrentTextColor());
+ }
+ }
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ captureValues(transitionValues);
+ }
+
+ @Override
+ public void captureEndValues(@NonNull TransitionValues transitionValues) {
+ captureValues(transitionValues);
+ }
+
+ @Override
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot, TransitionValues startValues,
+ TransitionValues endValues) {
+ if (startValues == null || endValues == null ||
+ !(startValues.view instanceof TextView) || !(endValues.view instanceof TextView)) {
+ return null;
+ }
+ final TextView view = (TextView) endValues.view;
+ Map startVals = startValues.values;
+ Map endVals = endValues.values;
+ final CharSequence startText = startVals.get(PROPNAME_TEXT) != null ?
+ (CharSequence) startVals.get(PROPNAME_TEXT) : "";
+ final CharSequence endText = endVals.get(PROPNAME_TEXT) != null ?
+ (CharSequence) endVals.get(PROPNAME_TEXT) : "";
+ final int startSelectionStart, startSelectionEnd, endSelectionStart, endSelectionEnd;
+ if (view instanceof EditText) {
+ startSelectionStart = startVals.get(PROPNAME_TEXT_SELECTION_START) != null ?
+ (Integer) startVals.get(PROPNAME_TEXT_SELECTION_START) : -1;
+ startSelectionEnd = startVals.get(PROPNAME_TEXT_SELECTION_END) != null ?
+ (Integer) startVals.get(PROPNAME_TEXT_SELECTION_END) : startSelectionStart;
+ endSelectionStart = endVals.get(PROPNAME_TEXT_SELECTION_START) != null ?
+ (Integer) endVals.get(PROPNAME_TEXT_SELECTION_START) : -1;
+ endSelectionEnd = endVals.get(PROPNAME_TEXT_SELECTION_END) != null ?
+ (Integer) endVals.get(PROPNAME_TEXT_SELECTION_END) : endSelectionStart;
+ } else {
+ startSelectionStart = startSelectionEnd = endSelectionStart = endSelectionEnd = -1;
+ }
+ if (!Objects.equals(startText, endText)) {
+ final int startColor;
+ final int endColor;
+ if (mChangeBehavior != CHANGE_BEHAVIOR_IN) {
+ view.setText(startText);
+ if (view instanceof EditText) {
+ setSelection(((EditText) view), startSelectionStart, startSelectionEnd);
+ }
+ }
+ Animator anim;
+ if (mChangeBehavior == CHANGE_BEHAVIOR_KEEP) {
+ startColor = endColor = 0;
+ anim = ValueAnimator.ofFloat(0, 1);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (Objects.equals(startText, view.getText())) {
+ // Only set if it hasn't been changed since anim started
+ view.setText(endText);
+ if (view instanceof EditText) {
+ setSelection(((EditText) view), endSelectionStart, endSelectionEnd);
+ }
+ }
+ }
+ });
+ } else {
+ startColor = (Integer) startVals.get(PROPNAME_TEXT_COLOR);
+ endColor = (Integer) endVals.get(PROPNAME_TEXT_COLOR);
+ // Fade out start text
+ ValueAnimator outAnim = null, inAnim = null;
+ if (mChangeBehavior == CHANGE_BEHAVIOR_OUT_IN ||
+ mChangeBehavior == CHANGE_BEHAVIOR_OUT) {
+ outAnim = ValueAnimator.ofInt(Color.alpha(startColor), 0);
+ outAnim.addUpdateListener(animation -> {
+ int currAlpha = (Integer) animation.getAnimatedValue();
+ view.setTextColor(currAlpha << 24 | startColor & 0xffffff);
+ });
+ outAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (Objects.equals(startText, view.getText())) {
+ // Only set if it hasn't been changed since anim started
+ view.setText(endText);
+ if (view instanceof EditText) {
+ setSelection(((EditText) view), endSelectionStart,
+ endSelectionEnd);
+ }
+ }
+ // restore opaque alpha and correct end color
+ view.setTextColor(endColor);
+ }
+ });
+ }
+ if (mChangeBehavior == CHANGE_BEHAVIOR_OUT_IN ||
+ mChangeBehavior == CHANGE_BEHAVIOR_IN) {
+ inAnim = ValueAnimator.ofInt(0, Color.alpha(endColor));
+ inAnim.addUpdateListener(animation -> {
+ int currAlpha = (Integer) animation.getAnimatedValue();
+ view.setTextColor(currAlpha << 24 | endColor & 0xffffff);
+ });
+ inAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ // restore opaque alpha and correct end color
+ view.setTextColor(endColor);
+ }
+ });
+ }
+ if (outAnim != null && inAnim != null) {
+ anim = new AnimatorSet();
+ final AnimatorSet animatorSet = (AnimatorSet) anim;
+ if (crossFade) {
+ animatorSet.playTogether(outAnim, inAnim);
+ } else {
+ animatorSet.playSequentially(outAnim, inAnim);
+ }
+ } else if (outAnim != null) {
+ anim = outAnim;
+ } else {
+ // Must be an in-only animation
+ anim = inAnim;
+ }
+ }
+ TransitionListener transitionListener = new TransitionListenerAdapter() {
+ int mPausedColor = 0;
+
+ @Override
+ public void onTransitionPause(@NonNull Transition transition) {
+ if (mChangeBehavior != CHANGE_BEHAVIOR_IN) {
+ view.setText(endText);
+ if (view instanceof EditText) {
+ setSelection(((EditText) view), endSelectionStart, endSelectionEnd);
+ }
+ }
+ if (mChangeBehavior > CHANGE_BEHAVIOR_KEEP) {
+ mPausedColor = view.getCurrentTextColor();
+ view.setTextColor(endColor);
+ }
+ }
+
+ @Override
+ public void onTransitionResume(@NonNull Transition transition) {
+ if (mChangeBehavior != CHANGE_BEHAVIOR_IN) {
+ view.setText(startText);
+ if (view instanceof EditText) {
+ setSelection(((EditText) view), startSelectionStart, startSelectionEnd);
+ }
+ }
+ if (mChangeBehavior > CHANGE_BEHAVIOR_KEEP) {
+ view.setTextColor(mPausedColor);
+ }
+ }
+
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ transition.removeListener(this);
+ }
+ };
+ addListener(transitionListener);
+ if (BuildConfig.DEBUG) {
+ Log.d(LOG_TAG, "createAnimator returning " + anim);
+ }
+ return anim;
+ }
+ return null;
+ }
+
+ private void setSelection(EditText editText, int start, int end) {
+ if (start >= 0 && end >= 0) {
+ editText.setSelection(start, end);
+ }
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/ControlFocusInsetsAnimationCallback.java b/app/src/main/java/awais/instagrabber/customviews/helpers/ControlFocusInsetsAnimationCallback.java
new file mode 100644
index 00000000..e1fda461
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/helpers/ControlFocusInsetsAnimationCallback.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package awais.instagrabber.customviews.helpers;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsAnimationCompat;
+import androidx.core.view.WindowInsetsCompat;
+
+import java.util.List;
+
+/**
+ * A [WindowInsetsAnimationCompat.Callback] which will request and clear focus on the given view,
+ * depending on the [WindowInsetsCompat.Type.ime] visibility state when an IME
+ * [WindowInsetsAnimationCompat] has finished.
+ *
+ * This is primarily used when animating the [WindowInsetsCompat.Type.ime], so that the
+ * appropriate view is focused for accepting input from the IME.
+ */
+public class ControlFocusInsetsAnimationCallback extends WindowInsetsAnimationCompat.Callback {
+
+ private final View view;
+
+ public ControlFocusInsetsAnimationCallback(@NonNull final View view) {
+ this(view, DISPATCH_MODE_STOP);
+ }
+
+ /**
+ * @param view the view to request/clear focus
+ * @param dispatchMode The dispatch mode for this callback.
+ * @see WindowInsetsAnimationCompat.Callback.DispatchMode
+ */
+ public ControlFocusInsetsAnimationCallback(@NonNull final View view, final int dispatchMode) {
+ super(dispatchMode);
+ this.view = view;
+ }
+
+ @NonNull
+ @Override
+ public WindowInsetsCompat onProgress(@NonNull final WindowInsetsCompat insets,
+ @NonNull final List runningAnimations) {
+ // no-op and return the insets
+ return insets;
+ }
+
+ @Override
+ public void onEnd(final WindowInsetsAnimationCompat animation) {
+ if ((animation.getTypeMask() & WindowInsetsCompat.Type.ime()) != 0) {
+ // The animation has now finished, so we can check the view's focus state.
+ // We post the check because the rootWindowInsets has not yet been updated, but will
+ // be in the next message traversal
+ view.post(this::checkFocus);
+ }
+ }
+
+ private void checkFocus() {
+ final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(view);
+ boolean imeVisible = false;
+ if (rootWindowInsets != null) {
+ imeVisible = rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime());
+ }
+ if (imeVisible && view.getRootView().findFocus() == null) {
+ // If the IME will be visible, and there is not a currently focused view in
+ // the hierarchy, request focus on our view
+ view.requestFocus();
+ } else if (!imeVisible && view.isFocused()) {
+ // If the IME will not be visible and our view is currently focused, clear the focus
+ view.clearFocus();
+ }
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/CustomHideBottomViewOnScrollBehavior.java b/app/src/main/java/awais/instagrabber/customviews/helpers/CustomHideBottomViewOnScrollBehavior.java
index 765fc679..dabc622b 100644
--- a/app/src/main/java/awais/instagrabber/customviews/helpers/CustomHideBottomViewOnScrollBehavior.java
+++ b/app/src/main/java/awais/instagrabber/customviews/helpers/CustomHideBottomViewOnScrollBehavior.java
@@ -1,5 +1,7 @@
package awais.instagrabber.customviews.helpers;
+import android.content.Context;
+import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.NonNull;
@@ -12,6 +14,13 @@ import com.google.android.material.bottomnavigation.BottomNavigationView;
public class CustomHideBottomViewOnScrollBehavior extends HideBottomViewOnScrollBehavior {
private static final String TAG = "CustomHideBottomView";
+ public CustomHideBottomViewOnScrollBehavior() {
+ }
+
+ public CustomHideBottomViewOnScrollBehavior(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
@Override
public boolean onStartNestedScroll(@NonNull final CoordinatorLayout coordinatorLayout,
@NonNull final BottomNavigationView child,
@@ -23,7 +32,13 @@ public class CustomHideBottomViewOnScrollBehavior extends HideBottomViewOnScroll
}
@Override
- public void onNestedPreScroll(@NonNull final CoordinatorLayout coordinatorLayout, @NonNull final BottomNavigationView child, @NonNull final View target, final int dx, final int dy, @NonNull final int[] consumed, final int type) {
+ public void onNestedPreScroll(@NonNull final CoordinatorLayout coordinatorLayout,
+ @NonNull final BottomNavigationView child,
+ @NonNull final View target,
+ final int dx,
+ final int dy,
+ @NonNull final int[] consumed,
+ final int type) {
if (dy > 0) {
slideDown(child);
} else if (dy < 0) {
diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/EmojiPickerInsetsAnimationCallback.java b/app/src/main/java/awais/instagrabber/customviews/helpers/EmojiPickerInsetsAnimationCallback.java
new file mode 100644
index 00000000..125b65c1
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/helpers/EmojiPickerInsetsAnimationCallback.java
@@ -0,0 +1,117 @@
+package awais.instagrabber.customviews.helpers;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsAnimationCompat;
+import androidx.core.view.WindowInsetsCompat;
+
+import java.util.List;
+
+/**
+ * A customized {@link TranslateDeferringInsetsAnimationCallback} for the emoji picker
+ */
+public class EmojiPickerInsetsAnimationCallback extends WindowInsetsAnimationCompat.Callback {
+ private static final String TAG = EmojiPickerInsetsAnimationCallback.class.getSimpleName();
+
+ private final View view;
+ private final int persistentInsetTypes;
+ private final int deferredInsetTypes;
+
+ private int kbHeight;
+ private onKbVisibilityChangeListener listener;
+ private boolean shouldTranslate;
+
+ public EmojiPickerInsetsAnimationCallback(final View view,
+ final int persistentInsetTypes,
+ final int deferredInsetTypes) {
+ this(view, persistentInsetTypes, deferredInsetTypes, DISPATCH_MODE_STOP);
+ }
+
+ public EmojiPickerInsetsAnimationCallback(final View view,
+ final int persistentInsetTypes,
+ final int deferredInsetTypes,
+ final int dispatchMode) {
+ super(dispatchMode);
+ if ((persistentInsetTypes & deferredInsetTypes) != 0) {
+ throw new IllegalArgumentException("persistentInsetTypes and deferredInsetTypes can not contain " +
+ "any of same WindowInsetsCompat.Type values");
+ }
+ this.view = view;
+ this.persistentInsetTypes = persistentInsetTypes;
+ this.deferredInsetTypes = deferredInsetTypes;
+ }
+
+ @NonNull
+ @Override
+ public WindowInsetsCompat onProgress(@NonNull final WindowInsetsCompat insets,
+ @NonNull final List runningAnimations) {
+ // onProgress() is called when any of the running animations progress...
+
+ // First we get the insets which are potentially deferred
+ final Insets typesInset = insets.getInsets(deferredInsetTypes);
+ // Then we get the persistent inset types which are applied as padding during layout
+ final Insets otherInset = insets.getInsets(persistentInsetTypes);
+
+ // Now that we subtract the two insets, to calculate the difference. We also coerce
+ // the insets to be >= 0, to make sure we don't use negative insets.
+ final Insets subtract = Insets.subtract(typesInset, otherInset);
+ final Insets diff = Insets.max(subtract, Insets.NONE);
+
+ // The resulting `diff` insets contain the values for us to apply as a translation
+ // to the view
+ view.setTranslationX(diff.left - diff.right);
+ view.setTranslationY(shouldTranslate ? diff.top - diff.bottom : -kbHeight);
+
+ return insets;
+ }
+
+ @Override
+ public void onEnd(@NonNull final WindowInsetsAnimationCompat animation) {
+ try {
+ final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(view);
+ if (kbHeight == 0) {
+ if (rootWindowInsets == null) return;
+ final Insets imeInsets = rootWindowInsets.getInsets(WindowInsetsCompat.Type.ime());
+ final Insets navBarInsets = rootWindowInsets.getInsets(WindowInsetsCompat.Type.navigationBars());
+ kbHeight = imeInsets.bottom - navBarInsets.bottom;
+ final ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
+ if (layoutParams != null) {
+ layoutParams.height = kbHeight;
+ layoutParams.setMargins(layoutParams.leftMargin, layoutParams.topMargin, layoutParams.rightMargin, -kbHeight);
+ }
+ }
+ view.setTranslationX(0f);
+ final boolean visible = rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime());
+ float translationY = 0;
+ if (!shouldTranslate) {
+ translationY = -kbHeight;
+ if (visible) {
+ translationY = 0;
+ }
+ }
+ view.setTranslationY(translationY);
+
+ if (listener != null && rootWindowInsets != null) {
+ listener.onChange(visible);
+ }
+ } finally {
+ shouldTranslate = true;
+ }
+ }
+
+ public void setShouldTranslate(final boolean shouldTranslate) {
+ this.shouldTranslate = shouldTranslate;
+ }
+
+ public void setKbVisibilityListener(final onKbVisibilityChangeListener listener) {
+ this.listener = listener;
+ }
+
+ public interface onKbVisibilityChangeListener {
+ void onChange(boolean isVisible);
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/GridSpacingItemDecoration.java b/app/src/main/java/awais/instagrabber/customviews/helpers/GridSpacingItemDecoration.java
index 81b4be92..f34c30f5 100755
--- a/app/src/main/java/awais/instagrabber/customviews/helpers/GridSpacingItemDecoration.java
+++ b/app/src/main/java/awais/instagrabber/customviews/helpers/GridSpacingItemDecoration.java
@@ -7,17 +7,24 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
- private final int spacing;
+ private final int halfSpace;
+
+ private boolean hasHeader;
public GridSpacingItemDecoration(int spacing) {
- this.spacing = spacing;
+ halfSpace = spacing / 2;
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
- final int halfSpace = spacing / 2;
+ if (hasHeader && parent.getChildAdapterPosition(view) == 0) {
+ outRect.bottom = halfSpace;
+ outRect.left = -halfSpace;
+ outRect.right = -halfSpace;
+ return;
+ }
if (parent.getPaddingLeft() != halfSpace) {
- parent.setPadding(halfSpace, halfSpace, halfSpace, halfSpace);
+ parent.setPadding(halfSpace, hasHeader ? 0 : halfSpace, halfSpace, halfSpace);
parent.setClipToPadding(false);
}
outRect.top = halfSpace;
@@ -25,4 +32,8 @@ public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
outRect.left = halfSpace;
outRect.right = halfSpace;
}
+
+ public void setHasHeader(final boolean hasHeader) {
+ this.hasHeader = hasHeader;
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/RootViewDeferringInsetsCallback.java b/app/src/main/java/awais/instagrabber/customviews/helpers/RootViewDeferringInsetsCallback.java
new file mode 100644
index 00000000..f58be88a
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/helpers/RootViewDeferringInsetsCallback.java
@@ -0,0 +1,139 @@
+package awais.instagrabber.customviews.helpers;/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.graphics.Insets;
+import androidx.core.view.OnApplyWindowInsetsListener;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsAnimationCompat;
+import androidx.core.view.WindowInsetsCompat;
+
+import java.util.List;
+
+/**
+ * A class which extends/implements both [WindowInsetsAnimationCompat.Callback] and
+ * [View.OnApplyWindowInsetsListener], which should be set on the root view in your layout.
+ *
+ * This class enables the root view is selectively defer handling any insets which match
+ * [deferredInsetTypes], to enable better looking [WindowInsetsAnimationCompat]s.
+ *
+ * An example is the following: when a [WindowInsetsAnimationCompat] is started, the system will dispatch
+ * a [WindowInsetsCompat] instance which contains the end state of the animation. For the scenario of
+ * the IME being animated in, that means that the insets contains the IME height. If the view's
+ * [View.OnApplyWindowInsetsListener] simply always applied the combination of
+ * [WindowInsetsCompat.Type.ime] and [WindowInsetsCompat.Type.systemBars] using padding, the viewport of any
+ * child views would then be smaller. This results in us animating a smaller (padded-in) view into
+ * a larger viewport. Visually, this results in the views looking clipped.
+ *
+ * This class allows us to implement a different strategy for the above scenario, by selectively
+ * deferring the [WindowInsetsCompat.Type.ime] insets until the [WindowInsetsAnimationCompat] is ended.
+ * For the above example, you would create a [RootViewDeferringInsetsCallback] like so:
+ *
+ * This class is not limited to just IME animations, and can work with any [WindowInsetsCompat.Type]s.
+ */
+public class RootViewDeferringInsetsCallback extends WindowInsetsAnimationCompat.Callback implements OnApplyWindowInsetsListener {
+
+ private final int persistentInsetTypes;
+ private final int deferredInsetTypes;
+ @Nullable
+ private View view = null;
+ @Nullable
+ private WindowInsetsCompat lastWindowInsets = null;
+ private boolean deferredInsets = false;
+
+ /**
+ * @param persistentInsetTypes the bitmask of any inset types which should always be handled
+ * through padding the attached view
+ * @param deferredInsetTypes the bitmask of insets types which should be deferred until after
+ * any related [WindowInsetsAnimationCompat]s have ended
+ */
+ public RootViewDeferringInsetsCallback(final int persistentInsetTypes, final int deferredInsetTypes) {
+ super(DISPATCH_MODE_CONTINUE_ON_SUBTREE);
+ if ((persistentInsetTypes & deferredInsetTypes) != 0) {
+ throw new IllegalArgumentException("persistentInsetTypes and deferredInsetTypes can not contain " +
+ "any of same WindowInsetsCompat.Type values");
+ }
+ this.persistentInsetTypes = persistentInsetTypes;
+ this.deferredInsetTypes = deferredInsetTypes;
+ }
+
+ @Override
+ public WindowInsetsCompat onApplyWindowInsets(@NonNull final View v, @NonNull final WindowInsetsCompat windowInsets) {
+ // Store the view and insets for us in onEnd() below
+ view = v;
+ lastWindowInsets = windowInsets;
+
+ final int types = deferredInsets
+ // When the deferred flag is enabled, we only use the systemBars() insets
+ ? persistentInsetTypes
+ // Otherwise we handle the combination of the the systemBars() and ime() insets
+ : persistentInsetTypes | deferredInsetTypes;
+
+ // Finally we apply the resolved insets by setting them as padding
+ final Insets typeInsets = windowInsets.getInsets(types);
+ v.setPadding(typeInsets.left, typeInsets.top, typeInsets.right, typeInsets.bottom);
+
+ // We return the new WindowInsetsCompat.CONSUMED to stop the insets being dispatched any
+ // further into the view hierarchy. This replaces the deprecated
+ // WindowInsetsCompat.consumeSystemWindowInsets() and related functions.
+ return WindowInsetsCompat.CONSUMED;
+ }
+
+ @Override
+ public void onPrepare(WindowInsetsAnimationCompat animation) {
+ if ((animation.getTypeMask() & deferredInsetTypes) != 0) {
+ // We defer the WindowInsetsCompat.Type.ime() insets if the IME is currently not visible.
+ // This results in only the WindowInsetsCompat.Type.systemBars() being applied, allowing
+ // the scrolling view to remain at it's larger size.
+ deferredInsets = true;
+ }
+ }
+
+ @NonNull
+ @Override
+ public WindowInsetsCompat onProgress(@NonNull final WindowInsetsCompat insets,
+ @NonNull final List runningAnims) {
+ // This is a no-op. We don't actually want to handle any WindowInsetsAnimations
+ return insets;
+ }
+
+ @Override
+ public void onEnd(@NonNull final WindowInsetsAnimationCompat animation) {
+ if (deferredInsets && (animation.getTypeMask() & deferredInsetTypes) != 0) {
+ // If we deferred the IME insets and an IME animation has finished, we need to reset
+ // the flag
+ deferredInsets = false;
+
+ // And finally dispatch the deferred insets to the view now.
+ // Ideally we would just call view.requestApplyInsets() and let the normal dispatch
+ // cycle happen, but this happens too late resulting in a visual flicker.
+ // Instead we manually dispatch the most recent WindowInsets to the view.
+ if (lastWindowInsets != null && view != null) {
+ ViewCompat.dispatchApplyWindowInsets(view, lastWindowInsets);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/SimpleImeAnimationController.java b/app/src/main/java/awais/instagrabber/customviews/helpers/SimpleImeAnimationController.java
new file mode 100644
index 00000000..9bcc24d8
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/helpers/SimpleImeAnimationController.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package awais.instagrabber.customviews.helpers;
+
+import android.os.CancellationSignal;
+import android.util.Log;
+import android.view.View;
+import android.view.animation.LinearInterpolator;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsAnimationControlListenerCompat;
+import androidx.core.view.WindowInsetsAnimationControllerCompat;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.core.view.WindowInsetsControllerCompat;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import awais.instagrabber.utils.ViewUtils;
+
+/**
+ * A wrapper around the [WindowInsetsAnimationControllerCompat] APIs in AndroidX Core, to simplify
+ * the implementation of common use-cases around the IME.
+ *
+ * See [InsetsAnimationLinearLayout] and [InsetsAnimationTouchListener] for examples of how
+ * to use this class.
+ */
+public class SimpleImeAnimationController {
+ private static final String TAG = SimpleImeAnimationController.class.getSimpleName();
+ /**
+ * Scroll threshold for determining whether to animating to the end state, or to the start state.
+ * Currently 15% of the total swipe distance distance
+ */
+ private static final float SCROLL_THRESHOLD = 0.15f;
+
+ @Nullable
+ private WindowInsetsAnimationControllerCompat insetsAnimationController = null;
+ @Nullable
+ private CancellationSignal pendingRequestCancellationSignal = null;
+ @Nullable
+ private OnRequestReadyListener pendingRequestOnReadyListener;
+ /**
+ * True if the IME was shown at the start of the current animation.
+ */
+ private boolean isImeShownAtStart = false;
+ @Nullable
+ private SpringAnimation currentSpringAnimation = null;
+ private WindowInsetsAnimationControlListenerCompat fwdListener;
+
+ /**
+ * A LinearInterpolator instance we can re-use across listeners.
+ */
+ private final LinearInterpolator linearInterpolator = new LinearInterpolator();
+ /* To take control of the an WindowInsetsAnimation, we need to pass in a listener to
+ controlWindowInsetsAnimation() in startControlRequest(). The listener created here
+ keeps track of the current WindowInsetsAnimationController and resets our state. */
+ private final WindowInsetsAnimationControlListenerCompat animationControlListener = new WindowInsetsAnimationControlListenerCompat() {
+ /**
+ * Once the request is ready, call our [onRequestReady] function
+ */
+ @Override
+ public void onReady(@NonNull final WindowInsetsAnimationControllerCompat controller, final int types) {
+ onRequestReady(controller);
+ if (fwdListener != null) {
+ fwdListener.onReady(controller, types);
+ }
+ }
+
+ /**
+ * If the request is finished, we should reset our internal state
+ */
+ @Override
+ public void onFinished(@NonNull final WindowInsetsAnimationControllerCompat controller) {
+ reset();
+ if (fwdListener != null) {
+ fwdListener.onFinished(controller);
+ }
+ }
+
+ /**
+ * If the request is cancelled, we should reset our internal state
+ */
+ @Override
+ public void onCancelled(@Nullable final WindowInsetsAnimationControllerCompat controller) {
+ reset();
+ if (fwdListener != null) {
+ fwdListener.onCancelled(controller);
+ }
+ }
+ };
+
+ /**
+ * Start a control request to the [view]s [android.view.WindowInsetsController]. This should
+ * be called once the view is in a position to take control over the position of the IME.
+ *
+ * @param view The view which is triggering this request
+ * @param onRequestReadyListener optional listener which will be called when the request is ready and
+ * the animation can proceed
+ */
+ public void startControlRequest(@NonNull final View view,
+ @Nullable final OnRequestReadyListener onRequestReadyListener) {
+ if (isInsetAnimationInProgress()) {
+ Log.w(TAG, "startControlRequest: Animation in progress. Can not start a new request to controlWindowInsetsAnimation()");
+ return;
+ }
+
+ // Keep track of the IME insets, and the IME visibility, at the start of the request
+ final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(view);
+ if (rootWindowInsets != null) {
+ isImeShownAtStart = rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime());
+ }
+
+ // Create a cancellation signal, which we pass to controlWindowInsetsAnimation() below
+ pendingRequestCancellationSignal = new CancellationSignal();
+ // Keep reference to the onReady callback
+ pendingRequestOnReadyListener = onRequestReadyListener;
+
+ // Finally we make a controlWindowInsetsAnimation() request:
+ final WindowInsetsControllerCompat windowInsetsController = ViewCompat.getWindowInsetsController(view);
+ if (windowInsetsController != null) {
+ windowInsetsController.controlWindowInsetsAnimation(
+ // We're only catering for IME animations in this listener
+ WindowInsetsCompat.Type.ime(),
+ // Animation duration. This is not used by the system, and is only passed to any
+ // WindowInsetsAnimation.Callback set on views. We pass in -1 to indicate that we're
+ // not starting a finite animation, and that this is completely controlled by
+ // the user's touch.
+ -1,
+ // The time interpolator used in calculating the animation progress. The fraction value
+ // we passed into setInsetsAndAlpha() which be passed into this interpolator before
+ // being used by the system to inset the IME. LinearInterpolator is a good type
+ // to use for scrolling gestures.
+ linearInterpolator,
+ // A cancellation signal, which allows us to cancel the request to control
+ pendingRequestCancellationSignal,
+ // The WindowInsetsAnimationControlListener
+ animationControlListener
+ );
+ }
+ }
+
+ /**
+ * Start a control request to the [view]s [android.view.WindowInsetsController], similar to
+ * [startControlRequest], but immediately fling to a finish using [velocityY] once ready.
+ *
+ * This function is useful for fire-and-forget operations to animate the IME.
+ *
+ * @param view The view which is triggering this request
+ * @param velocityY the velocity of the touch gesture which caused this call
+ */
+ public void startAndFling(@NonNull final View view, final float velocityY) {
+ startControlRequest(view, null);
+ animateToFinish(velocityY);
+ }
+
+ /**
+ * Update the inset position of the IME by the given [dy] value. This value will be coerced
+ * into the hidden and shown inset values.
+ *
+ * This function should only be called if [isInsetAnimationInProgress] returns true.
+ *
+ * @return the amount of [dy] consumed by the inset animation, in pixels
+ */
+ public int insetBy(final int dy) {
+ if (insetsAnimationController == null) {
+ throw new IllegalStateException("Current WindowInsetsAnimationController is null." +
+ "This should only be called if isAnimationInProgress() returns true");
+ }
+ final WindowInsetsAnimationControllerCompat controller = insetsAnimationController;
+
+ // Call updateInsetTo() with the new inset value
+ return insetTo(controller.getCurrentInsets().bottom - dy);
+ }
+
+ /**
+ * Update the inset position of the IME to be the given [inset] value. This value will be
+ * coerced into the hidden and shown inset values.
+ *
+ * This function should only be called if [isInsetAnimationInProgress] returns true.
+ *
+ * @return the distance moved by the inset animation, in pixels
+ */
+ public int insetTo(final int inset) {
+ if (insetsAnimationController == null) {
+ throw new IllegalStateException("Current WindowInsetsAnimationController is null." +
+ "This should only be called if isAnimationInProgress() returns true");
+ }
+ final WindowInsetsAnimationControllerCompat controller = insetsAnimationController;
+
+ final int hiddenBottom = controller.getHiddenStateInsets().bottom;
+ final int shownBottom = controller.getShownStateInsets().bottom;
+ final int startBottom = isImeShownAtStart ? shownBottom : hiddenBottom;
+ final int endBottom = isImeShownAtStart ? hiddenBottom : shownBottom;
+
+ // We coerce the given inset within the limits of the hidden and shown insets
+ final int coercedBottom = coerceIn(inset, hiddenBottom, shownBottom);
+
+ final int consumedDy = controller.getCurrentInsets().bottom - coercedBottom;
+
+ // Finally update the insets in the WindowInsetsAnimationController using
+ // setInsetsAndAlpha().
+ controller.setInsetsAndAlpha(
+ // Here we update the animating insets. This is what controls where the IME is displayed.
+ // It is also passed through to views via their WindowInsetsAnimation.Callback.
+ Insets.of(0, 0, 0, coercedBottom),
+ // This controls the alpha value. We don't want to alter the alpha so use 1f
+ 1f,
+ // Finally we calculate the animation progress fraction. This value is passed through
+ // to any WindowInsetsAnimation.Callbacks, but it is not used by the system.
+ (coercedBottom - startBottom) / (float) (endBottom - startBottom)
+ );
+
+ return consumedDy;
+ }
+
+ /**
+ * Return `true` if an inset animation is in progress.
+ */
+ public boolean isInsetAnimationInProgress() {
+ return insetsAnimationController != null;
+ }
+
+ /**
+ * Return `true` if an inset animation is currently finishing.
+ */
+ public boolean isInsetAnimationFinishing() {
+ return currentSpringAnimation != null;
+ }
+
+ /**
+ * Return `true` if a request to control an inset animation is in progress.
+ */
+ public boolean isInsetAnimationRequestPending() {
+ return pendingRequestCancellationSignal != null;
+ }
+
+ /**
+ * Cancel the current [WindowInsetsAnimationControllerCompat]. We immediately finish
+ * the animation, reverting back to the state at the start of the gesture.
+ */
+ public void cancel() {
+ if (insetsAnimationController != null) {
+ insetsAnimationController.finish(isImeShownAtStart);
+ }
+ if (pendingRequestCancellationSignal != null) {
+ pendingRequestCancellationSignal.cancel();
+ }
+ if (currentSpringAnimation != null) {
+ // Cancel the current spring animation
+ currentSpringAnimation.cancel();
+ }
+ reset();
+ }
+
+ /**
+ * Finish the current [WindowInsetsAnimationControllerCompat] immediately.
+ */
+ public void finish() {
+ final WindowInsetsAnimationControllerCompat controller = insetsAnimationController;
+
+ if (controller == null) {
+ // If we don't currently have a controller, cancel any pending request and return
+ if (pendingRequestCancellationSignal != null) {
+ pendingRequestCancellationSignal.cancel();
+ }
+ return;
+ }
+
+ final int current = controller.getCurrentInsets().bottom;
+ final int shown = controller.getShownStateInsets().bottom;
+ final int hidden = controller.getHiddenStateInsets().bottom;
+
+ // The current inset matches either the shown/hidden inset, finish() immediately
+ if (current == shown) {
+ controller.finish(true);
+ } else if (current == hidden) {
+ controller.finish(false);
+ } else {
+ // Otherwise, we'll look at the current position...
+ if (controller.getCurrentFraction() >= SCROLL_THRESHOLD) {
+ // If the IME is past the 'threshold' we snap to the toggled state
+ controller.finish(!isImeShownAtStart);
+ } else {
+ // ...otherwise, we snap back to the original visibility
+ controller.finish(isImeShownAtStart);
+ }
+ }
+ }
+
+ /**
+ * Finish the current [WindowInsetsAnimationControllerCompat]. We finish the animation,
+ * animating to the end state if necessary.
+ *
+ * @param velocityY the velocity of the touch gesture which caused this call to [animateToFinish].
+ * Can be `null` if velocity is not available.
+ */
+ public void animateToFinish(@Nullable final Float velocityY) {
+ final WindowInsetsAnimationControllerCompat controller = insetsAnimationController;
+
+ if (controller == null) {
+ // If we don't currently have a controller, cancel any pending request and return
+ if (pendingRequestCancellationSignal != null) {
+ pendingRequestCancellationSignal.cancel();
+ }
+ return;
+ }
+
+ final int current = controller.getCurrentInsets().bottom;
+ final int shown = controller.getShownStateInsets().bottom;
+ final int hidden = controller.getHiddenStateInsets().bottom;
+
+ if (velocityY != null) {
+ // If we have a velocity, we can use it's direction to determine
+ // the visibility. Upwards == visible
+ animateImeToVisibility(velocityY > 0, velocityY);
+ } else if (current == shown) {
+ // The current inset matches either the shown/hidden inset, finish() immediately
+ controller.finish(true);
+ } else if (current == hidden) {
+ controller.finish(false);
+ } else {
+ // Otherwise, we'll look at the current position...
+ if (controller.getCurrentFraction() >= SCROLL_THRESHOLD) {
+ // If the IME is past the 'threshold' we animate it to the toggled state
+ animateImeToVisibility(!isImeShownAtStart, null);
+ } else {
+ // ...otherwise, we animate it back to the original visibility
+ animateImeToVisibility(isImeShownAtStart, null);
+ }
+ }
+ }
+
+ private void onRequestReady(@NonNull final WindowInsetsAnimationControllerCompat controller) {
+ // The request is ready, so clear out the pending cancellation signal
+ pendingRequestCancellationSignal = null;
+ // Store the current WindowInsetsAnimationController
+ insetsAnimationController = controller;
+
+ // Call any pending callback
+ if (pendingRequestOnReadyListener != null) {
+ pendingRequestOnReadyListener.onRequestReady(controller);
+ }
+ pendingRequestOnReadyListener = null;
+ }
+
+ /**
+ * Resets all of our internal state.
+ */
+ private void reset() {
+ // Clear all of our internal state
+ insetsAnimationController = null;
+ pendingRequestCancellationSignal = null;
+ isImeShownAtStart = false;
+ if (currentSpringAnimation != null) {
+ currentSpringAnimation.cancel();
+ }
+ currentSpringAnimation = null;
+ pendingRequestOnReadyListener = null;
+ }
+
+ /**
+ * Animate the IME to a given visibility.
+ *
+ * @param visible `true` to animate the IME to it's fully shown state, `false` to it's
+ * fully hidden state.
+ * @param velocityY the velocity of the touch gesture which caused this call. Can be `null`
+ * if velocity is not available.
+ */
+ private void animateImeToVisibility(final boolean visible, @Nullable final Float velocityY) {
+ if (insetsAnimationController == null) {
+ throw new IllegalStateException("Controller should not be null");
+ }
+ final WindowInsetsAnimationControllerCompat controller = insetsAnimationController;
+
+ final FloatPropertyCompat