Browse Source
Merge branch 'stamatiap/development' of https://github.com/raniapl/barinsta into stamatiap/development
renovate/org.robolectric-robolectric-4.x
Merge branch 'stamatiap/development' of https://github.com/raniapl/barinsta into stamatiap/development
renovate/org.robolectric-robolectric-4.x
131 changed files with 5100 additions and 3116 deletions
-
19.all-contributorsrc
-
15.github/workflows/github_nightly_release.yml
-
15.github/workflows/github_pre_release.yml
-
2.idea/compiler.xml
-
1.idea/gradle.xml
-
2.idea/misc.xml
-
1.idea/runConfigurations.xml
-
28README.md
-
59app/build.gradle
-
3app/src/main/AndroidManifest.xml
-
45app/src/main/java/awais/instagrabber/activities/MainActivity.java
-
2app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java
-
40app/src/main/java/awais/instagrabber/adapters/FeedStoriesListAdapter.java
-
16app/src/main/java/awais/instagrabber/adapters/SliderCallbackAdapter.java
-
22app/src/main/java/awais/instagrabber/adapters/SliderItemsAdapter.java
-
77app/src/main/java/awais/instagrabber/adapters/viewholder/SliderPhotoViewHolder.java
-
117app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java
-
3app/src/main/java/awais/instagrabber/adapters/viewholder/StoryListViewHolder.java
-
8app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemAnimatedMediaViewHolder.java
-
8app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaShareViewHolder.java
-
9app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaViewHolder.java
-
8app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemRavenMediaViewHolder.java
-
9app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemStoryShareViewHolder.java
-
4app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java
-
8app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemXmaViewHolder.java
-
4app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java
-
17app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java
-
165app/src/main/java/awais/instagrabber/customviews/FormattedNumberTextView.java
-
75app/src/main/java/awais/instagrabber/customviews/FragmentNavigatorWithDefaultAnimations.java
-
246app/src/main/java/awais/instagrabber/customviews/InsetsAnimationLinearLayout.java
-
33app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingCoordinatorLayout.java
-
35app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingLinearLayout.java
-
60app/src/main/java/awais/instagrabber/customviews/NavHostFragmentWithDefaultAnimations.java
-
23app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java
-
7app/src/main/java/awais/instagrabber/customviews/Tooltip.java
-
10app/src/main/java/awais/instagrabber/customviews/VideoPlayerCallbackAdapter.java
-
411app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java
-
2app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java
-
100app/src/main/java/awais/instagrabber/customviews/emoji/EmojiBottomSheetDialog.java
-
13app/src/main/java/awais/instagrabber/customviews/emoji/EmojiGridAdapter.java
-
163app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPopupWindow.java
-
1app/src/main/java/awais/instagrabber/customviews/emoji/GoogleCompatEmojiDrawable.java
-
320app/src/main/java/awais/instagrabber/customviews/helpers/ChangeText.java
-
87app/src/main/java/awais/instagrabber/customviews/helpers/ControlFocusInsetsAnimationCallback.java
-
17app/src/main/java/awais/instagrabber/customviews/helpers/CustomHideBottomViewOnScrollBehavior.java
-
117app/src/main/java/awais/instagrabber/customviews/helpers/EmojiPickerInsetsAnimationCallback.java
-
19app/src/main/java/awais/instagrabber/customviews/helpers/GridSpacingItemDecoration.java
-
139app/src/main/java/awais/instagrabber/customviews/helpers/RootViewDeferringInsetsCallback.java
-
443app/src/main/java/awais/instagrabber/customviews/helpers/SimpleImeAnimationController.java
-
128app/src/main/java/awais/instagrabber/customviews/helpers/TranslateDeferringInsetsAnimationCallback.java
-
9app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java
-
54app/src/main/java/awais/instagrabber/fragments/CollectionPostsFragment.java
-
8app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java
-
70app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java
-
54app/src/main/java/awais/instagrabber/fragments/LocationFragment.java
-
14app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java
-
1138app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java
-
19app/src/main/java/awais/instagrabber/fragments/SavedViewerFragment.java
-
14app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java
-
43app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java
-
21app/src/main/java/awais/instagrabber/fragments/TopicPostsFragment.java
-
16app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.java
-
473app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java
-
61app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java
-
80app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java
-
18app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java
-
1app/src/main/java/awais/instagrabber/fragments/settings/PreferenceKeys.java
-
4app/src/main/java/awais/instagrabber/managers/ThreadManager.java
-
24app/src/main/java/awais/instagrabber/models/FeedStoryModel.java
-
70app/src/main/java/awais/instagrabber/repositories/responses/User.java
-
10app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemEmojiReaction.java
-
8app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemReactions.java
-
8app/src/main/java/awais/instagrabber/repositories/responses/search/SearchItem.java
-
15app/src/main/java/awais/instagrabber/utils/CombinedDrawable.java
-
32app/src/main/java/awais/instagrabber/utils/DMUtils.java
-
7app/src/main/java/awais/instagrabber/utils/NavigationExtensions.java
-
91app/src/main/java/awais/instagrabber/utils/NullSafePair.java
-
75app/src/main/java/awais/instagrabber/utils/NumberUtils.java
-
6app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java
-
3app/src/main/java/awais/instagrabber/utils/SettingsHelper.java
-
88app/src/main/java/awais/instagrabber/utils/Utils.java
-
49app/src/main/java/awais/instagrabber/utils/ViewUtils.java
-
8app/src/main/java/awais/instagrabber/utils/emoji/EmojiParser.java
-
6app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java
-
36app/src/main/java/awais/instagrabber/webservices/GraphQLService.java
-
68app/src/main/java/awais/instagrabber/webservices/StoriesService.java
-
1app/src/main/java/awais/instagrabber/webservices/interceptors/IgErrorsInterceptor.java
-
11app/src/main/res/anim/slide_in_right.xml
-
9app/src/main/res/anim/slide_left.xml
-
11app/src/main/res/anim/slide_out_left.xml
-
9app/src/main/res/anim/slide_right.xml
-
6app/src/main/res/drawable/ic_bookmark.xml
-
10app/src/main/res/drawable/ic_round_bookmark_border_24.xml
-
10app/src/main/res/drawable/ic_round_edit_24.xml
-
2app/src/main/res/drawable/shape_oval_light.xml
-
9app/src/main/res/layout/activity_main.xml
-
391app/src/main/res/layout/dialog_post_view.xml
-
2app/src/main/res/layout/fragment_collection_posts.xml
-
41app/src/main/res/layout/fragment_direct_messages_thread.xml
-
3app/src/main/res/layout/fragment_discover.xml
@ -15,18 +15,19 @@ jobs: |
|||||
uses: actions/checkout@v2 |
uses: actions/checkout@v2 |
||||
|
|
||||
- name: set up JDK 1.8 |
- name: set up JDK 1.8 |
||||
uses: actions/setup-java@v1 |
|
||||
|
uses: actions/setup-java@v2 |
||||
with: |
with: |
||||
java-version: 1.8 |
|
||||
|
distribution: 'zulu' |
||||
|
java-version: '8' |
||||
|
|
||||
- name: Grant execute permission for gradlew |
- name: Grant execute permission for gradlew |
||||
run: chmod +x gradlew |
run: chmod +x gradlew |
||||
|
|
||||
- name: Build Github unsigned apk |
- name: Build Github unsigned apk |
||||
run: ./gradlew assembleGithubRelease --stacktrace --project-prop pre |
|
||||
|
run: ./gradlew assembleGithubRelease --stacktrace --project-prop pre --project-prop split |
||||
|
|
||||
- name: Sign APK |
- name: Sign APK |
||||
uses: r0adkll/sign-android-release@v1 |
|
||||
|
uses: ammargitham/[email protected].1 |
||||
# ID used to access action output |
# ID used to access action output |
||||
id: sign_app |
id: sign_app |
||||
with: |
with: |
||||
@ -45,7 +46,8 @@ jobs: |
|||||
uses: actions/upload-artifact@v2 |
uses: actions/upload-artifact@v2 |
||||
with: |
with: |
||||
name: barinsta_nightly_${{ steps.date.outputs.date }} |
name: barinsta_nightly_${{ steps.date.outputs.date }} |
||||
path: ${{steps.sign_app.outputs.signedReleaseFile}} |
|
||||
|
# path: ${{steps.sign_app.outputs.signedReleaseFile}} |
||||
|
path: app/build/outputs/apk/github/release/*-signed.apk |
||||
|
|
||||
# Send success notification |
# Send success notification |
||||
- name: Send success Telegram notification |
- name: Send success Telegram notification |
||||
@ -55,7 +57,8 @@ jobs: |
|||||
to: ${{ secrets.TELEGRAM_BUILDS_CHANNEL_TO }} |
to: ${{ secrets.TELEGRAM_BUILDS_CHANNEL_TO }} |
||||
token: ${{ secrets.TELEGRAM_BUILDS_BOT_TOKEN }} |
token: ${{ secrets.TELEGRAM_BUILDS_BOT_TOKEN }} |
||||
message: "${{ github.workflow }} ${{ github.job }} #${{ github.run_number }} completed successfully.\nhttps://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" |
message: "${{ github.workflow }} ${{ github.job }} #${{ github.run_number }} completed successfully.\nhttps://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" |
||||
document: ${{steps.sign_app.outputs.signedReleaseFile}} |
|
||||
|
# document: ${{steps.sign_app.outputs.signedReleaseFile}} |
||||
|
document: app/build/outputs/apk/github/release/*-signed.apk |
||||
|
|
||||
# Send failure notification |
# Send failure notification |
||||
- name: Send failure Telegram notification |
- name: Send failure Telegram notification |
||||
|
@ -16,18 +16,19 @@ jobs: |
|||||
uses: actions/checkout@v2 |
uses: actions/checkout@v2 |
||||
|
|
||||
- name: set up JDK 1.8 |
- name: set up JDK 1.8 |
||||
uses: actions/setup-java@v1 |
|
||||
|
uses: actions/setup-java@v2 |
||||
with: |
with: |
||||
java-version: 1.8 |
|
||||
|
distribution: 'zulu' |
||||
|
java-version: '8' |
||||
|
|
||||
- name: Grant execute permission for gradlew |
- name: Grant execute permission for gradlew |
||||
run: chmod +x gradlew |
run: chmod +x gradlew |
||||
|
|
||||
- name: Build Github unsigned pre-release apk |
- name: Build Github unsigned pre-release apk |
||||
run: ./gradlew assembleGithubRelease --stacktrace --project-prop pre |
|
||||
|
run: ./gradlew assembleGithubRelease --stacktrace --project-prop pre --project-prop split |
||||
|
|
||||
- name: Sign APK |
- name: Sign APK |
||||
uses: r0adkll/sign-android-release@v1 |
|
||||
|
uses: ammargitham/[email protected].1 |
||||
# ID used to access action output |
# ID used to access action output |
||||
id: sign_app |
id: sign_app |
||||
with: |
with: |
||||
@ -46,7 +47,8 @@ jobs: |
|||||
uses: actions/upload-artifact@v2 |
uses: actions/upload-artifact@v2 |
||||
with: |
with: |
||||
name: barinsta_pre-release_${{ steps.date.outputs.date }} |
name: barinsta_pre-release_${{ steps.date.outputs.date }} |
||||
path: ${{steps.sign_app.outputs.signedReleaseFile}} |
|
||||
|
# path: ${{steps.sign_app.outputs.signedReleaseFile}} |
||||
|
path: app/build/outputs/apk/github/release/*-signed.apk |
||||
|
|
||||
# Send success notification |
# Send success notification |
||||
- name: Send success Telegram notification |
- name: Send success Telegram notification |
||||
@ -56,7 +58,8 @@ jobs: |
|||||
to: ${{ secrets.TELEGRAM_BUILDS_CHANNEL_TO }} |
to: ${{ secrets.TELEGRAM_BUILDS_CHANNEL_TO }} |
||||
token: ${{ secrets.TELEGRAM_BUILDS_BOT_TOKEN }} |
token: ${{ secrets.TELEGRAM_BUILDS_BOT_TOKEN }} |
||||
message: "${{ github.workflow }} ${{ github.job }} #${{ github.run_number }} completed successfully.\nURL: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" |
message: "${{ github.workflow }} ${{ github.job }} #${{ github.run_number }} completed successfully.\nURL: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" |
||||
document: ${{steps.sign_app.outputs.signedReleaseFile}} |
|
||||
|
# document: ${{steps.sign_app.outputs.signedReleaseFile}} |
||||
|
document: app/build/outputs/apk/github/release/*-signed.apk |
||||
|
|
||||
# Send failure notification |
# Send failure notification |
||||
- name: Send failure Telegram notification |
- name: Send failure Telegram notification |
||||
|
@ -1,6 +1,6 @@ |
|||||
<?xml version="1.0" encoding="UTF-8"?> |
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
<project version="4"> |
||||
<component name="CompilerConfiguration"> |
<component name="CompilerConfiguration"> |
||||
<bytecodeTargetLevel target="1.8" /> |
|
||||
|
<bytecodeTargetLevel target="11" /> |
||||
</component> |
</component> |
||||
</project> |
</project> |
@ -0,0 +1,165 @@ |
|||||
|
package awais.instagrabber.customviews; |
||||
|
|
||||
|
import android.content.Context; |
||||
|
import android.util.AttributeSet; |
||||
|
import android.util.Log; |
||||
|
import android.view.ViewGroup; |
||||
|
|
||||
|
import androidx.annotation.NonNull; |
||||
|
import androidx.annotation.Nullable; |
||||
|
import androidx.appcompat.widget.AppCompatTextView; |
||||
|
import androidx.transition.ChangeBounds; |
||||
|
import androidx.transition.Transition; |
||||
|
import androidx.transition.TransitionManager; |
||||
|
import androidx.transition.TransitionSet; |
||||
|
|
||||
|
import java.time.Duration; |
||||
|
|
||||
|
import awais.instagrabber.customviews.helpers.ChangeText; |
||||
|
import awais.instagrabber.utils.NumberUtils; |
||||
|
|
||||
|
public class FormattedNumberTextView extends AppCompatTextView { |
||||
|
private static final String TAG = FormattedNumberTextView.class.getSimpleName(); |
||||
|
private static final Transition TRANSITION; |
||||
|
|
||||
|
private long number = Long.MIN_VALUE; |
||||
|
private boolean showAbbreviation = true; |
||||
|
private boolean animateChanges = false; |
||||
|
private boolean toggleOnClick = true; |
||||
|
private boolean autoToggleToAbbreviation = true; |
||||
|
private long autoToggleTimeoutMs = Duration.ofSeconds(2).toMillis(); |
||||
|
private boolean initDone = false; |
||||
|
|
||||
|
static { |
||||
|
final TransitionSet transitionSet = new TransitionSet(); |
||||
|
final ChangeText changeText = new ChangeText().setChangeBehavior(ChangeText.CHANGE_BEHAVIOR_OUT_IN); |
||||
|
transitionSet.addTransition(changeText).addTransition(new ChangeBounds()); |
||||
|
TRANSITION = transitionSet; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public FormattedNumberTextView(@NonNull final Context context) { |
||||
|
super(context); |
||||
|
init(); |
||||
|
} |
||||
|
|
||||
|
public FormattedNumberTextView(@NonNull final Context context, @Nullable final AttributeSet attrs) { |
||||
|
super(context, attrs); |
||||
|
init(); |
||||
|
} |
||||
|
|
||||
|
public FormattedNumberTextView(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) { |
||||
|
super(context, attrs, defStyleAttr); |
||||
|
init(); |
||||
|
} |
||||
|
|
||||
|
private void init() { |
||||
|
if (initDone) return; |
||||
|
setupClickToggle(); |
||||
|
initDone = true; |
||||
|
} |
||||
|
|
||||
|
private void setupClickToggle() { |
||||
|
setOnClickListener(null); |
||||
|
} |
||||
|
|
||||
|
private OnClickListener getWrappedClickListener(@Nullable final OnClickListener l) { |
||||
|
if (!toggleOnClick) { |
||||
|
return l; |
||||
|
} |
||||
|
return v -> { |
||||
|
toggleAbbreviation(); |
||||
|
if (l != null) { |
||||
|
l.onClick(this); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public void setNumber(final long number) { |
||||
|
if (this.number == number) return; |
||||
|
this.number = number; |
||||
|
format(); |
||||
|
} |
||||
|
|
||||
|
public void clearNumber() { |
||||
|
if (number == Long.MIN_VALUE) return; |
||||
|
number = Long.MIN_VALUE; |
||||
|
format(); |
||||
|
} |
||||
|
|
||||
|
public void setShowAbbreviation(final boolean showAbbreviation) { |
||||
|
if (this.showAbbreviation && showAbbreviation) return; |
||||
|
this.showAbbreviation = showAbbreviation; |
||||
|
format(); |
||||
|
} |
||||
|
|
||||
|
public boolean isShowAbbreviation() { |
||||
|
return showAbbreviation; |
||||
|
} |
||||
|
|
||||
|
private void toggleAbbreviation() { |
||||
|
if (number == Long.MIN_VALUE) return; |
||||
|
setShowAbbreviation(!showAbbreviation); |
||||
|
} |
||||
|
|
||||
|
public void setToggleOnClick(final boolean toggleOnClick) { |
||||
|
this.toggleOnClick = toggleOnClick; |
||||
|
} |
||||
|
|
||||
|
public boolean isToggleOnClick() { |
||||
|
return toggleOnClick; |
||||
|
} |
||||
|
|
||||
|
public void setAutoToggleToAbbreviation(final boolean autoToggleToAbbreviation) { |
||||
|
this.autoToggleToAbbreviation = autoToggleToAbbreviation; |
||||
|
} |
||||
|
|
||||
|
public boolean isAutoToggleToAbbreviation() { |
||||
|
return autoToggleToAbbreviation; |
||||
|
} |
||||
|
|
||||
|
public void setAutoToggleTimeoutMs(final long autoToggleTimeoutMs) { |
||||
|
this.autoToggleTimeoutMs = autoToggleTimeoutMs; |
||||
|
} |
||||
|
|
||||
|
public long getAutoToggleTimeoutMs() { |
||||
|
return autoToggleTimeoutMs; |
||||
|
} |
||||
|
|
||||
|
public void setAnimateChanges(final boolean animateChanges) { |
||||
|
this.animateChanges = animateChanges; |
||||
|
} |
||||
|
|
||||
|
public boolean isAnimateChanges() { |
||||
|
return animateChanges; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void setOnClickListener(@Nullable final OnClickListener l) { |
||||
|
super.setOnClickListener(getWrappedClickListener(l)); |
||||
|
} |
||||
|
|
||||
|
private void format() { |
||||
|
post(() -> { |
||||
|
if (animateChanges) { |
||||
|
try { |
||||
|
TransitionManager.beginDelayedTransition((ViewGroup) getParent(), TRANSITION); |
||||
|
} catch (Exception e) { |
||||
|
Log.e(TAG, "format: ", e); |
||||
|
} |
||||
|
} |
||||
|
if (number == Long.MIN_VALUE) { |
||||
|
setText(null); |
||||
|
return; |
||||
|
} |
||||
|
if (showAbbreviation) { |
||||
|
setText(NumberUtils.abbreviate(number)); |
||||
|
return; |
||||
|
} |
||||
|
setText(String.valueOf(number)); |
||||
|
if (autoToggleToAbbreviation) { |
||||
|
getHandler().postDelayed(() -> setShowAbbreviation(true), autoToggleTimeoutMs); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -0,0 +1,75 @@ |
|||||
|
package awais.instagrabber.customviews; |
||||
|
|
||||
|
import android.content.Context; |
||||
|
import android.os.Bundle; |
||||
|
|
||||
|
import androidx.annotation.NonNull; |
||||
|
import androidx.annotation.Nullable; |
||||
|
import androidx.fragment.app.FragmentManager; |
||||
|
import androidx.navigation.NavDestination; |
||||
|
import androidx.navigation.NavOptions; |
||||
|
import androidx.navigation.Navigator; |
||||
|
import androidx.navigation.fragment.FragmentNavigator; |
||||
|
|
||||
|
import awais.instagrabber.R; |
||||
|
|
||||
|
@Navigator.Name("fragment") |
||||
|
public class FragmentNavigatorWithDefaultAnimations extends FragmentNavigator { |
||||
|
|
||||
|
private final NavOptions emptyNavOptions = new NavOptions.Builder().build(); |
||||
|
// private final NavOptions defaultNavOptions = new NavOptions.Builder() |
||||
|
// .setEnterAnim(R.animator.nav_default_enter_anim) |
||||
|
// .setExitAnim(R.animator.nav_default_exit_anim) |
||||
|
// .setPopEnterAnim(R.animator.nav_default_pop_enter_anim) |
||||
|
// .setPopExitAnim(R.animator.nav_default_pop_exit_anim) |
||||
|
// .build(); |
||||
|
|
||||
|
private final NavOptions defaultNavOptions = new NavOptions.Builder() |
||||
|
.setEnterAnim(R.anim.slide_in_right) |
||||
|
.setExitAnim(R.anim.slide_out_left) |
||||
|
.setPopEnterAnim(android.R.anim.slide_in_left) |
||||
|
.setPopExitAnim(android.R.anim.slide_out_right) |
||||
|
.build(); |
||||
|
|
||||
|
public FragmentNavigatorWithDefaultAnimations(@NonNull final Context context, |
||||
|
@NonNull final FragmentManager manager, |
||||
|
final int containerId) { |
||||
|
super(context, manager, containerId); |
||||
|
} |
||||
|
|
||||
|
@Nullable |
||||
|
@Override |
||||
|
public NavDestination navigate(@NonNull final Destination destination, |
||||
|
@Nullable final Bundle args, |
||||
|
@Nullable final NavOptions navOptions, |
||||
|
@Nullable final Navigator.Extras navigatorExtras) { |
||||
|
// this will try to fill in empty animations with defaults when no shared element transitions are set |
||||
|
// https://developer.android.com/guide/navigation/navigation-animate-transitions#shared-element |
||||
|
final boolean shouldUseTransitionsInstead = navigatorExtras != null; |
||||
|
final NavOptions navOptions1 = shouldUseTransitionsInstead ? navOptions : fillEmptyAnimationsWithDefaults(navOptions); |
||||
|
return super.navigate(destination, args, navOptions1, navigatorExtras); |
||||
|
} |
||||
|
|
||||
|
private NavOptions fillEmptyAnimationsWithDefaults(@Nullable final NavOptions navOptions) { |
||||
|
if (navOptions == null) { |
||||
|
return defaultNavOptions; |
||||
|
} |
||||
|
return copyNavOptionsWithDefaultAnimations(navOptions); |
||||
|
} |
||||
|
|
||||
|
@NonNull |
||||
|
private NavOptions copyNavOptionsWithDefaultAnimations(@NonNull final NavOptions navOptions) { |
||||
|
return new NavOptions.Builder() |
||||
|
.setLaunchSingleTop(navOptions.shouldLaunchSingleTop()) |
||||
|
.setPopUpTo(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive()) |
||||
|
.setEnterAnim(navOptions.getEnterAnim() == emptyNavOptions.getEnterAnim() |
||||
|
? defaultNavOptions.getEnterAnim() : navOptions.getEnterAnim()) |
||||
|
.setExitAnim(navOptions.getExitAnim() == emptyNavOptions.getExitAnim() |
||||
|
? defaultNavOptions.getExitAnim() : navOptions.getExitAnim()) |
||||
|
.setPopEnterAnim(navOptions.getPopEnterAnim() == emptyNavOptions.getPopEnterAnim() |
||||
|
? defaultNavOptions.getPopEnterAnim() : navOptions.getPopEnterAnim()) |
||||
|
.setPopExitAnim(navOptions.getPopExitAnim() == emptyNavOptions.getPopExitAnim() |
||||
|
? defaultNavOptions.getPopExitAnim() : navOptions.getPopExitAnim()) |
||||
|
.build(); |
||||
|
} |
||||
|
} |
@ -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); |
||||
|
} |
||||
|
} |
||||
|
|
@ -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; |
||||
|
} |
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
@ -0,0 +1,60 @@ |
|||||
|
package awais.instagrabber.customviews; |
||||
|
|
||||
|
import android.os.Bundle; |
||||
|
|
||||
|
import androidx.annotation.NavigationRes; |
||||
|
import androidx.annotation.NonNull; |
||||
|
import androidx.annotation.Nullable; |
||||
|
import androidx.navigation.NavController; |
||||
|
import androidx.navigation.Navigator; |
||||
|
import androidx.navigation.fragment.FragmentNavigator; |
||||
|
import androidx.navigation.fragment.NavHostFragment; |
||||
|
|
||||
|
public class NavHostFragmentWithDefaultAnimations extends NavHostFragment { |
||||
|
private static final String KEY_GRAPH_ID = "android-support-nav:fragment:graphId"; |
||||
|
private static final String KEY_START_DESTINATION_ARGS = |
||||
|
"android-support-nav:fragment:startDestinationArgs"; |
||||
|
private static final String KEY_NAV_CONTROLLER_STATE = |
||||
|
"android-support-nav:fragment:navControllerState"; |
||||
|
private static final String KEY_DEFAULT_NAV_HOST = "android-support-nav:fragment:defaultHost"; |
||||
|
|
||||
|
@NonNull |
||||
|
public static NavHostFragment create(@NavigationRes int graphResId) { |
||||
|
return create(graphResId, null); |
||||
|
} |
||||
|
|
||||
|
@NonNull |
||||
|
public static NavHostFragment create(@NavigationRes int graphResId, |
||||
|
@Nullable Bundle startDestinationArgs) { |
||||
|
Bundle b = null; |
||||
|
if (graphResId != 0) { |
||||
|
b = new Bundle(); |
||||
|
b.putInt(KEY_GRAPH_ID, graphResId); |
||||
|
} |
||||
|
if (startDestinationArgs != null) { |
||||
|
if (b == null) { |
||||
|
b = new Bundle(); |
||||
|
} |
||||
|
b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs); |
||||
|
} |
||||
|
|
||||
|
final NavHostFragmentWithDefaultAnimations result = new NavHostFragmentWithDefaultAnimations(); |
||||
|
if (b != null) { |
||||
|
result.setArguments(b); |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
@NonNull |
||||
|
@Override |
||||
|
protected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() { |
||||
|
return new FragmentNavigatorWithDefaultAnimations(requireContext(), getChildFragmentManager(), getId()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void onCreateNavController(@NonNull final NavController navController) { |
||||
|
super.onCreateNavController(navController); |
||||
|
navController.getNavigatorProvider() |
||||
|
.addNavigator(new FragmentNavigatorWithDefaultAnimations(requireContext(), getChildFragmentManager(), getId())); |
||||
|
} |
||||
|
} |
@ -0,0 +1,100 @@ |
|||||
|
package awais.instagrabber.customviews.emoji; |
||||
|
|
||||
|
import android.app.Dialog; |
||||
|
import android.content.Context; |
||||
|
import android.os.Bundle; |
||||
|
import android.view.LayoutInflater; |
||||
|
import android.view.View; |
||||
|
import android.view.ViewGroup; |
||||
|
|
||||
|
import androidx.annotation.NonNull; |
||||
|
import androidx.annotation.Nullable; |
||||
|
import androidx.fragment.app.DialogFragment; |
||||
|
import androidx.fragment.app.Fragment; |
||||
|
import androidx.recyclerview.widget.GridLayoutManager; |
||||
|
import androidx.recyclerview.widget.RecyclerView; |
||||
|
|
||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog; |
||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment; |
||||
|
|
||||
|
import awais.instagrabber.R; |
||||
|
import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration; |
||||
|
import awais.instagrabber.utils.Utils; |
||||
|
|
||||
|
public class EmojiBottomSheetDialog extends BottomSheetDialogFragment { |
||||
|
public static final String TAG = EmojiBottomSheetDialog.class.getSimpleName(); |
||||
|
|
||||
|
private RecyclerView grid; |
||||
|
private EmojiPicker.OnEmojiClickListener callback; |
||||
|
|
||||
|
@NonNull |
||||
|
public static EmojiBottomSheetDialog newInstance() { |
||||
|
// Bundle args = new Bundle(); |
||||
|
// fragment.setArguments(args); |
||||
|
return new EmojiBottomSheetDialog(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onCreate(@Nullable final Bundle savedInstanceState) { |
||||
|
super.onCreate(savedInstanceState); |
||||
|
setStyle(DialogFragment.STYLE_NORMAL, R.style.ThemeOverlay_Rounded_BottomSheetDialog); |
||||
|
} |
||||
|
|
||||
|
@Nullable |
||||
|
@Override |
||||
|
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { |
||||
|
final Context context = getContext(); |
||||
|
if (context == null) return null; |
||||
|
grid = new RecyclerView(context); |
||||
|
return grid; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { |
||||
|
init(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onStart() { |
||||
|
super.onStart(); |
||||
|
final Dialog dialog = getDialog(); |
||||
|
if (dialog == null) return; |
||||
|
final BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialog; |
||||
|
final View bottomSheetInternal = bottomSheetDialog.findViewById(com.google.android.material.R.id.design_bottom_sheet); |
||||
|
if (bottomSheetInternal == null) return; |
||||
|
bottomSheetInternal.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; |
||||
|
bottomSheetInternal.requestLayout(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onAttach(@NonNull final Context context) { |
||||
|
super.onAttach(context); |
||||
|
final Fragment parentFragment = getParentFragment(); |
||||
|
if (parentFragment instanceof EmojiPicker.OnEmojiClickListener) { |
||||
|
callback = (EmojiPicker.OnEmojiClickListener) parentFragment; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onDestroyView() { |
||||
|
grid = null; |
||||
|
super.onDestroyView(); |
||||
|
} |
||||
|
|
||||
|
private void init() { |
||||
|
final Context context = getContext(); |
||||
|
if (context == null) return; |
||||
|
final GridLayoutManager gridLayoutManager = new GridLayoutManager(context, 9); |
||||
|
grid.setLayoutManager(gridLayoutManager); |
||||
|
grid.setHasFixedSize(true); |
||||
|
grid.setClipToPadding(false); |
||||
|
grid.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(8))); |
||||
|
final EmojiGridAdapter adapter = new EmojiGridAdapter(null, (view, emoji) -> { |
||||
|
if (callback != null) { |
||||
|
callback.onClick(view, emoji); |
||||
|
} |
||||
|
dismiss(); |
||||
|
}, null); |
||||
|
grid.setAdapter(adapter); |
||||
|
} |
||||
|
} |
@ -1,163 +0,0 @@ |
|||||
package awais.instagrabber.customviews.emoji; |
|
||||
|
|
||||
import android.content.Context; |
|
||||
import android.graphics.Rect; |
|
||||
import android.view.Gravity; |
|
||||
import android.view.View; |
|
||||
import android.view.WindowManager.LayoutParams; |
|
||||
import android.widget.PopupWindow; |
|
||||
|
|
||||
import awais.instagrabber.R; |
|
||||
import awais.instagrabber.customviews.emoji.EmojiPicker.OnBackspaceClickListener; |
|
||||
import awais.instagrabber.customviews.emoji.EmojiPicker.OnEmojiClickListener; |
|
||||
import awais.instagrabber.utils.Utils; |
|
||||
|
|
||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; |
|
||||
|
|
||||
/** |
|
||||
* https://stackoverflow.com/a/33897583/1436766 |
|
||||
*/ |
|
||||
public class EmojiPopupWindow extends PopupWindow { |
|
||||
|
|
||||
private int keyBoardHeight = 0; |
|
||||
private Boolean pendingOpen = false; |
|
||||
private Boolean isOpened = false; |
|
||||
private final View rootView; |
|
||||
private final Context context; |
|
||||
private final OnEmojiClickListener onEmojiClickListener; |
|
||||
private final OnBackspaceClickListener onBackspaceClickListener; |
|
||||
|
|
||||
private OnSoftKeyboardOpenCloseListener onSoftKeyboardOpenCloseListener; |
|
||||
|
|
||||
|
|
||||
/** |
|
||||
* Constructor |
|
||||
* |
|
||||
* @param rootView The top most layout in your view hierarchy. The difference of this view and the screen height will be used to calculate the keyboard height. |
|
||||
*/ |
|
||||
public EmojiPopupWindow(final View rootView, |
|
||||
final OnEmojiClickListener onEmojiClickListener, |
|
||||
final OnBackspaceClickListener onBackspaceClickListener) { |
|
||||
super(rootView.getContext()); |
|
||||
this.rootView = rootView; |
|
||||
this.context = rootView.getContext(); |
|
||||
this.onEmojiClickListener = onEmojiClickListener; |
|
||||
this.onBackspaceClickListener = onBackspaceClickListener; |
|
||||
View customView = createCustomView(); |
|
||||
setContentView(customView); |
|
||||
setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); |
|
||||
//default size |
|
||||
setSize((int) context.getResources().getDimension(R.dimen.keyboard_height), MATCH_PARENT); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Set the listener for the event of keyboard opening or closing. |
|
||||
*/ |
|
||||
public void setOnSoftKeyboardOpenCloseListener(OnSoftKeyboardOpenCloseListener listener) { |
|
||||
this.onSoftKeyboardOpenCloseListener = listener; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Use this function to show the emoji popup. |
|
||||
* NOTE: Since, the soft keyboard sizes are variable on different android devices, the |
|
||||
* library needs you to open the soft keyboard atleast once before calling this function. |
|
||||
* If that is not possible see showAtBottomPending() function. |
|
||||
*/ |
|
||||
public void showAtBottom() { |
|
||||
showAtLocation(rootView, Gravity.BOTTOM, 0, 0); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Use this function when the soft keyboard has not been opened yet. This |
|
||||
* will show the emoji popup after the keyboard is up next time. |
|
||||
* Generally, you will be calling InputMethodManager.showSoftInput function after |
|
||||
* calling this function. |
|
||||
*/ |
|
||||
public void showAtBottomPending() { |
|
||||
if (isKeyBoardOpen()) |
|
||||
showAtBottom(); |
|
||||
else |
|
||||
pendingOpen = true; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @return Returns true if the soft keyboard is open, false otherwise. |
|
||||
*/ |
|
||||
public Boolean isKeyBoardOpen() { |
|
||||
return isOpened; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Dismiss the popup |
|
||||
*/ |
|
||||
@Override |
|
||||
public void dismiss() { |
|
||||
super.dismiss(); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Call this function to resize the emoji popup according to your soft keyboard size |
|
||||
*/ |
|
||||
public void setSizeForSoftKeyboard() { |
|
||||
rootView.getViewTreeObserver().addOnGlobalLayoutListener(() -> { |
|
||||
Rect r = new Rect(); |
|
||||
rootView.getWindowVisibleDisplayFrame(r); |
|
||||
|
|
||||
int screenHeight = getUsableScreenHeight(); |
|
||||
int heightDifference = screenHeight - (r.bottom - r.top); |
|
||||
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); |
|
||||
if (resourceId > 0) { |
|
||||
heightDifference -= context.getResources() |
|
||||
.getDimensionPixelSize(resourceId); |
|
||||
} |
|
||||
if (heightDifference > 100) { |
|
||||
keyBoardHeight = heightDifference; |
|
||||
setSize(MATCH_PARENT, keyBoardHeight); |
|
||||
if (!isOpened) { |
|
||||
if (onSoftKeyboardOpenCloseListener != null) |
|
||||
onSoftKeyboardOpenCloseListener.onKeyboardOpen(keyBoardHeight); |
|
||||
} |
|
||||
isOpened = true; |
|
||||
if (pendingOpen) { |
|
||||
showAtBottom(); |
|
||||
pendingOpen = false; |
|
||||
} |
|
||||
} else { |
|
||||
isOpened = false; |
|
||||
if (onSoftKeyboardOpenCloseListener != null) |
|
||||
onSoftKeyboardOpenCloseListener.onKeyboardClose(); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private int getUsableScreenHeight() { |
|
||||
return Utils.displayMetrics.heightPixels; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Manually set the popup window size |
|
||||
* |
|
||||
* @param width Width of the popup |
|
||||
* @param height Height of the popup |
|
||||
*/ |
|
||||
public void setSize(int width, int height) { |
|
||||
setWidth(width); |
|
||||
setHeight(height); |
|
||||
} |
|
||||
|
|
||||
private View createCustomView() { |
|
||||
final EmojiPicker emojiPicker = new EmojiPicker(context); |
|
||||
final LayoutParams layoutParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT); |
|
||||
emojiPicker.setLayoutParams(layoutParams); |
|
||||
emojiPicker.init(rootView, onEmojiClickListener, onBackspaceClickListener); |
|
||||
return emojiPicker; |
|
||||
} |
|
||||
|
|
||||
|
|
||||
public interface OnSoftKeyboardOpenCloseListener { |
|
||||
void onKeyboardOpen(int keyBoardHeight); |
|
||||
|
|
||||
void onKeyboardClose(); |
|
||||
} |
|
||||
} |
|
||||
|
|
@ -0,0 +1,320 @@ |
|||||
|
package awais.instagrabber.customviews.helpers; |
||||
|
|
||||
|
/* |
||||
|
* Copyright (C) 2013 The Android Open Source Project |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
|
||||
|
import android.animation.Animator; |
||||
|
import android.animation.AnimatorListenerAdapter; |
||||
|
import android.animation.AnimatorSet; |
||||
|
import android.animation.ValueAnimator; |
||||
|
import android.graphics.Color; |
||||
|
import android.util.Log; |
||||
|
import android.view.ViewGroup; |
||||
|
import android.widget.EditText; |
||||
|
import android.widget.TextView; |
||||
|
|
||||
|
import androidx.annotation.NonNull; |
||||
|
import androidx.transition.Transition; |
||||
|
import androidx.transition.TransitionListenerAdapter; |
||||
|
import androidx.transition.TransitionValues; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
import java.util.Objects; |
||||
|
|
||||
|
import awais.instagrabber.BuildConfig; |
||||
|
|
||||
|
/** |
||||
|
* This transition tracks changes to the text in TextView targets. If the text |
||||
|
* changes between the start and end scenes, the transition ensures that the |
||||
|
* starting text stays until the transition ends, at which point it changes |
||||
|
* to the end text. This is useful in situations where you want to resize a |
||||
|
* text view to its new size before displaying the text that goes there. |
||||
|
*/ |
||||
|
public class ChangeText extends Transition { |
||||
|
private static final String LOG_TAG = "TextChange"; |
||||
|
private static final String PROPNAME_TEXT = "android:textchange:text"; |
||||
|
private static final String PROPNAME_TEXT_SELECTION_START = |
||||
|
"android:textchange:textSelectionStart"; |
||||
|
private static final String PROPNAME_TEXT_SELECTION_END = |
||||
|
"android:textchange:textSelectionEnd"; |
||||
|
private static final String PROPNAME_TEXT_COLOR = "android:textchange:textColor"; |
||||
|
private int mChangeBehavior = CHANGE_BEHAVIOR_KEEP; |
||||
|
private boolean crossFade; |
||||
|
/** |
||||
|
* Flag specifying that the text in affected/changing TextView targets will keep |
||||
|
* their original text during the transition, setting it to the final text when |
||||
|
* the transition ends. This is the default behavior. |
||||
|
* |
||||
|
* @see #setChangeBehavior(int) |
||||
|
*/ |
||||
|
public static final int CHANGE_BEHAVIOR_KEEP = 0; |
||||
|
/** |
||||
|
* Flag specifying that the text changing animation should first fade |
||||
|
* out the original text completely. The new text is set on the target |
||||
|
* view at the end of the fade-out animation. This transition is typically |
||||
|
* used with a later {@link #CHANGE_BEHAVIOR_IN} transition, allowing more |
||||
|
* flexibility than the {@link #CHANGE_BEHAVIOR_OUT_IN} by allowing other |
||||
|
* transitions to be run sequentially or in parallel with these fades. |
||||
|
* |
||||
|
* @see #setChangeBehavior(int) |
||||
|
*/ |
||||
|
public static final int CHANGE_BEHAVIOR_OUT = 1; |
||||
|
/** |
||||
|
* Flag specifying that the text changing animation should fade in the |
||||
|
* end text into the affected target view(s). This transition is typically |
||||
|
* used in conjunction with an earlier {@link #CHANGE_BEHAVIOR_OUT} |
||||
|
* transition, possibly with other transitions running as well, such as |
||||
|
* a sequence to fade out, then resize the view, then fade in. |
||||
|
* |
||||
|
* @see #setChangeBehavior(int) |
||||
|
*/ |
||||
|
public static final int CHANGE_BEHAVIOR_IN = 2; |
||||
|
/** |
||||
|
* Flag specifying that the text changing animation should first fade |
||||
|
* out the original text completely and then fade in the |
||||
|
* new text. |
||||
|
* |
||||
|
* @see #setChangeBehavior(int) |
||||
|
*/ |
||||
|
public static final int CHANGE_BEHAVIOR_OUT_IN = 3; |
||||
|
private static final String[] sTransitionProperties = { |
||||
|
PROPNAME_TEXT, |
||||
|
PROPNAME_TEXT_SELECTION_START, |
||||
|
PROPNAME_TEXT_SELECTION_END |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* Sets the type of changing animation that will be run, one of |
||||
|
* {@link #CHANGE_BEHAVIOR_KEEP}, {@link #CHANGE_BEHAVIOR_OUT}, |
||||
|
* {@link #CHANGE_BEHAVIOR_IN}, and {@link #CHANGE_BEHAVIOR_OUT_IN}. |
||||
|
* |
||||
|
* @param changeBehavior The type of fading animation to use when this |
||||
|
* transition is run. |
||||
|
* @return this textChange object. |
||||
|
*/ |
||||
|
public ChangeText setChangeBehavior(int changeBehavior) { |
||||
|
if (changeBehavior >= CHANGE_BEHAVIOR_KEEP && changeBehavior <= CHANGE_BEHAVIOR_OUT_IN) { |
||||
|
mChangeBehavior = changeBehavior; |
||||
|
} |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
public ChangeText setCrossFade(final boolean crossFade) { |
||||
|
this.crossFade = crossFade; |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String[] getTransitionProperties() { |
||||
|
return sTransitionProperties; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns the type of changing animation that will be run. |
||||
|
* |
||||
|
* @return either {@link #CHANGE_BEHAVIOR_KEEP}, {@link #CHANGE_BEHAVIOR_OUT}, |
||||
|
* {@link #CHANGE_BEHAVIOR_IN}, or {@link #CHANGE_BEHAVIOR_OUT_IN}. |
||||
|
*/ |
||||
|
public int getChangeBehavior() { |
||||
|
return mChangeBehavior; |
||||
|
} |
||||
|
|
||||
|
private void captureValues(TransitionValues transitionValues) { |
||||
|
if (transitionValues.view instanceof TextView) { |
||||
|
TextView textview = (TextView) transitionValues.view; |
||||
|
transitionValues.values.put(PROPNAME_TEXT, textview.getText()); |
||||
|
if (textview instanceof EditText) { |
||||
|
transitionValues.values.put(PROPNAME_TEXT_SELECTION_START, |
||||
|
textview.getSelectionStart()); |
||||
|
transitionValues.values.put(PROPNAME_TEXT_SELECTION_END, |
||||
|
textview.getSelectionEnd()); |
||||
|
} |
||||
|
if (mChangeBehavior > CHANGE_BEHAVIOR_KEEP) { |
||||
|
transitionValues.values.put(PROPNAME_TEXT_COLOR, textview.getCurrentTextColor()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void captureStartValues(@NonNull TransitionValues transitionValues) { |
||||
|
captureValues(transitionValues); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void captureEndValues(@NonNull TransitionValues transitionValues) { |
||||
|
captureValues(transitionValues); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Animator createAnimator(@NonNull ViewGroup sceneRoot, TransitionValues startValues, |
||||
|
TransitionValues endValues) { |
||||
|
if (startValues == null || endValues == null || |
||||
|
!(startValues.view instanceof TextView) || !(endValues.view instanceof TextView)) { |
||||
|
return null; |
||||
|
} |
||||
|
final TextView view = (TextView) endValues.view; |
||||
|
Map<String, Object> startVals = startValues.values; |
||||
|
Map<String, Object> endVals = endValues.values; |
||||
|
final CharSequence startText = startVals.get(PROPNAME_TEXT) != null ? |
||||
|
(CharSequence) startVals.get(PROPNAME_TEXT) : ""; |
||||
|
final CharSequence endText = endVals.get(PROPNAME_TEXT) != null ? |
||||
|
(CharSequence) endVals.get(PROPNAME_TEXT) : ""; |
||||
|
final int startSelectionStart, startSelectionEnd, endSelectionStart, endSelectionEnd; |
||||
|
if (view instanceof EditText) { |
||||
|
startSelectionStart = startVals.get(PROPNAME_TEXT_SELECTION_START) != null ? |
||||
|
(Integer) startVals.get(PROPNAME_TEXT_SELECTION_START) : -1; |
||||
|
startSelectionEnd = startVals.get(PROPNAME_TEXT_SELECTION_END) != null ? |
||||
|
(Integer) startVals.get(PROPNAME_TEXT_SELECTION_END) : startSelectionStart; |
||||
|
endSelectionStart = endVals.get(PROPNAME_TEXT_SELECTION_START) != null ? |
||||
|
(Integer) endVals.get(PROPNAME_TEXT_SELECTION_START) : -1; |
||||
|
endSelectionEnd = endVals.get(PROPNAME_TEXT_SELECTION_END) != null ? |
||||
|
(Integer) endVals.get(PROPNAME_TEXT_SELECTION_END) : endSelectionStart; |
||||
|
} else { |
||||
|
startSelectionStart = startSelectionEnd = endSelectionStart = endSelectionEnd = -1; |
||||
|
} |
||||
|
if (!Objects.equals(startText, endText)) { |
||||
|
final int startColor; |
||||
|
final int endColor; |
||||
|
if (mChangeBehavior != CHANGE_BEHAVIOR_IN) { |
||||
|
view.setText(startText); |
||||
|
if (view instanceof EditText) { |
||||
|
setSelection(((EditText) view), startSelectionStart, startSelectionEnd); |
||||
|
} |
||||
|
} |
||||
|
Animator anim; |
||||
|
if (mChangeBehavior == CHANGE_BEHAVIOR_KEEP) { |
||||
|
startColor = endColor = 0; |
||||
|
anim = ValueAnimator.ofFloat(0, 1); |
||||
|
anim.addListener(new AnimatorListenerAdapter() { |
||||
|
@Override |
||||
|
public void onAnimationEnd(Animator animation) { |
||||
|
if (Objects.equals(startText, view.getText())) { |
||||
|
// Only set if it hasn't been changed since anim started |
||||
|
view.setText(endText); |
||||
|
if (view instanceof EditText) { |
||||
|
setSelection(((EditText) view), endSelectionStart, endSelectionEnd); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} else { |
||||
|
startColor = (Integer) startVals.get(PROPNAME_TEXT_COLOR); |
||||
|
endColor = (Integer) endVals.get(PROPNAME_TEXT_COLOR); |
||||
|
// Fade out start text |
||||
|
ValueAnimator outAnim = null, inAnim = null; |
||||
|
if (mChangeBehavior == CHANGE_BEHAVIOR_OUT_IN || |
||||
|
mChangeBehavior == CHANGE_BEHAVIOR_OUT) { |
||||
|
outAnim = ValueAnimator.ofInt(Color.alpha(startColor), 0); |
||||
|
outAnim.addUpdateListener(animation -> { |
||||
|
int currAlpha = (Integer) animation.getAnimatedValue(); |
||||
|
view.setTextColor(currAlpha << 24 | startColor & 0xffffff); |
||||
|
}); |
||||
|
outAnim.addListener(new AnimatorListenerAdapter() { |
||||
|
@Override |
||||
|
public void onAnimationEnd(Animator animation) { |
||||
|
if (Objects.equals(startText, view.getText())) { |
||||
|
// Only set if it hasn't been changed since anim started |
||||
|
view.setText(endText); |
||||
|
if (view instanceof EditText) { |
||||
|
setSelection(((EditText) view), endSelectionStart, |
||||
|
endSelectionEnd); |
||||
|
} |
||||
|
} |
||||
|
// restore opaque alpha and correct end color |
||||
|
view.setTextColor(endColor); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
if (mChangeBehavior == CHANGE_BEHAVIOR_OUT_IN || |
||||
|
mChangeBehavior == CHANGE_BEHAVIOR_IN) { |
||||
|
inAnim = ValueAnimator.ofInt(0, Color.alpha(endColor)); |
||||
|
inAnim.addUpdateListener(animation -> { |
||||
|
int currAlpha = (Integer) animation.getAnimatedValue(); |
||||
|
view.setTextColor(currAlpha << 24 | endColor & 0xffffff); |
||||
|
}); |
||||
|
inAnim.addListener(new AnimatorListenerAdapter() { |
||||
|
@Override |
||||
|
public void onAnimationCancel(Animator animation) { |
||||
|
// restore opaque alpha and correct end color |
||||
|
view.setTextColor(endColor); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
if (outAnim != null && inAnim != null) { |
||||
|
anim = new AnimatorSet(); |
||||
|
final AnimatorSet animatorSet = (AnimatorSet) anim; |
||||
|
if (crossFade) { |
||||
|
animatorSet.playTogether(outAnim, inAnim); |
||||
|
} else { |
||||
|
animatorSet.playSequentially(outAnim, inAnim); |
||||
|
} |
||||
|
} else if (outAnim != null) { |
||||
|
anim = outAnim; |
||||
|
} else { |
||||
|
// Must be an in-only animation |
||||
|
anim = inAnim; |
||||
|
} |
||||
|
} |
||||
|
TransitionListener transitionListener = new TransitionListenerAdapter() { |
||||
|
int mPausedColor = 0; |
||||
|
|
||||
|
@Override |
||||
|
public void onTransitionPause(@NonNull Transition transition) { |
||||
|
if (mChangeBehavior != CHANGE_BEHAVIOR_IN) { |
||||
|
view.setText(endText); |
||||
|
if (view instanceof EditText) { |
||||
|
setSelection(((EditText) view), endSelectionStart, endSelectionEnd); |
||||
|
} |
||||
|
} |
||||
|
if (mChangeBehavior > CHANGE_BEHAVIOR_KEEP) { |
||||
|
mPausedColor = view.getCurrentTextColor(); |
||||
|
view.setTextColor(endColor); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onTransitionResume(@NonNull Transition transition) { |
||||
|
if (mChangeBehavior != CHANGE_BEHAVIOR_IN) { |
||||
|
view.setText(startText); |
||||
|
if (view instanceof EditText) { |
||||
|
setSelection(((EditText) view), startSelectionStart, startSelectionEnd); |
||||
|
} |
||||
|
} |
||||
|
if (mChangeBehavior > CHANGE_BEHAVIOR_KEEP) { |
||||
|
view.setTextColor(mPausedColor); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onTransitionEnd(Transition transition) { |
||||
|
transition.removeListener(this); |
||||
|
} |
||||
|
}; |
||||
|
addListener(transitionListener); |
||||
|
if (BuildConfig.DEBUG) { |
||||
|
Log.d(LOG_TAG, "createAnimator returning " + anim); |
||||
|
} |
||||
|
return anim; |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
private void setSelection(EditText editText, int start, int end) { |
||||
|
if (start >= 0 && end >= 0) { |
||||
|
editText.setSelection(start, end); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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. |
||||
|
* <p> |
||||
|
* 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<WindowInsetsAnimationCompat> 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(); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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<WindowInsetsAnimationCompat> 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); |
||||
|
} |
||||
|
} |
@ -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. |
||||
|
* <p> |
||||
|
* This class enables the root view is selectively defer handling any insets which match |
||||
|
* [deferredInsetTypes], to enable better looking [WindowInsetsAnimationCompat]s. |
||||
|
* <p> |
||||
|
* 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. |
||||
|
* <p> |
||||
|
* 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: |
||||
|
* <p> |
||||
|
* ``` |
||||
|
* val callback = RootViewDeferringInsetsCallback( |
||||
|
* persistentInsetTypes = WindowInsetsCompat.Type.systemBars(), |
||||
|
* deferredInsetTypes = WindowInsetsCompat.Type.ime() |
||||
|
* ) |
||||
|
* ``` |
||||
|
* <p> |
||||
|
* 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<WindowInsetsAnimationCompat> 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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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. |
||||
|
* <p> |
||||
|
* 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. |
||||
|
* <p> |
||||
|
* 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. |
||||
|
* <p> |
||||
|
* 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. |
||||
|
* <p> |
||||
|
* 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<Object> property = new FloatPropertyCompat<Object>("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); |
||||
|
} |
||||
|
} |
@ -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. |
||||
|
* <p> |
||||
|
* 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<WindowInsetsAnimationCompat> 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; |
||||
|
} |
||||
|
} |
1138
app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,91 @@ |
|||||
|
package awais.instagrabber.utils; |
||||
|
|
||||
|
/* |
||||
|
* Copyright (C) 2009 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 androidx.annotation.NonNull; |
||||
|
import androidx.annotation.Nullable; |
||||
|
import androidx.core.util.ObjectsCompat; |
||||
|
|
||||
|
/** |
||||
|
* Container to ease passing around a tuple of two objects. This object provides a sensible |
||||
|
* implementation of equals(), returning true if equals() is true on each of the contained |
||||
|
* objects. |
||||
|
*/ |
||||
|
public class NullSafePair<F, S> { |
||||
|
public final @NonNull |
||||
|
F first; |
||||
|
public final @NonNull |
||||
|
S second; |
||||
|
|
||||
|
/** |
||||
|
* Constructor for a Pair. |
||||
|
* |
||||
|
* @param first the first object in the Pair |
||||
|
* @param second the second object in the pair |
||||
|
*/ |
||||
|
public NullSafePair(@NonNull F first, @NonNull S second) { |
||||
|
this.first = first; |
||||
|
this.second = second; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Checks the two objects for equality by delegating to their respective |
||||
|
* {@link Object#equals(Object)} methods. |
||||
|
* |
||||
|
* @param o the {@link androidx.core.util.Pair} to which this one is to be checked for equality |
||||
|
* @return true if the underlying objects of the Pair are both considered |
||||
|
* equal |
||||
|
*/ |
||||
|
@Override |
||||
|
public boolean equals(Object o) { |
||||
|
if (!(o instanceof androidx.core.util.Pair)) { |
||||
|
return false; |
||||
|
} |
||||
|
androidx.core.util.Pair<?, ?> p = (androidx.core.util.Pair<?, ?>) o; |
||||
|
return ObjectsCompat.equals(p.first, first) && ObjectsCompat.equals(p.second, second); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Compute a hash code using the hash codes of the underlying objects |
||||
|
* |
||||
|
* @return a hashcode of the Pair |
||||
|
*/ |
||||
|
@Override |
||||
|
public int hashCode() { |
||||
|
return first.hashCode() ^ second.hashCode(); |
||||
|
} |
||||
|
|
||||
|
@NonNull |
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return "Pair{" + first + " " + second + "}"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convenience method for creating an appropriately typed pair. |
||||
|
* |
||||
|
* @param a the first object in the Pair |
||||
|
* @param b the second object in the pair |
||||
|
* @return a Pair that is templatized with the types of a and b |
||||
|
*/ |
||||
|
@NonNull |
||||
|
public static <A, B> androidx.core.util.Pair<A, B> create(@Nullable A a, @Nullable B b) { |
||||
|
return new androidx.core.util.Pair<A, B>(a, b); |
||||
|
} |
||||
|
} |
||||
|
|
@ -0,0 +1,11 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
|
<translate |
||||
|
android:duration="@android:integer/config_mediumAnimTime" |
||||
|
android:fromXDelta="50%p" |
||||
|
android:toXDelta="0" /> |
||||
|
<alpha |
||||
|
android:duration="@android:integer/config_mediumAnimTime" |
||||
|
android:fromAlpha="0.0" |
||||
|
android:toAlpha="1.0" /> |
||||
|
</set> |
@ -1,10 +1,5 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
<?xml version="1.0" encoding="utf-8"?> |
||||
<set xmlns:android="http://schemas.android.com/apk/res/android" |
|
||||
android:shareInterpolator="false"> |
|
||||
<translate |
|
||||
|
<translate xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:duration="300" |
android:duration="300" |
||||
android:fromXDelta="100%" |
android:fromXDelta="100%" |
||||
android:fromYDelta="0%" |
|
||||
android:toXDelta="0%" |
|
||||
android:toYDelta="0%" /> |
|
||||
</set> |
|
||||
|
android:toXDelta="0%" /> |
@ -0,0 +1,11 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
|
<translate |
||||
|
android:duration="@android:integer/config_mediumAnimTime" |
||||
|
android:fromXDelta="0" |
||||
|
android:toXDelta="-50%p" /> |
||||
|
<alpha |
||||
|
android:duration="@android:integer/config_mediumAnimTime" |
||||
|
android:fromAlpha="1.0" |
||||
|
android:toAlpha="0.0" /> |
||||
|
</set> |
@ -1,10 +1,5 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
<?xml version="1.0" encoding="utf-8"?> |
||||
<set xmlns:android="http://schemas.android.com/apk/res/android" |
|
||||
android:shareInterpolator="false"> |
|
||||
<translate |
|
||||
|
<translate xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:duration="300" |
android:duration="300" |
||||
android:fromXDelta="0%" |
android:fromXDelta="0%" |
||||
android:fromYDelta="0%" |
|
||||
android:toXDelta="100%" |
|
||||
android:toYDelta="0%" /> |
|
||||
</set> |
|
||||
|
android:toXDelta="100%" /> |
@ -1,10 +1,10 @@ |
|||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:width="24dp" |
android:width="24dp" |
||||
android:height="24dp" |
android:height="24dp" |
||||
|
android:tint="?colorControlNormal" |
||||
android:viewportWidth="24" |
android:viewportWidth="24" |
||||
android:viewportHeight="24" |
|
||||
android:tint="#333333"> |
|
||||
|
android:viewportHeight="24"> |
||||
<path |
<path |
||||
android:fillColor="@android:color/white" |
android:fillColor="@android:color/white" |
||||
android:pathData="M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z"/> |
|
||||
|
android:pathData="M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z" /> |
||||
</vector> |
</vector> |
@ -0,0 +1,10 @@ |
|||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
android:width="24dp" |
||||
|
android:height="24dp" |
||||
|
android:viewportWidth="24" |
||||
|
android:viewportHeight="24" |
||||
|
android:tint="?attr/colorControlNormal"> |
||||
|
<path |
||||
|
android:fillColor="@android:color/white" |
||||
|
android:pathData="M17,3L7,3c-1.1,0 -2,0.9 -2,2v16l7,-3 7,3L19,5c0,-1.1 -0.9,-2 -2,-2zM17,18l-5,-2.18L7,18L7,6c0,-0.55 0.45,-1 1,-1h8c0.55,0 1,0.45 1,1v12z"/> |
||||
|
</vector> |
@ -0,0 +1,10 @@ |
|||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
android:width="24dp" |
||||
|
android:height="24dp" |
||||
|
android:viewportWidth="24" |
||||
|
android:viewportHeight="24" |
||||
|
android:tint="?attr/colorControlNormal"> |
||||
|
<path |
||||
|
android:fillColor="@android:color/white" |
||||
|
android:pathData="M3,17.46v3.04c0,0.28 0.22,0.5 0.5,0.5h3.04c0.13,0 0.26,-0.05 0.35,-0.15L17.81,9.94l-3.75,-3.75L3.15,17.1c-0.1,0.1 -0.15,0.22 -0.15,0.36zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/> |
||||
|
</vector> |
@ -1,5 +1,5 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
<?xml version="1.0" encoding="utf-8"?> |
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" |
<shape xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:shape="oval"> |
android:shape="oval"> |
||||
<solid android:color="@color/black" /> |
|
||||
|
<solid android:color="@android:color/transparent" /> |
||||
</shape> |
</shape> |
Some files were not shown because too many files changed in this diff
Write
Preview
Loading…
Cancel
Save
Reference in new issue