diff --git a/.all-contributorsrc b/.all-contributorsrc index cd865d08..7a9958b9 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -79,6 +79,25 @@ "code" ] }, + { + "login": "stamatiap", + "name": "Stamatia Papageorgiou", + "avatar_url": "https://avatars.githubusercontent.com/u/57223967?v=4", + "profile": "https://github.com/stamatiap", + "contributions": [ + "code", + "translation" + ] + }, + { + "login": "The-EDev", + "name": "Farook Al-Sammarraie", + "avatar_url": "https://avatars.githubusercontent.com/u/60552923?v=4", + "profile": "https://github.com/The-EDev", + "contributions": [ + "code" + ] + }, { "login": "Zopieux", "name": "Alexandre Macabies", diff --git a/.github/workflows/github_nightly_release.yml b/.github/workflows/github_nightly_release.yml index 8edb6a63..bddf63b5 100644 --- a/.github/workflows/github_nightly_release.yml +++ b/.github/workflows/github_nightly_release.yml @@ -15,10 +15,11 @@ jobs: uses: actions/checkout@v2 - name: set up JDK 1.8 - uses: actions/setup-java@v1 + uses: actions/setup-java@v2 with: - java-version: 1.8 - + distribution: 'zulu' + java-version: '8' + - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/.github/workflows/github_pre_release.yml b/.github/workflows/github_pre_release.yml index 9700fdfc..42d8e481 100644 --- a/.github/workflows/github_pre_release.yml +++ b/.github/workflows/github_pre_release.yml @@ -16,9 +16,10 @@ jobs: uses: actions/checkout@v2 - name: set up JDK 1.8 - uses: actions/setup-java@v1 + uses: actions/setup-java@v2 with: - java-version: 1.8 + distribution: 'zulu' + java-version: '8' - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/README.md b/README.md index d1301419..15999d16 100755 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com) [![GPLv3 license](https://img.shields.io/badge/License-GPLv3-blue.svg)](./LICENSE) [![GitHub stars](https://img.shields.io/github/stars/austinhuang0131/instagrabber.svg?style=social&label=Star)](https://GitHub.com/austinhuang0131/barinsta/stargazers/) -[![All Contributors](https://img.shields.io/badge/all_contributors-42-orange.svg)](#contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-44-orange.svg)](#contributors) Instagram client; previously known as InstaGrabber. @@ -63,49 +63,53 @@ Prominent contributors are listed here in the [all-contributors](https://allcont
Pablo RodrΓ­guez

πŸ’» +
Stamatia Papageorgiou

πŸ’» 🌍 +
Farook Al-Sammarraie

πŸ’»
Alexandre Macabies

πŸ’»
Stefan Najdovski

🎨 🌍
CrazyMarvin

πŸ’΅ -
Kevin Thomas

πŸ’΅ -
Shadowspear123

πŸ“ πŸ› πŸ€” πŸ’¬ +
Kevin Thomas

πŸ’΅ +
Shadowspear123

πŸ“ πŸ› πŸ€” πŸ’¬
Ricardo

πŸ› 🌍
Akrai

πŸ€” 🌍
avtkal

🌍
CΓ©zar Augusto

🌍 -
Dimitris T

🌍 -
farzadx

🌍 +
Dimitris T

🌍 +
farzadx

🌍
Fatih AydΔ±n

🌍
fouze555

🌍
Galang23

🌍
Initdebugs

🌍 -
Jakub Janek

🌍 -
GenosseFlosse

🌍 +
Jakub Janek

🌍 +
GenosseFlosse

🌍
kernoeb

🌍
MoaufmKlo

🌍
nalinalini

🌍
peterge1998

🌍 -
PierreM0

🌍 -
Pyrobauve

🌍 +
PierreM0

🌍 +
Pyrobauve

🌍
RAMAR-RAR

🌍
rohang02

🌍
retiolus

🌍
rikishi0071

🌍 -
Alexey Peschany

🌍 -
Sitavi

🌍 +
Alexey Peschany

🌍 +
Sitavi

🌍
Still Hsu

🌍
Ten_Lego

🌍
wagnim

🌍
wokija

🌍 + +
ysakamoto

🌍
ZDVokoun

🌍 @@ -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 d8ee6ee6..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' @@ -165,8 +165,6 @@ dependencies { 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" implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation "androidx.viewpager2:viewpager2:1.0.0" @@ -180,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" 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 9b052c8c..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(); 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 { if (notificationClickListener == null) return; - notificationClickListener.onFeedStoryClick(model, position); + notificationClickListener.onFeedStoryClick(model); }); } 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/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/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/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: + *

+ * ``` + * val callback = RootViewDeferringInsetsCallback( + * persistentInsetTypes = WindowInsetsCompat.Type.systemBars(), + * deferredInsetTypes = WindowInsetsCompat.Type.ime() + * ) + * ``` + *

+ * 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 property = new FloatPropertyCompat("property") { + @Override + public float getValue(final Object object) { + return controller.getCurrentInsets().bottom; + } + + @Override + public void setValue(final Object object, final float value) { + if (insetsAnimationController == null) { + return; + } + insetTo((int) value); + } + }; + final float finalPosition = visible ? controller.getShownStateInsets().bottom + : controller.getHiddenStateInsets().bottom; + final SpringForce force = new SpringForce(finalPosition) + // Tweak the damping value, to remove any bounciness. + .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY) + // The stiffness value controls the strength of the spring animation, which + // controls the speed. Medium (the default) is a good value, but feel free to + // play around with this value. + .setStiffness(SpringForce.STIFFNESS_MEDIUM); + ViewUtils.springAnimationOf(this, property, finalPosition) + .setSpring(force) + .setStartVelocity(velocityY != null ? velocityY : 0) + .addEndListener((animation, canceled, value, velocity) -> { + if (animation == currentSpringAnimation) { + currentSpringAnimation = null; + } + // Once the animation has ended, finish the controller + finish(); + }).start(); + } + + private int coerceIn(final int v, final int min, final int max) { + if (v >= min && v <= max) { + return v; + } + if (v < min) { + return min; + } + return max; + } + + public void setAnimationControlListener(final WindowInsetsAnimationControlListenerCompat listener) { + fwdListener = listener; + } + + public interface OnRequestReadyListener { + void onRequestReady(WindowInsetsAnimationControllerCompat windowInsetsAnimationControllerCompat); + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/TranslateDeferringInsetsAnimationCallback.java b/app/src/main/java/awais/instagrabber/customviews/helpers/TranslateDeferringInsetsAnimationCallback.java new file mode 100644 index 00000000..e10105ea --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/helpers/TranslateDeferringInsetsAnimationCallback.java @@ -0,0 +1,128 @@ +/* + * 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.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsAnimationCompat; +import androidx.core.view.WindowInsetsCompat; + +import java.util.List; + +/** + * A [WindowInsetsAnimationCompat.Callback] which will translate/move the given view during any + * inset animations of the given inset type. + *

+ * This class works in tandem with [RootViewDeferringInsetsCallback] to support the deferring of + * certain [WindowInsetsCompat.Type] values during a [WindowInsetsAnimationCompat], provided in + * [deferredInsetTypes]. The values passed into this constructor should match those which + * the [RootViewDeferringInsetsCallback] is created with. + */ +public class TranslateDeferringInsetsAnimationCallback extends WindowInsetsAnimationCompat.Callback { + private final View view; + private final int persistentInsetTypes; + private final int deferredInsetTypes; + + private boolean shouldTranslate = true; + private int kbHeight; + + public TranslateDeferringInsetsAnimationCallback(final View view, + final int persistentInsetTypes, + final int deferredInsetTypes) { + this(view, persistentInsetTypes, deferredInsetTypes, DISPATCH_MODE_STOP); + } + + /** + * @param view the view to translate from it's start to end state + * @param persistentInsetTypes the bitmask of any inset types which were handled as part of the + * layout + * @param deferredInsetTypes the bitmask of insets types which should be deferred until after + * any [WindowInsetsAnimationCompat]s have ended + * @param dispatchMode The dispatch mode for this callback. + * See [WindowInsetsAnimationCompat.Callback.getDispatchMode]. + */ + public TranslateDeferringInsetsAnimationCallback(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; + } + // Once the animation has ended, reset the translation values + 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); + } finally { + shouldTranslate = true; + } + } + + public void setShouldTranslate(final boolean shouldTranslate) { + this.shouldTranslate = shouldTranslate; + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java b/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java index 975357e8..a7804de0 100644 --- a/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java @@ -2,6 +2,7 @@ package awais.instagrabber.fragments; import android.content.Context; import android.os.Bundle; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -102,8 +103,13 @@ public class FavoritesFragment extends Fragment { // Log.d(TAG, "locationId: " + locationId); final NavController navController = NavHostFragment.findNavController(this); final Bundle bundle = new Bundle(); - bundle.putString("locationId", locationId); - navController.navigate(R.id.action_global_locationFragment, bundle); + try { + bundle.putLong("locationId", Long.parseLong(locationId)); + navController.navigate(R.id.action_global_locationFragment, bundle); + } catch (Exception e) { + Log.e(TAG, "init: ", e); + return; + } break; } case HASHTAG: { diff --git a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java index 65315e35..35249824 100644 --- a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java @@ -18,6 +18,7 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.Window; import android.widget.Toast; import androidx.annotation.NonNull; @@ -29,6 +30,7 @@ import androidx.appcompat.widget.PopupMenu; import androidx.appcompat.widget.Toolbar; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.PermissionChecker; +import androidx.core.view.WindowCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.LiveData; @@ -100,7 +102,6 @@ import static androidx.core.content.PermissionChecker.checkSelfPermission; import static awais.instagrabber.fragments.HashTagFragment.ARG_HASHTAG; import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_SHOWN_COUNT_TOOLTIP; import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; -import static awais.instagrabber.utils.Utils.getAttrValue; import static awais.instagrabber.utils.Utils.settingsHelper; public class PostViewV2Fragment extends Fragment implements EditTextDialogFragment.EditTextDialogFragmentCallback { @@ -131,6 +132,9 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme private boolean isInFullScreenMode; private StyledPlayerView playerView; private int playerViewOriginalHeight; + private Drawable originalRootBackground; + private ColorStateList originalLikeColorStateList; + private ColorStateList originalSaveColorStateList; private final Observer backStackSavedStateObserver = result -> { if (result == null) return; @@ -141,7 +145,6 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme // clear result backStackSavedStateResultLiveData.postValue(null); }; - private Drawable originalRootBackground; public void setOnDeleteListener(final OnDeleteListener onDeleteListener) { if (onDeleteListener == null) return; @@ -441,6 +444,7 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme } private void setupLike() { + originalLikeColorStateList = bottom.like.getIconTint(); final boolean likableMedia = viewModel.hasPk() /*&& viewModel.getMedia().isCommentLikesEnabled()*/; if (!likableMedia) { bottom.like.setVisibility(View.GONE); @@ -503,25 +507,25 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme private void setLikedResources(final boolean liked) { final int iconResource; - final int tintResource; + final ColorStateList tintColorStateList; final Context context = getContext(); if (context == null) return; final Resources resources = context.getResources(); if (resources == null) return; if (liked) { iconResource = R.drawable.ic_like; - tintResource = resources.getColor(R.color.red_600); - // textResId = R.string.unlike_without_count; + tintColorStateList = ColorStateList.valueOf(resources.getColor(R.color.red_600)); } else { iconResource = R.drawable.ic_not_liked; - tintResource = getAttrValue(context, R.attr.colorPrimary); - // textResId = R.string.like_without_count; + tintColorStateList = originalLikeColorStateList != null ? originalLikeColorStateList + : ColorStateList.valueOf(resources.getColor(R.color.white)); } bottom.like.setIconResource(iconResource); - bottom.like.setIconTint(ColorStateList.valueOf(tintResource)); + bottom.like.setIconTint(tintColorStateList); } private void setupSave() { + originalSaveColorStateList = bottom.save.getIconTint(); if (!viewModel.isLoggedIn() || !viewModel.hasPk() || !viewModel.getMedia().canViewerSave()) { bottom.save.setVisibility(View.GONE); return; @@ -574,22 +578,21 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme private void setSavedResources(final boolean saved) { final int iconResource; - final int tintResource; + final ColorStateList tintColorStateList; final Context context = getContext(); if (context == null) return; final Resources resources = context.getResources(); if (resources == null) return; if (saved) { iconResource = R.drawable.ic_bookmark; - tintResource = resources.getColor(R.color.blue_700); - // textResId = R.string.saved; + tintColorStateList = ColorStateList.valueOf(resources.getColor(R.color.blue_700)); } else { iconResource = R.drawable.ic_round_bookmark_border_24; - tintResource = getAttrValue(context, R.attr.colorPrimary); - // textResId = R.string.save; + tintColorStateList = originalSaveColorStateList != null ? originalSaveColorStateList + : ColorStateList.valueOf(resources.getColor(R.color.white)); } bottom.save.setIconResource(iconResource); - bottom.save.setIconTint(ColorStateList.valueOf(tintResource)); + bottom.save.setIconTint(tintColorStateList); } private void setupProfilePic(final User user) { @@ -1427,8 +1430,10 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme if (toolbar != null) { toolbar.setVisibility(View.VISIBLE); } - final View decorView = activity.getWindow().getDecorView(); + final Window window = activity.getWindow(); + final View decorView = window.getDecorView(); decorView.setSystemUiVisibility(originalSystemUi); + WindowCompat.setDecorFitsSystemWindows(window, false); isInFullScreenMode = false; } diff --git a/app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java index f4bf129a..f8bdfb81 100644 --- a/app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java @@ -23,9 +23,12 @@ import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import com.google.common.collect.Iterables; + import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import awais.instagrabber.R; import awais.instagrabber.adapters.FeedStoriesListAdapter; @@ -58,15 +61,17 @@ public final class StoryListViewerFragment extends Fragment implements SwipeRefr private StoriesService storiesService; private Context context; private String type; - private String currentQuery; private String endCursor = null; private FeedStoriesListAdapter adapter; - private MenuItem menuSearch; private final OnFeedStoryClickListener clickListener = new OnFeedStoryClickListener() { @Override - public void onFeedStoryClick(final FeedStoryModel model, final int position) { + public void onFeedStoryClick(final FeedStoryModel model) { if (model == null) return; + final List feedStoryModels = feedStoriesViewModel.getList().getValue(); + if (feedStoryModels == null) return; + final int position = Iterables.indexOf(feedStoryModels, feedStoryModel -> feedStoryModel != null + && Objects.equals(feedStoryModel.getStoryMediaId(), model.getStoryMediaId())); final NavDirections action = StoryListViewerFragmentDirections .actionStoryListFragmentToStoryViewerFragment(StoryViewerOptions.forFeedStoryPosition(position)); NavHostFragment.findNavController(StoryListViewerFragment.this).navigate(action); @@ -153,7 +158,7 @@ public final class StoryListViewerFragment extends Fragment implements SwipeRefr @Override public void onCreateOptionsMenu(@NonNull final Menu menu, final MenuInflater inflater) { inflater.inflate(R.menu.search, menu); - menuSearch = menu.findItem(R.id.action_search); + final MenuItem menuSearch = menu.findItem(R.id.action_search); final SearchView searchView = (SearchView) menuSearch.getActionView(); searchView.setQueryHint(getResources().getString(R.string.action_search)); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @@ -166,7 +171,6 @@ public final class StoryListViewerFragment extends Fragment implements SwipeRefr @Override public boolean onQueryTextChange(final String query) { if (adapter != null) { - currentQuery = query; adapter.getFilter().filter(query); } return true; diff --git a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java index 96ca53e5..eb1aa41f 100644 --- a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java @@ -120,6 +120,7 @@ public class StoryViewerFragment extends Fragment { private View root; private FragmentStoryViewerBinding binding; private String currentStoryUsername; + private String highlightTitle; private StoriesAdapter storiesAdapter; private SwipeEvent swipeEvent; private GestureDetectorCompat gestureDetector; @@ -275,7 +276,9 @@ public class StoryViewerFragment extends Fragment { @Override public void onPause() { super.onPause(); - releasePlayer(); + if (player != null) { + player.pause(); + } } @Override @@ -749,7 +752,7 @@ public class StoryViewerFragment extends Fragment { final HighlightModel model = models.get(currentFeedStoryIndex); currentStoryMediaId = model.getId(); fetchOptions = StoryViewerOptions.forHighlight(model.getId()); - currentStoryUsername = model.getTitle(); + highlightTitle = model.getTitle(); break; } case FEED_STORY_POSITION: { @@ -849,8 +852,8 @@ public class StoryViewerFragment extends Fragment { if (type == Type.HIGHLIGHT) { final ActionBar actionBar = fragmentActivity.getSupportActionBar(); if (actionBar != null) { - actionBarTitle = options.getName(); - actionBar.setTitle(options.getName()); + actionBarTitle = highlightTitle; + actionBar.setTitle(highlightTitle); } } else if (hasUsername) { currentStoryUsername = currentStoryUsername.replace("@", ""); diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.java index 60a7482e..d8a98087 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.java @@ -16,6 +16,7 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.view.menu.ActionMenuItemView; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.fragment.app.Fragment; import androidx.lifecycle.Observer; @@ -27,6 +28,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.google.android.material.badge.BadgeDrawable; import com.google.android.material.badge.BadgeUtils; +import com.google.android.material.internal.ToolbarUtils; import com.google.android.material.snackbar.Snackbar; import java.util.List; @@ -102,7 +104,9 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh super.onPause(); unregisterReceiver(); isPendingRequestTotalBadgeAttached = false; - if (pendingRequestTotalBadgeDrawable != null) { + @SuppressLint("RestrictedApi") final ActionMenuItemView menuItemView = ToolbarUtils + .getActionMenuItemView(fragmentActivity.getToolbar(), pendingRequestsMenuItem.getItemId()); + if (pendingRequestTotalBadgeDrawable != null && menuItemView != null) { BadgeUtils.detachBadgeDrawable(pendingRequestTotalBadgeDrawable, fragmentActivity.getToolbar(), pendingRequestsMenuItem.getItemId()); pendingRequestTotalBadgeDrawable = null; } @@ -217,7 +221,11 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh pendingRequestTotalBadgeDrawable = BadgeDrawable.create(context); } if (count == null || count == 0) { - BadgeUtils.detachBadgeDrawable(pendingRequestTotalBadgeDrawable, fragmentActivity.getToolbar(), pendingRequestsMenuItem.getItemId()); + @SuppressLint("RestrictedApi") final ActionMenuItemView menuItemView = ToolbarUtils + .getActionMenuItemView(fragmentActivity.getToolbar(), pendingRequestsMenuItem.getItemId()); + if (menuItemView != null) { + BadgeUtils.detachBadgeDrawable(pendingRequestTotalBadgeDrawable, fragmentActivity.getToolbar(), pendingRequestsMenuItem.getItemId()); + } isPendingRequestTotalBadgeAttached = false; pendingRequestTotalBadgeDrawable.setNumber(0); pendingRequestsMenuItem.setVisible(false); diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java index 1d6b8f6e..eb97b60f 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java @@ -15,6 +15,7 @@ import android.net.Uri; import android.os.Bundle; import android.text.Editable; import android.util.Log; +import android.util.Pair; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; @@ -25,11 +26,18 @@ import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.Toast; +import androidx.activity.OnBackPressedCallback; +import androidx.activity.OnBackPressedDispatcher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; -import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.appcompat.view.menu.ActionMenuItemView; import androidx.core.content.ContextCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsAnimationCompat; +import androidx.core.view.WindowInsetsAnimationControlListenerCompat; +import androidx.core.view.WindowInsetsAnimationControllerCompat; +import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -49,6 +57,7 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; import com.google.android.material.badge.BadgeDrawable; import com.google.android.material.badge.BadgeUtils; +import com.google.android.material.internal.ToolbarUtils; import com.google.android.material.snackbar.Snackbar; import com.google.common.collect.ImmutableList; @@ -70,16 +79,21 @@ import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemOrHeader; import awais.instagrabber.adapters.DirectReactionsAdapter; import awais.instagrabber.adapters.viewholder.directmessages.DirectItemViewHolder; import awais.instagrabber.animations.CubicBezierInterpolator; +import awais.instagrabber.customviews.InsetsAnimationLinearLayout; +import awais.instagrabber.customviews.KeyNotifyingEmojiEditText; import awais.instagrabber.customviews.RecordView; import awais.instagrabber.customviews.Tooltip; import awais.instagrabber.customviews.emoji.Emoji; import awais.instagrabber.customviews.emoji.EmojiBottomSheetDialog; import awais.instagrabber.customviews.emoji.EmojiPicker; +import awais.instagrabber.customviews.helpers.ControlFocusInsetsAnimationCallback; +import awais.instagrabber.customviews.helpers.EmojiPickerInsetsAnimationCallback; import awais.instagrabber.customviews.helpers.HeaderItemDecoration; -import awais.instagrabber.customviews.helpers.HeightProvider; import awais.instagrabber.customviews.helpers.RecyclerLazyLoaderAtEdge; +import awais.instagrabber.customviews.helpers.SimpleImeAnimationController; import awais.instagrabber.customviews.helpers.SwipeAndRestoreItemTouchHelperCallback; import awais.instagrabber.customviews.helpers.TextWatcherAdapter; +import awais.instagrabber.customviews.helpers.TranslateDeferringInsetsAnimationCallback; import awais.instagrabber.databinding.FragmentDirectMessagesThreadBinding; import awais.instagrabber.dialogs.DirectItemReactionDialogFragment; import awais.instagrabber.dialogs.GifPickerBottomDialogFragment; @@ -111,9 +125,6 @@ import awais.instagrabber.viewmodels.AppStateViewModel; import awais.instagrabber.viewmodels.DirectThreadViewModel; import awais.instagrabber.viewmodels.factories.DirectThreadViewModelFactory; -import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING; -import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN; - public class DirectMessageThreadFragment extends Fragment implements DirectReactionsAdapter.OnReactionClickListener, EmojiPicker.OnEmojiClickListener { private static final String TAG = DirectMessageThreadFragment.class.getSimpleName(); @@ -125,7 +136,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact private DirectItemsAdapter itemsAdapter; private MainActivity fragmentActivity; private DirectThreadViewModel viewModel; - private ConstraintLayout root; + private InsetsAnimationLinearLayout root; private boolean shouldRefresh = true; private List itemOrHeaders; private List users; @@ -135,14 +146,8 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact private ActionBar actionBar; private AppStateViewModel appStateViewModel; private Runnable prevTitleRunnable; - private int originalSoftInputMode; private AnimatorSet animatorSet; - private boolean isEmojiPickerShown; - private boolean isKbShown; - private HeightProvider heightProvider; private boolean isRecording; - private boolean wasKbShowing; - private int keyboardHeight = Utils.convertDpToPx(250); private DirectItemReactionDialogFragment reactionDialogFragment; private DirectItem itemToForward; private MutableLiveData backStackSavedStateResultLiveData; @@ -163,6 +168,11 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact private MenuItem markAsSeenMenuItem; private Media tempMedia; private DirectItem addReactionItem; + private TranslateDeferringInsetsAnimationCallback inputHolderAnimationCallback; + private TranslateDeferringInsetsAnimationCallback chatsAnimationCallback; + private EmojiPickerInsetsAnimationCallback emojiPickerAnimationCallback; + private boolean hasKbOpenedOnce; + private boolean wasToggled; private final AppExecutors appExecutors = AppExecutors.getInstance(); private final Animatable2Compat.AnimationCallback micToSendAnimationCallback = new Animatable2Compat.AnimationCallback() { @@ -304,7 +314,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact emojiBottomSheetDialog.show(getChildFragmentManager(), EmojiBottomSheetDialog.TAG); } }; - private final DirectItemLongClickListener directItemLongClickListener = position -> { // viewModel.setSelectedPosition(position); }; @@ -333,6 +342,14 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact backStackSavedStateResultLiveData.postValue(null); }; private final MutableLiveData inputLength = new MutableLiveData<>(0); + private final MutableLiveData emojiPickerVisible = new MutableLiveData<>(false); + private final MutableLiveData kbVisible = new MutableLiveData<>(false); + private final OnBackPressedCallback onEmojiPickerBackPressedCallback = new OnBackPressedCallback(false) { + @Override + public void handleOnBackPressed() { + emojiPickerVisible.postValue(false); + } + }; @Override public void onCreate(@Nullable final Bundle savedInstanceState) { @@ -371,13 +388,13 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact return root; } tooltip = new Tooltip(context, root, getResources().getColor(R.color.grey_400), getResources().getColor(R.color.black)); - originalSoftInputMode = fragmentActivity.getWindow().getAttributes().softInputMode; // todo check has camera and remove view return root; } @Override public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { + // WindowCompat.setDecorFitsSystemWindows(fragmentActivity.getWindow(), false); if (!shouldRefresh) return; init(); binding.send.post(() -> initialSendX = binding.send.getX()); @@ -490,10 +507,11 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact if (isRecording) { binding.recordView.cancelRecording(binding.send); } - if (isKbShown) { - wasKbShowing = true; - binding.emojiPicker.setAlpha(0); - } + emojiPickerVisible.postValue(false); + kbVisible.postValue(false); + binding.inputHolder.setTranslationY(0); + binding.chats.setTranslationY(0); + binding.emojiPicker.setTranslationY(0); removeObservers(); super.onPause(); } @@ -501,16 +519,12 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact @Override public void onResume() { super.onResume(); - fragmentActivity.getWindow().setSoftInputMode(SOFT_INPUT_ADJUST_NOTHING | SOFT_INPUT_STATE_HIDDEN); - if (wasKbShowing) { - binding.input.requestFocus(); - binding.input.post(this::showKeyboard); - wasKbShowing = false; - } if (initialSendX != 0) { binding.send.setX(initialSendX); } binding.send.stopScale(); + final OnBackPressedDispatcher onBackPressedDispatcher = fragmentActivity.getOnBackPressedDispatcher(); + onBackPressedDispatcher.addCallback(onEmojiPickerBackPressedCallback); setupBackStackResultObserver(); setObservers(); // attachPendingRequestsBadge(viewModel.getPendingRequestsCount().getValue()); @@ -533,13 +547,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact if (prevTitleRunnable != null) { appExecutors.mainThread().cancel(prevTitleRunnable); } - if (heightProvider != null) { - // need to close the height provider popup before navigating back to prevent leak - heightProvider.dismiss(); - } - if (originalSoftInputMode != 0) { - fragmentActivity.getWindow().setSoftInputMode(originalSoftInputMode); - } for (int childCount = binding.chats.getChildCount(), i = 0; i < childCount; ++i) { final RecyclerView.ViewHolder holder = binding.chats.getChildViewHolder(binding.chats.getChildAt(i)); if (holder == null) continue; @@ -549,7 +556,11 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact } isPendingRequestCountBadgeAttached = false; if (pendingRequestCountBadgeDrawable != null) { - BadgeUtils.detachBadgeDrawable(pendingRequestCountBadgeDrawable, fragmentActivity.getToolbar(), R.id.info); + @SuppressLint("RestrictedApi") final ActionMenuItemView menuItemView = ToolbarUtils + .getActionMenuItemView(fragmentActivity.getToolbar(), R.id.info); + if (menuItemView != null) { + BadgeUtils.detachBadgeDrawable(pendingRequestCountBadgeDrawable, fragmentActivity.getToolbar(), R.id.info); + } pendingRequestCountBadgeDrawable = null; } } @@ -561,37 +572,8 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact actionBar = fragmentActivity.getSupportActionBar(); setupList(); root.post(this::setupInput); - // root.post(this::getInitialData); } - // private void getInitialData() { - // final Bundle arguments = getArguments(); - // if (arguments == null) return; - // final DirectMessageThreadFragmentArgs args = DirectMessageThreadFragmentArgs.fromBundle(arguments); - // final boolean pending = args.getPending(); - // final NavController navController = NavHostFragment.findNavController(this); - // final ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph); - // final List threads; - // if (!pending) { - // final DirectInboxViewModel threadListViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectInboxViewModel.class); - // threads = threadListViewModel.getThreads().getValue(); - // } else { - // final DirectPendingInboxViewModel threadListViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectPendingInboxViewModel.class); - // threads = threadListViewModel.getThreads().getValue(); - // } - // final Optional first = threads != null - // ? threads.stream() - // .filter(thread -> thread.getThreadId().equals(viewModel.getThreadId())) - // .findFirst() - // : Optional.empty(); - // if (first.isPresent()) { - // final DirectThread thread = first.get(); - // viewModel.setThread(thread); - // return; - // } - // viewModel.fetchChats(); - // } - private void setupList() { final Context context = getContext(); if (context == null) return; @@ -862,7 +844,11 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact pendingRequestCountBadgeDrawable = BadgeDrawable.create(context); } if (count == null || count == 0) { - BadgeUtils.detachBadgeDrawable(pendingRequestCountBadgeDrawable, fragmentActivity.getToolbar(), R.id.info); + @SuppressLint("RestrictedApi") final ActionMenuItemView menuItemView = ToolbarUtils + .getActionMenuItemView(fragmentActivity.getToolbar(), R.id.info); + if (menuItemView != null) { + BadgeUtils.detachBadgeDrawable(pendingRequestCountBadgeDrawable, fragmentActivity.getToolbar(), R.id.info); + } isPendingRequestCountBadgeAttached = false; pendingRequestCountBadgeDrawable.setNumber(0); return; @@ -1100,23 +1086,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { final int length = s.length(); inputLength.postValue(length); - // boolean showExtraInputOptionsChanged = false; - // if (prevLength != 0 && length == 0) { - // inputLength.postValue(true); - // showExtraInputOptionsChanged = true; - // binding.send.setListenForRecord(true); - // startIconAnimation(); - // } - // if (prevLength == 0 && length != 0) { - // inputLength.postValue(false); - // showExtraInputOptionsChanged = true; - // binding.send.setListenForRecord(false); - // startIconAnimation(); - // } - // if (!showExtraInputOptionsChanged) { - // showExtraInputOptions.postValue(length == 0); - // } - // prevLength = length; } }); binding.send.setOnRecordClickListener(v -> { @@ -1131,30 +1100,15 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact Log.d(TAG, "setOnRecordLongClickListener"); return true; }); - binding.input.setShowSoftInputOnFocus(false); - binding.input.requestFocus(); - binding.input.setOnKeyEventListener((keyCode, keyEvent) -> { - if (keyCode != KeyEvent.KEYCODE_BACK) return false; - // We'll close the keyboard/emoji picker only when user releases the back button - // return true so that system doesn't handle the event - if (keyEvent.getAction() != KeyEvent.ACTION_UP) return true; - if (!isKbShown && !isEmojiPickerShown) { - // if both keyboard and emoji picker are hidden, navigate back - if (heightProvider != null) { - // need to close the height provider popup before navigating back to prevent leak - heightProvider.dismiss(); - } - NavHostFragment.findNavController(this).navigateUp(); - return true; - } - binding.emojiToggle.setIconResource(R.drawable.ic_face_24); - hideKeyboard(true); - return true; - }); - binding.input.setOnClickListener(v -> { - if (isKbShown) return; - showKeyboard(); + binding.input.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) return; + final Boolean emojiPickerVisibleValue = emojiPickerVisible.getValue(); + if (emojiPickerVisibleValue == null || !emojiPickerVisibleValue) return; + inputHolderAnimationCallback.setShouldTranslate(false); + chatsAnimationCallback.setShouldTranslate(false); + emojiPickerAnimationCallback.setShouldTranslate(false); }); + setupInsetsCallback(); setupEmojiPicker(); binding.gallery.setOnClickListener(v -> { final MediaPickerBottomDialogFragment mediaPicker = MediaPickerBottomDialogFragment.newInstance(); @@ -1163,10 +1117,11 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact if (!isAdded()) return; if (!entry.isVideo) { navigateToImageEditFragment(entry.path); + return; } + handleSentMessage(viewModel.sendUri(entry)); }); mediaPicker.show(getChildFragmentManager(), "MediaPicker"); - hideKeyboard(true); }); binding.gif.setOnClickListener(v -> { final GifPickerBottomDialogFragment gifPicker = GifPickerBottomDialogFragment.newInstance(); @@ -1176,7 +1131,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact handleSentMessage(viewModel.sendAnimatedMedia(giphyGif)); }); gifPicker.show(getChildFragmentManager(), "GifPicker"); - hideKeyboard(true); }); binding.camera.setOnClickListener(v -> { final Intent intent = new Intent(context, CameraActivity.class); @@ -1184,6 +1138,73 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact }); } + private void setupInsetsCallback() { + inputHolderAnimationCallback = new TranslateDeferringInsetsAnimationCallback( + binding.inputHolder, + WindowInsetsCompat.Type.systemBars(), + WindowInsetsCompat.Type.ime(), + WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE + ); + ViewCompat.setWindowInsetsAnimationCallback(binding.inputHolder, inputHolderAnimationCallback); + chatsAnimationCallback = new TranslateDeferringInsetsAnimationCallback( + binding.chats, + WindowInsetsCompat.Type.systemBars(), + WindowInsetsCompat.Type.ime() + ); + ViewCompat.setWindowInsetsAnimationCallback(binding.chats, chatsAnimationCallback); + emojiPickerAnimationCallback = new EmojiPickerInsetsAnimationCallback( + binding.emojiPicker, + WindowInsetsCompat.Type.systemBars(), + WindowInsetsCompat.Type.ime() + ); + emojiPickerAnimationCallback.setKbVisibilityListener(this::onKbVisibilityChange); + ViewCompat.setWindowInsetsAnimationCallback(binding.emojiPicker, emojiPickerAnimationCallback); + ViewCompat.setWindowInsetsAnimationCallback( + binding.input, + new ControlFocusInsetsAnimationCallback(binding.input) + ); + final SimpleImeAnimationController imeAnimController = root.getImeAnimController(); + if (imeAnimController != null) { + imeAnimController.setAnimationControlListener(new WindowInsetsAnimationControlListenerCompat() { + @Override + public void onReady(@NonNull final WindowInsetsAnimationControllerCompat controller, final int types) {} + + @Override + public void onFinished(@NonNull final WindowInsetsAnimationControllerCompat controller) { + checkKbVisibility(); + } + + @Override + public void onCancelled(@Nullable final WindowInsetsAnimationControllerCompat controller) { + checkKbVisibility(); + } + + private void checkKbVisibility() { + final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(binding.getRoot()); + final boolean visible = rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime()); + onKbVisibilityChange(visible); + } + }); + } + } + + private void onKbVisibilityChange(final boolean kbVisible) { + this.kbVisible.postValue(kbVisible); + if (wasToggled) { + emojiPickerVisible.postValue(!kbVisible); + wasToggled = false; + return; + } + final Boolean emojiPickerVisibleValue = emojiPickerVisible.getValue(); + if (kbVisible && emojiPickerVisibleValue != null && emojiPickerVisibleValue) { + emojiPickerVisible.postValue(false); + return; + } + if (!kbVisible) { + emojiPickerVisible.postValue(false); + } + } + private void startIconAnimation() { final Drawable icon = binding.send.getIcon(); if (icon instanceof Animatable) { @@ -1230,15 +1251,87 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact private void setupEmojiPicker() { root.post(() -> binding.emojiPicker.init( root, - (view, emoji) -> binding.input.append(emoji.getUnicode()), + (view, emoji) -> { + final KeyNotifyingEmojiEditText input = binding.input; + final int start = input.getSelectionStart(); + final int end = input.getSelectionEnd(); + if (start < 0) { + input.append(emoji.getUnicode()); + return; + } + input.getText().replace( + Math.min(start, end), + Math.max(start, end), + emoji.getUnicode(), + 0, + emoji.getUnicode().length() + ); + }, () -> binding.input.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)) )); - setupKbHeightProvider(); - if (keyboardHeight == 0) { - keyboardHeight = Utils.convertDpToPx(250); - } - setEmojiPickerBounds(); - binding.emojiToggle.setOnClickListener(v -> toggleEmojiPicker()); + binding.emojiToggle.setOnClickListener(v -> { + Boolean isEmojiPickerVisible = emojiPickerVisible.getValue(); + if (isEmojiPickerVisible == null) isEmojiPickerVisible = false; + Boolean isKbVisible = kbVisible.getValue(); + if (isKbVisible == null) isKbVisible = false; + wasToggled = isEmojiPickerVisible || isKbVisible; + + if (isEmojiPickerVisible) { + if (hasKbOpenedOnce && binding.emojiPicker.getTranslationY() != 0) { + inputHolderAnimationCallback.setShouldTranslate(false); + chatsAnimationCallback.setShouldTranslate(false); + emojiPickerAnimationCallback.setShouldTranslate(false); + } + // trigger ime. + // Since the kb visibility listener will toggle the emojiPickerVisible live data, we do not explicitly toggle it here + showKeyboard(); + return; + } + + if (isKbVisible) { + // hide the keyboard, but don't translate the views + // Since the kb visibility listener will toggle the emojiPickerVisible live data, we do not explicitly toggle it here + inputHolderAnimationCallback.setShouldTranslate(false); + chatsAnimationCallback.setShouldTranslate(false); + emojiPickerAnimationCallback.setShouldTranslate(false); + hideKeyboard(); + } + emojiPickerVisible.postValue(true); + }); + final LiveData> emojiKbVisibilityLD = Utils.zipLiveData(emojiPickerVisible, kbVisible); + emojiKbVisibilityLD.observe(getViewLifecycleOwner(), pair -> { + Boolean isEmojiPickerVisible = pair.first; + Boolean isKbVisible = pair.second; + if (isEmojiPickerVisible == null) isEmojiPickerVisible = false; + if (isKbVisible == null) isKbVisible = false; + root.setScrollImeOffScreenWhenVisible(!isEmojiPickerVisible); + root.setScrollImeOnScreenWhenNotVisible(!isEmojiPickerVisible); + onEmojiPickerBackPressedCallback.setEnabled(isEmojiPickerVisible && !isKbVisible); + if (isEmojiPickerVisible && !isKbVisible) { + animatePan(binding.emojiPicker.getMeasuredHeight(), unused -> { + binding.emojiPicker.setAlpha(1); + binding.emojiToggle.setIconResource(R.drawable.ic_keyboard_24); + return null; + }, null); + return; + } + if (!isEmojiPickerVisible && !isKbVisible) { + animatePan(0, null, unused -> { + binding.emojiPicker.setAlpha(0); + binding.emojiToggle.setIconResource(R.drawable.ic_face_24); + return null; + }); + return; + } + // isKbVisible will always be true going forward + hasKbOpenedOnce = true; + if (!isEmojiPickerVisible) { + binding.emojiToggle.setIconResource(R.drawable.ic_face_24); + binding.emojiPicker.setAlpha(0); + return; + } + binding.emojiPicker.setAlpha(1); + }); } public void showKeyboard() { @@ -1246,67 +1339,21 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact if (context == null) return; final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); if (imm == null) return; - if (!isEmojiPickerShown) { - binding.emojiPicker.setAlpha(0); + if (!binding.input.isFocused()) { + binding.input.requestFocus(); } final boolean shown = imm.showSoftInput(binding.input, InputMethodManager.SHOW_IMPLICIT); if (!shown) { Log.e(TAG, "showKeyboard: System did not display the keyboard"); } - if (!isEmojiPickerShown) { - animatePan(keyboardHeight); - } - isKbShown = true; } - public void hideKeyboard(final boolean shouldPan) { + public void hideKeyboard() { final Context context = getContext(); if (context == null) return; final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); if (imm == null) return; - if (shouldPan) { - binding.emojiPicker.setAlpha(0); - } imm.hideSoftInputFromWindow(binding.input.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN); - if (shouldPan) { - animatePan(0); - isEmojiPickerShown = false; - binding.emojiToggle.setIconResource(R.drawable.ic_face_24); - } - isKbShown = false; - } - - /** - * Toggle between emoji picker and keyboard - * If both are hidden, the emoji picker is shown first - */ - private void toggleEmojiPicker() { - if (isKbShown) { - binding.emojiToggle.setIconResource(R.drawable.ic_keyboard_24); - hideKeyboard(false); - return; - } - if (isEmojiPickerShown) { - binding.emojiToggle.setIconResource(R.drawable.ic_face_24); - showKeyboard(); - return; - } - binding.emojiToggle.setIconResource(R.drawable.ic_keyboard_24); - animatePan(keyboardHeight); - isEmojiPickerShown = true; - } - - /** - * Set height of the emoji picker - */ - private void setEmojiPickerBounds() { - final ViewGroup.LayoutParams layoutParams = binding.emojiPicker.getLayoutParams(); - layoutParams.height = keyboardHeight; - if (!isEmojiPickerShown) { - // If emoji picker is hidden reset the translationY so that it doesn't peek from bottom - binding.emojiPicker.setTranslationY(keyboardHeight); - } - binding.emojiPicker.requestLayout(); } private void setSendToMicIcon() { @@ -1375,40 +1422,18 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact return null; } - private void setupKbHeightProvider() { - if (heightProvider != null) return; - heightProvider = new HeightProvider(fragmentActivity).init().setHeightListener(height -> { - if (height > 100 && keyboardHeight != height) { - // save the current keyboard height to settings to use later - keyboardHeight = height; - setEmojiPickerBounds(); - animatePan(keyboardHeight); - } - }); - } - // Sets the translationY of views to height with animation - private void animatePan(final int height) { + private void animatePan(final int height, + @Nullable final Function onAnimationStart, + @Nullable final Function onAnimationEnd) { if (animatorSet != null && animatorSet.isStarted()) { animatorSet.cancel(); } final ImmutableList.Builder builder = ImmutableList.builder(); builder.add( ObjectAnimator.ofFloat(binding.chats, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.input, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.inputBg, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.recordView, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.emojiToggle, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.gif, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.gallery, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.camera, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.send, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.replyBg, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.replyInfo, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.replyCancel, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.replyPreviewImage, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.replyPreviewText, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.emojiPicker, TRANSLATION_Y, keyboardHeight - height) + ObjectAnimator.ofFloat(binding.inputHolder, TRANSLATION_Y, -height), + ObjectAnimator.ofFloat(binding.emojiPicker, TRANSLATION_Y, -height) ); // if (headerItemDecoration != null && headerItemDecoration.getCurrentHeader() != null) { // builder.add(ObjectAnimator.ofFloat(headerItemDecoration.getCurrentHeader(), TRANSLATION_Y, height)); @@ -1418,10 +1443,21 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact animatorSet.setDuration(200); animatorSet.setInterpolator(CubicBezierInterpolator.EASE_IN); animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(final Animator animation) { + super.onAnimationStart(animation); + if (onAnimationStart != null) { + onAnimationStart.apply(null); + } + } + @Override public void onAnimationEnd(final Animator animation) { - binding.emojiPicker.setAlpha(1); + super.onAnimationEnd(animation); animatorSet = null; + if (onAnimationEnd != null) { + onAnimationEnd.apply(null); + } } }); animatorSet.start(); diff --git a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java index ea54ceca..d22b5a51 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java @@ -337,6 +337,12 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre return super.onOptionsItemSelected(item); } + @Override + public void onResume() { + super.onResume(); + binding.getRoot().postDelayed(feedStoriesAdapter::notifyDataSetChanged, 1000); + } + @Override public void onRefresh() { binding.feedRecyclerView.refresh(); @@ -418,15 +424,16 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre } private void fetchStories() { + if (storiesFetching) return; // final String cookie = settingsHelper.getString(Constants.COOKIE); storiesFetching = true; updateSwipeRefreshState(); storiesService.getFeedStories(new ServiceCallback>() { @Override public void onSuccess(final List result) { + storiesFetching = false; feedStoriesViewModel.getList().postValue(result); feedStoriesAdapter.submitList(result); - storiesFetching = false; if (storyListMenu != null) storyListMenu.setVisible(true); updateSwipeRefreshState(); } @@ -451,8 +458,10 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre } public void scrollToTop() { - binding.feedRecyclerView.smoothScrollToPosition(0); - // binding.storiesContainer.setExpanded(true); + if (binding != null) { + binding.feedRecyclerView.smoothScrollToPosition(0); + // binding.storiesContainer.setExpanded(true); + } } private boolean isSafeToNavigate(final NavController navController) { diff --git a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java index 17dfa964..cc646855 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java @@ -407,7 +407,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe } chainingMenuItem = menu.findItem(R.id.chaining); if (chainingMenuItem != null) { - chainingMenuItem.setVisible(isNotMe); + chainingMenuItem.setVisible(isNotMe && profileModel.hasChaining()); } } @@ -527,7 +527,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe @Override public void onRefresh() { - profileDetailsBinding.countsBarrier.getRoot().setVisibility(View.GONE); + profileDetailsBinding.countsDivider.getRoot().setVisibility(View.GONE); profileDetailsBinding.mainProfileImage.setVisibility(View.INVISIBLE); fetchProfileDetails(); } @@ -662,17 +662,21 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe Toast.makeText(context, R.string.error_loading_profile, Toast.LENGTH_SHORT).show(); return; } - if (!postsSetupDone) { - setupPosts(); - } else { - binding.postsRecyclerView.refresh(); + final long profileId = profileModel.getPk(); + if (!isReallyPrivate()) { + if (!postsSetupDone) { + setupPosts(); + } + else { + binding.postsRecyclerView.refresh(); + } + if (isLoggedIn) { + fetchStoryAndHighlights(profileId); + } } profileDetailsBinding.isVerified.setVisibility(profileModel.isVerified() ? View.VISIBLE : View.GONE); profileDetailsBinding.isPrivate.setVisibility(profileModel.isPrivate() ? View.VISIBLE : View.GONE); - final long profileId = profileModel.getPk(); - if (isLoggedIn) { - fetchStoryAndHighlights(profileId); - } + setupButtons(profileId); final FavoriteRepository favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(getContext())); favoriteRepository.getFavorite(profileModel.getUsername(), FavoriteType.USER, new RepositoryCallback() { @@ -744,7 +748,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe profileDetailsBinding.mainProfileImage.setImageURI(profileModel.getProfilePicUrl()); profileDetailsBinding.mainProfileImage.setVisibility(View.VISIBLE); - profileDetailsBinding.countsBarrier.getRoot().setVisibility(View.VISIBLE); + profileDetailsBinding.countsDivider.getRoot().setVisibility(View.VISIBLE); final long followersCount = profileModel.getFollowerCount(); final long followingCount = profileModel.getFollowingCount(); @@ -905,6 +909,8 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe binding.privatePage1.setImageResource(R.drawable.lock); binding.privatePage2.setText(R.string.priv_acc); binding.privatePage.setVisibility(View.VISIBLE); + binding.privatePage1.setVisibility(View.VISIBLE); + binding.privatePage2.setVisibility(View.VISIBLE); binding.postsRecyclerView.setVisibility(View.GONE); binding.swipeRefreshLayout.setRefreshing(false); } @@ -970,7 +976,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe mutePostsMenuItem.setTitle(profileModel.getFriendshipStatus().isMuting() ? R.string.unmute_posts : R.string.mute_posts); } if (chainingMenuItem != null) { - chainingMenuItem.setVisible(true); + chainingMenuItem.setVisible(profileModel.hasChaining()); } } } diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java index a022d9ee..e610fe56 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java @@ -4,8 +4,11 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.res.Resources; +import android.os.Bundle; import android.util.Log; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; @@ -19,6 +22,7 @@ import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceViewHolder; +import androidx.recyclerview.widget.RecyclerView; import java.util.List; @@ -53,6 +57,20 @@ public class MorePreferencesFragment extends BasePreferencesFragment { public MorePreferencesFragment() { } + @Override + public RecyclerView onCreateRecyclerView(final LayoutInflater inflater, final ViewGroup parent, final Bundle savedInstanceState) { + final RecyclerView recyclerView = super.onCreateRecyclerView(inflater, parent, savedInstanceState); + final Context context = getContext(); + if (recyclerView != null && context != null) { + recyclerView.setClipToPadding(false); + recyclerView.setPadding(recyclerView.getPaddingLeft(), + recyclerView.getPaddingTop(), + recyclerView.getPaddingRight(), + Utils.getActionBarHeight(context)); + } + return recyclerView; + } + @Override void setupPreferenceScreen(final PreferenceScreen screen) { final String cookie = settingsHelper.getString(Constants.COOKIE); diff --git a/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java b/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java index 04595093..b87091e5 100755 --- a/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java +++ b/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java @@ -16,11 +16,15 @@ public final class FeedStoryModel implements Serializable { private final boolean isLive, isBestie; private final long timestamp; private final int mediaCount; - private boolean isShown = true; - public FeedStoryModel(final String storyMediaId, final User profileModel, final boolean fullyRead, - final long timestamp, final StoryModel firstStoryModel, final int mediaCount, - final boolean isLive, final boolean isBestie) { + public FeedStoryModel(final String storyMediaId, + final User profileModel, + final boolean fullyRead, + final long timestamp, + final StoryModel firstStoryModel, + final int mediaCount, + final boolean isLive, + final boolean isBestie) { this.storyMediaId = storyMediaId; this.profileModel = profileModel; this.fullyRead = fullyRead; @@ -52,10 +56,6 @@ public final class FeedStoryModel implements Serializable { return profileModel; } - // public void setFirstStoryModel(final StoryModel firstStoryModel) { - // this.firstStoryModel = firstStoryModel; - // } - public StoryModel getFirstStoryModel() { return firstStoryModel; } @@ -75,12 +75,4 @@ public final class FeedStoryModel implements Serializable { public boolean isBestie() { return isBestie; } - - public boolean isShown() { - return isShown; - } - - public void setShown(final boolean shown) { - isShown = shown; - } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/User.java b/app/src/main/java/awais/instagrabber/repositories/responses/User.java index b2f2a0b2..0d707f72 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/User.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/User.java @@ -17,6 +17,7 @@ public class User implements Serializable { private final boolean isUnpublished; private final boolean isFavorite; private final boolean isDirectappInstalled; + private final boolean hasChaining; private final String reelAutoArchive; private final String allowedCommenterType; private final long mediaCount; @@ -28,11 +29,10 @@ public class User implements Serializable { private final long usertagsCount; private final String publicEmail; private final HdProfilePicUrlInfo hdProfilePicUrlInfo; - private final String profileContext; - private final List profileContextLinksWithUserIds; - private final String socialContext; - // if a DM member is a Facebook user, this is present - private final String interopMessagingUserFbid; + private final String profileContext; // "also followed by" your friends + private final List profileContextLinksWithUserIds; // ^ + private final String socialContext; // AYML + private final String interopMessagingUserFbid; // in DMs only: Facebook user ID public User(final long pk, final String username, @@ -46,6 +46,7 @@ public class User implements Serializable { final boolean isUnpublished, final boolean isFavorite, final boolean isDirectappInstalled, + final boolean hasChaining, final String reelAutoArchive, final String allowedCommenterType, final long mediaCount, @@ -73,6 +74,7 @@ public class User implements Serializable { this.isUnpublished = isUnpublished; this.isFavorite = isFavorite; this.isDirectappInstalled = isDirectappInstalled; + this.hasChaining = hasChaining; this.reelAutoArchive = reelAutoArchive; this.allowedCommenterType = allowedCommenterType; this.mediaCount = mediaCount; @@ -90,6 +92,53 @@ public class User implements Serializable { this.interopMessagingUserFbid = interopMessagingUserFbid; } + public User(final long pk, + final String username, + final String fullName, + final boolean isPrivate, + final String profilePicUrl, + final boolean isVerified) { + this.pk = pk; + this.username = username; + this.fullName = fullName; + this.isPrivate = isPrivate; + this.profilePicUrl = profilePicUrl; + this.profilePicId = null; + this.friendshipStatus = new FriendshipStatus( + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ); + this.isVerified = isVerified; + this.hasAnonymousProfilePicture = false; + this.isUnpublished = false; + this.isFavorite = false; + this.isDirectappInstalled = false; + this.hasChaining = false; + this.reelAutoArchive = null; + this.allowedCommenterType = null; + this.mediaCount = 0; + this.followerCount = 0; + this.followingCount = 0; + this.followingTagCount = 0; + this.biography = null; + this.externalUrl = null; + this.usertagsCount = 0; + this.publicEmail = null; + this.hdProfilePicUrlInfo = null; + this.profileContext = null; + this.profileContextLinksWithUserIds = null; + this.socialContext = null; + this.interopMessagingUserFbid = null; + } + public long getPk() { return pk; } @@ -149,6 +198,10 @@ public class User implements Serializable { return isDirectappInstalled; } + public boolean hasChaining() { + return hasChaining; + } + public String getReelAutoArchive() { return reelAutoArchive; } @@ -237,7 +290,7 @@ public class User implements Serializable { @Override public int hashCode() { return Objects.hash(pk, username, fullName, isPrivate, profilePicUrl, profilePicId, friendshipStatus, isVerified, hasAnonymousProfilePicture, - isUnpublished, isFavorite, isDirectappInstalled, reelAutoArchive, allowedCommenterType, mediaCount, followerCount, - followingCount, followingTagCount, biography, externalUrl, usertagsCount, publicEmail); + isUnpublished, isFavorite, isDirectappInstalled, hasChaining, reelAutoArchive, allowedCommenterType, mediaCount, + followerCount, followingCount, followingTagCount, biography, externalUrl, usertagsCount, publicEmail); } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/search/SearchItem.java b/app/src/main/java/awais/instagrabber/repositories/responses/search/SearchItem.java index f99c7dfd..9f56b765 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/search/SearchItem.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/search/SearchItem.java @@ -191,9 +191,7 @@ public class SearchItem { recentSearch.getName(), false, recentSearch.getPicUrl(), - null, null, false, false, false, false, false, - null, null, 0, 0, 0, 0, null, null, - 0, null, null, null, null, null, null + false ); } @@ -205,9 +203,7 @@ public class SearchItem { favorite.getDisplayName(), false, favorite.getPicUrl(), - null, null, false, false, false, false, false, - null, null, 0, 0, 0, 0, null, null, - 0, null, null, null, null, null, null + false ); } diff --git a/app/src/main/java/awais/instagrabber/utils/NavigationExtensions.java b/app/src/main/java/awais/instagrabber/utils/NavigationExtensions.java index a6bccbfd..bc0f3cb4 100644 --- a/app/src/main/java/awais/instagrabber/utils/NavigationExtensions.java +++ b/app/src/main/java/awais/instagrabber/utils/NavigationExtensions.java @@ -63,7 +63,7 @@ public class NavigationExtensions { selectedItemTag = graphIdToTagMap.get(bottomNavigationView.getSelectedItemId()); final String firstFragmentTag = graphIdToTagMap.get(firstFragmentGraphId); isOnFirstFragment = selectedItemTag != null && selectedItemTag.equals(firstFragmentTag); - bottomNavigationView.setOnNavigationItemSelectedListener(item -> { + bottomNavigationView.setOnItemSelectedListener(item -> { if (fragmentManager.isStateSaved()) { return false; } @@ -169,7 +169,7 @@ public class NavigationExtensions { private static void setupItemReselected(final BottomNavigationView bottomNavigationView, final SparseArray graphIdToTagMap, final FragmentManager fragmentManager) { - bottomNavigationView.setOnNavigationItemReselectedListener(item -> { + bottomNavigationView.setOnItemReselectedListener(item -> { final String newlySelectedItemTag = graphIdToTagMap.get(item.getItemId()); final Fragment fragmentByTag = fragmentManager.findFragmentByTag(newlySelectedItemTag); if (fragmentByTag == null) { diff --git a/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java b/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java index b3968fdd..f205b04f 100644 --- a/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java @@ -767,11 +767,7 @@ public final class ResponseBodyUtils { owner.optString("full_name"), false, owner.optString("profile_pic_url"), - null, - friendshipStatus, - owner.optBoolean("is_verified"), - false, false, false, false, null, null, 0, 0, 0, 0, null, null, 0, null, null, - null, null, null, null); + owner.optBoolean("is_verified")); } final String id = feedItem.getString(Constants.EXTRAS_ID); VideoVersion videoVersion = null; diff --git a/app/src/main/java/awais/instagrabber/utils/Utils.java b/app/src/main/java/awais/instagrabber/utils/Utils.java index a9424b33..3a157f1a 100644 --- a/app/src/main/java/awais/instagrabber/utils/Utils.java +++ b/app/src/main/java/awais/instagrabber/utils/Utils.java @@ -38,6 +38,8 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MediatorLiveData; import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; import com.google.android.exoplayer2.database.ExoDatabaseProvider; @@ -562,4 +564,39 @@ public final class Utils { display.getRealSize(size); return size; } + + public static LiveData> zipLiveData(@NonNull final LiveData firstLiveData, + @NonNull final LiveData secondLiveData) { + final ZippedLiveData zippedLiveData = new ZippedLiveData<>(); + zippedLiveData.addFirstSource(firstLiveData); + zippedLiveData.addSecondSource(secondLiveData); + return zippedLiveData; + } + + public static class ZippedLiveData extends MediatorLiveData> { + private F lastF; + private S lastS; + + private void update() { + F localLastF = lastF; + S localLastS = lastS; + if (localLastF != null && localLastS != null) { + setValue(new Pair<>(localLastF, localLastS)); + } + } + + public void addFirstSource(@NonNull final LiveData firstLiveData) { + addSource(firstLiveData, f -> { + lastF = f; + update(); + }); + } + + public void addSecondSource(@NonNull final LiveData secondLiveData) { + addSource(secondLiveData, s -> { + lastS = s; + update(); + }); + } + } } diff --git a/app/src/main/java/awais/instagrabber/utils/ViewUtils.java b/app/src/main/java/awais/instagrabber/utils/ViewUtils.java index bf39d7a4..c2c91e61 100644 --- a/app/src/main/java/awais/instagrabber/utils/ViewUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/ViewUtils.java @@ -1,18 +1,28 @@ package awais.instagrabber.utils; +import android.annotation.SuppressLint; import android.content.Context; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.RoundRectShape; +import android.os.Build; import android.view.View; +import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.TextView; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.content.res.ResourcesCompat; import androidx.core.util.Pair; +import androidx.dynamicanimation.animation.FloatPropertyCompat; +import androidx.dynamicanimation.animation.SpringAnimation; + +import org.jetbrains.annotations.NotNull; + +import kotlin.jvm.internal.Intrinsics; public final class ViewUtils { @@ -69,4 +79,43 @@ public final class ViewUtils { public static float getTextViewValueWidth(final TextView textView, final String text) { return textView.getPaint().measureText(text); } + + /** + * Creates [SpringAnimation] for object. + * If finalPosition is not [Float.NaN] then create [SpringAnimation] with + * [SpringForce.mFinalPosition]. + * + * @param object Object + * @param property object's property to be animated. + * @param finalPosition [SpringForce.mFinalPosition] Final position of spring. + * @return [SpringAnimation] + */ + @NonNull + public static SpringAnimation springAnimationOf(final Object object, + final FloatPropertyCompat property, + @Nullable final Float finalPosition) { + return finalPosition == null ? new SpringAnimation(object, property) : new SpringAnimation(object, property, finalPosition); + } + + public static void suppressLayoutCompat(@NotNull ViewGroup $this$suppressLayoutCompat, boolean suppress) { + Intrinsics.checkNotNullParameter($this$suppressLayoutCompat, "$this$suppressLayoutCompat"); + if (Build.VERSION.SDK_INT >= 29) { + $this$suppressLayoutCompat.suppressLayout(suppress); + } else { + hiddenSuppressLayout($this$suppressLayoutCompat, suppress); + } + } + + private static boolean tryHiddenSuppressLayout = true; + + @SuppressLint({"NewApi"}) + private static void hiddenSuppressLayout(ViewGroup group, boolean suppress) { + if (tryHiddenSuppressLayout) { + try { + group.suppressLayout(suppress); + } catch (NoSuchMethodError var3) { + tryHiddenSuppressLayout = false; + } + } + } } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java index 109b6fea..ab42e84d 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java @@ -238,11 +238,7 @@ public class CommentsViewerViewModel extends ViewModel { null, false, owner.getString("profile_pic_url"), - null, - new FriendshipStatus(false, false, false, false, false, false, false, false, false, false), - owner.optBoolean("is_verified"), - false, false, false, false, null, null, 0, 0, 0, 0, null, null, 0, null, null, null, null, - null, null); + owner.optBoolean("is_verified")); final JSONObject likedBy = commentJsonObject.optJSONObject("edge_liked_by"); final String commentId = commentJsonObject.getString("id"); final JSONObject childCommentsJsonObject = commentJsonObject.optJSONObject("edge_threaded_comments"); diff --git a/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java b/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java index eef7af9a..ddb6da6b 100644 --- a/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java +++ b/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java @@ -34,7 +34,6 @@ import retrofit2.Response; public class GraphQLService extends BaseService { private static final String TAG = "GraphQLService"; - // private static final boolean loadFromMock = false; private final GraphQLRepository repository; @@ -230,39 +229,7 @@ public class GraphQLService extends BaseService { userObject.optString("full_name"), userObject.optBoolean("is_private"), userObject.getString("profile_pic_url"), - null, - new FriendshipStatus( - false, - false, - false, - false, - false, - false, - false, - false, - false, - false - ), - userObject.optBoolean("is_verified"), - false, - false, - false, - false, - null, - null, - 0, - 0, - 0, - 0, - null, - null, - 0, - null, - null, - null, - null, - null, - null + userObject.optBoolean("is_verified") )); // userModels.add(new ProfileModel(userObject.optBoolean("is_private"), // false, @@ -357,6 +324,7 @@ public class GraphQLService extends BaseService { false, false, false, + false, null, null, timelineMedia.getLong("count"), diff --git a/app/src/main/java/awais/instagrabber/webservices/StoriesService.java b/app/src/main/java/awais/instagrabber/webservices/StoriesService.java index 169f630a..96e9bc7d 100644 --- a/app/src/main/java/awais/instagrabber/webservices/StoriesService.java +++ b/app/src/main/java/awais/instagrabber/webservices/StoriesService.java @@ -143,39 +143,7 @@ public class StoriesService extends BaseService { userJson.optString("full_name"), userJson.optBoolean("is_private"), userJson.getString("profile_pic_url"), - null, - new FriendshipStatus( - false, - false, - false, - false, - false, - false, - false, - false, - false, - false - ), - userJson.optBoolean("is_verified"), - false, - false, - false, - false, - null, - null, - 0, - 0, - 0, - 0, - null, - null, - 0, - null, - null, - null, - null, - null, - null + userJson.optBoolean("is_verified") ); final long timestamp = node.getLong("latest_reel_media"); final boolean fullyRead = !node.isNull("seen") && node.getLong("seen") == timestamp; @@ -210,39 +178,7 @@ public class StoriesService extends BaseService { userJson.optString("full_name"), userJson.optBoolean("is_private"), userJson.getString("profile_pic_url"), - null, - new FriendshipStatus( - false, - false, - false, - false, - false, - false, - false, - false, - false, - false - ), - userJson.optBoolean("is_verified"), - false, - false, - false, - false, - null, - null, - 0, - 0, - 0, - 0, - null, - null, - 0, - null, - null, - null, - null, - null, - null + userJson.optBoolean("is_verified") ); feedStoryModels.add(new FeedStoryModel( node.getString("id"), diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 1e46b341..9d1e880a 100755 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,5 +1,5 @@ - + - \ No newline at end of file + app:labelVisibilityMode="auto" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_direct_messages_thread.xml b/app/src/main/res/layout/fragment_direct_messages_thread.xml index 96b0bc34..50120027 100644 --- a/app/src/main/res/layout/fragment_direct_messages_thread.xml +++ b/app/src/main/res/layout/fragment_direct_messages_thread.xml @@ -1,312 +1,315 @@ - + android:clipToPadding="false" + android:orientation="vertical"> - + - + - + - + - + - + - - - - - - - + - + + + + + + + - + - + - + - + - + - + - + + + + + + + + + + - - - - - - - \ No newline at end of file + app:layout_constraintStart_toStartOf="parent" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_discover.xml b/app/src/main/res/layout/fragment_discover.xml index e18824c7..ff29514e 100644 --- a/app/src/main/res/layout/fragment_discover.xml +++ b/app/src/main/res/layout/fragment_discover.xml @@ -11,7 +11,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" - android:paddingBottom="?attr/actionBarSize" app:layout_behavior="@string/appbar_scrolling_view_behavior"> diff --git a/app/src/main/res/layout/fragment_location.xml b/app/src/main/res/layout/fragment_location.xml index 9293dc2a..724e5814 100644 --- a/app/src/main/res/layout/fragment_location.xml +++ b/app/src/main/res/layout/fragment_location.xml @@ -19,7 +19,6 @@ android:layout_width="match_parent" android:layout_height="0dp" android:clipToPadding="false" - android:paddingBottom="?attr/actionBarSize" app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@id/header"> diff --git a/app/src/main/res/layout/fragment_profile.xml b/app/src/main/res/layout/fragment_profile.xml index 051dd0aa..48b36bfd 100644 --- a/app/src/main/res/layout/fragment_profile.xml +++ b/app/src/main/res/layout/fragment_profile.xml @@ -38,7 +38,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="top" - android:layout_marginTop="@dimen/private_page_margins" android:gravity="center" android:orientation="vertical" android:visibility="gone" @@ -48,6 +47,8 @@ android:id="@+id/privatePage1" android:layout_width="@dimen/private_page_size" android:layout_height="@dimen/private_page_size" + android:visibility="gone" + tools:visibility="visible" app:srcCompat="@drawable/lock" /> diff --git a/app/src/main/res/layout/item_feed_top.xml b/app/src/main/res/layout/item_feed_top.xml index 7aac41b5..61b49c31 100755 --- a/app/src/main/res/layout/item_feed_top.xml +++ b/app/src/main/res/layout/item_feed_top.xml @@ -43,7 +43,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/title" + android:ellipsize="end" android:gravity="center_vertical" + android:maxLines="1" android:textAppearance="@style/TextAppearance.AppCompat.Subhead" android:textSize="15sp" android:visibility="visible" diff --git a/app/src/main/res/layout/layout_profile_details.xml b/app/src/main/res/layout/layout_profile_details.xml index d7f5bdd8..61cec0ff 100644 --- a/app/src/main/res/layout/layout_profile_details.xml +++ b/app/src/main/res/layout/layout_profile_details.xml @@ -15,10 +15,11 @@ android:transitionName="profile_pic" android:visibility="invisible" app:actualImageScaleType="centerCrop" - app:layout_constraintBottom_toBottomOf="@id/btnTagged" + app:layout_constraintBottom_toTopOf="@id/top_barrier" app:layout_constraintEnd_toStartOf="@id/btnFollow" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0" tools:foreground="@mipmap/ic_launcher" tools:visibility="visible" /> @@ -112,7 +113,7 @@ app:chipBackgroundColor="@null" app:chipIcon="@drawable/ic_outline_person_pin_24" app:chipIconTint="@color/deep_orange_800" - app:layout_constraintBottom_toTopOf="@+id/mainFullName" + app:layout_constraintBottom_toTopOf="@+id/top_barrier" app:layout_constraintStart_toEndOf="@id/mainProfileImage" app:layout_constraintTop_toBottomOf="@id/fav_chip" app:rippleColor="@color/deep_orange_400" @@ -128,12 +129,18 @@ app:chipBackgroundColor="@null" app:chipIcon="@drawable/ic_round_send_24" app:chipIconTint="@color/green" - app:layout_constraintBottom_toTopOf="@+id/mainFullName" + app:layout_constraintBottom_toTopOf="@+id/top_barrier" app:layout_constraintStart_toEndOf="@id/btnTagged" app:layout_constraintTop_toBottomOf="@id/fav_chip" app:rippleColor="@color/green" tools:visibility="visible" /> + + @@ -233,6 +237,7 @@ android:paddingBottom="4dp" android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:visibility="gone" + app:layout_constraintBottom_toTopOf="@id/profileContext" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/mainBiography" @@ -254,7 +259,7 @@ android:textSize="12sp" android:textStyle="italic" android:visibility="gone" - app:layout_constraintBottom_toTopOf="@id/counts_barrier" + app:layout_constraintBottom_toTopOf="@id/counts_divider" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/mainUrl" @@ -262,11 +267,12 @@ tools:visibility="visible" /> @color/text_color_light #FF5500 - #FFFFFFFF + @color/white #FFBB00 - #FF000000 + @color/black #efefef diff --git a/app/src/main/res/xml/header_list_scene.xml b/app/src/main/res/xml/header_list_scene.xml index 389b7cb1..f4a76d88 100644 --- a/app/src/main/res/xml/header_list_scene.xml +++ b/app/src/main/res/xml/header_list_scene.xml @@ -9,6 +9,13 @@ motion:layout_constraintEnd_toEndOf="parent" motion:layout_constraintStart_toStartOf="parent" motion:layout_constraintTop_toTopOf="parent" /> +