Ammar Githam
4 years ago
33 changed files with 1405 additions and 148 deletions
-
44app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java
-
18app/src/main/java/awais/instagrabber/adapters/viewholder/DirectInboxItemViewHolder.java
-
1app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemLinkViewHolder.java
-
31app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaShareViewHolder.java
-
184app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java
-
1app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemVoiceMediaViewHolder.java
-
84app/src/main/java/awais/instagrabber/animations/RevealOutlineAnimation.java
-
56app/src/main/java/awais/instagrabber/animations/RoundedRectRevealOutlineProvider.java
-
3app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java
-
457app/src/main/java/awais/instagrabber/customviews/DirectItemContextMenu.java
-
110app/src/main/java/awais/instagrabber/customviews/DirectItemFrameLayout.java
-
55app/src/main/java/awais/instagrabber/customviews/PopupDialog.java
-
76app/src/main/java/awais/instagrabber/customviews/emoji/ReactionsManager.java
-
36app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java
-
7app/src/main/java/awais/instagrabber/repositories/requests/directmessages/ReactionBroadcastOptions.java
-
6app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItem.java
-
18app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemEmojiReaction.java
-
37app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemReactions.java
-
1app/src/main/java/awais/instagrabber/utils/Constants.java
-
3app/src/main/java/awais/instagrabber/utils/SettingsHelper.java
-
7app/src/main/java/awais/instagrabber/utils/emoji/EmojiParser.java
-
114app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java
-
10app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java
-
12app/src/main/res/drawable/bg_rounded_corner.xml
-
10app/src/main/res/drawable/ic_add.xml
-
10app/src/main/res/layout/fragment_direct_messages_thread.xml
-
2app/src/main/res/layout/item_pref_divider.xml
-
69app/src/main/res/layout/layout_direct_item_options.xml
-
70app/src/main/res/layout/layout_dm_base.xml
-
5app/src/main/res/layout/layout_dm_media_share.xml
-
10app/src/main/res/values/dimens.xml
-
5app/src/main/res/values/ids.xml
-
1app/src/main/res/values/strings.xml
@ -0,0 +1,84 @@ |
|||
package awais.instagrabber.animations; |
|||
|
|||
import android.animation.Animator; |
|||
import android.animation.AnimatorListenerAdapter; |
|||
import android.animation.ValueAnimator; |
|||
import android.graphics.Outline; |
|||
import android.graphics.Rect; |
|||
import android.view.View; |
|||
import android.view.ViewOutlineProvider; |
|||
|
|||
/** |
|||
* A {@link ViewOutlineProvider} that has helper functions to create reveal animations. |
|||
* This class should be extended so that subclasses can define the reveal shape as the |
|||
* animation progresses from 0 to 1. |
|||
*/ |
|||
public abstract class RevealOutlineAnimation extends ViewOutlineProvider { |
|||
protected Rect mOutline; |
|||
protected float mOutlineRadius; |
|||
|
|||
public RevealOutlineAnimation() { |
|||
mOutline = new Rect(); |
|||
} |
|||
|
|||
/** |
|||
* Returns whether elevation should be removed for the duration of the reveal animation. |
|||
*/ |
|||
abstract boolean shouldRemoveElevationDuringAnimation(); |
|||
|
|||
/** |
|||
* Sets the progress, from 0 to 1, of the reveal animation. |
|||
*/ |
|||
abstract void setProgress(float progress); |
|||
|
|||
public ValueAnimator createRevealAnimator(final View revealView, boolean isReversed) { |
|||
ValueAnimator va = |
|||
isReversed ? ValueAnimator.ofFloat(1f, 0f) : ValueAnimator.ofFloat(0f, 1f); |
|||
final float elevation = revealView.getElevation(); |
|||
|
|||
va.addListener(new AnimatorListenerAdapter() { |
|||
private boolean mIsClippedToOutline; |
|||
private ViewOutlineProvider mOldOutlineProvider; |
|||
|
|||
public void onAnimationStart(Animator animation) { |
|||
mIsClippedToOutline = revealView.getClipToOutline(); |
|||
mOldOutlineProvider = revealView.getOutlineProvider(); |
|||
|
|||
revealView.setOutlineProvider(RevealOutlineAnimation.this); |
|||
revealView.setClipToOutline(true); |
|||
if (shouldRemoveElevationDuringAnimation()) { |
|||
revealView.setTranslationZ(-elevation); |
|||
} |
|||
} |
|||
|
|||
public void onAnimationEnd(Animator animation) { |
|||
revealView.setOutlineProvider(mOldOutlineProvider); |
|||
revealView.setClipToOutline(mIsClippedToOutline); |
|||
if (shouldRemoveElevationDuringAnimation()) { |
|||
revealView.setTranslationZ(0); |
|||
} |
|||
} |
|||
|
|||
}); |
|||
|
|||
va.addUpdateListener(v -> { |
|||
float progress = (Float) v.getAnimatedValue(); |
|||
setProgress(progress); |
|||
revealView.invalidateOutline(); |
|||
}); |
|||
return va; |
|||
} |
|||
|
|||
@Override |
|||
public void getOutline(View v, Outline outline) { |
|||
outline.setRoundRect(mOutline, mOutlineRadius); |
|||
} |
|||
|
|||
public float getRadius() { |
|||
return mOutlineRadius; |
|||
} |
|||
|
|||
public void getOutline(Rect out) { |
|||
out.set(mOutline); |
|||
} |
|||
} |
@ -0,0 +1,56 @@ |
|||
/* |
|||
* Copyright (C) 2017 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.animations; |
|||
|
|||
import android.graphics.Rect; |
|||
|
|||
/** |
|||
* A {@link RevealOutlineAnimation} that provides an outline that interpolates between two radii |
|||
* and two {@link Rect}s. |
|||
* <p> |
|||
* An example usage of this provider is an outline that starts out as a circle and ends |
|||
* as a rounded rectangle. |
|||
*/ |
|||
public class RoundedRectRevealOutlineProvider extends RevealOutlineAnimation { |
|||
private final float mStartRadius; |
|||
private final float mEndRadius; |
|||
|
|||
private final Rect mStartRect; |
|||
private final Rect mEndRect; |
|||
|
|||
public RoundedRectRevealOutlineProvider(float startRadius, float endRadius, Rect startRect, Rect endRect) { |
|||
mStartRadius = startRadius; |
|||
mEndRadius = endRadius; |
|||
mStartRect = startRect; |
|||
mEndRect = endRect; |
|||
} |
|||
|
|||
@Override |
|||
public boolean shouldRemoveElevationDuringAnimation() { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public void setProgress(float progress) { |
|||
mOutlineRadius = (1 - progress) * mStartRadius + progress * mEndRadius; |
|||
|
|||
mOutline.left = (int) ((1 - progress) * mStartRect.left + progress * mEndRect.left); |
|||
mOutline.top = (int) ((1 - progress) * mStartRect.top + progress * mEndRect.top); |
|||
mOutline.right = (int) ((1 - progress) * mStartRect.right + progress * mEndRect.right); |
|||
mOutline.bottom = (int) ((1 - progress) * mStartRect.bottom + progress * mEndRect.bottom); |
|||
} |
|||
} |
@ -0,0 +1,457 @@ |
|||
package awais.instagrabber.customviews; |
|||
|
|||
import android.animation.Animator; |
|||
import android.animation.AnimatorListenerAdapter; |
|||
import android.animation.AnimatorSet; |
|||
import android.animation.TimeInterpolator; |
|||
import android.animation.ValueAnimator; |
|||
import android.content.Context; |
|||
import android.graphics.Point; |
|||
import android.graphics.Rect; |
|||
import android.util.TypedValue; |
|||
import android.view.Gravity; |
|||
import android.view.LayoutInflater; |
|||
import android.view.View; |
|||
import android.view.ViewGroup; |
|||
import android.view.animation.AccelerateDecelerateInterpolator; |
|||
import android.widget.ImageView; |
|||
import android.widget.PopupWindow; |
|||
|
|||
import androidx.annotation.IdRes; |
|||
import androidx.annotation.NonNull; |
|||
import androidx.annotation.Nullable; |
|||
import androidx.annotation.StringRes; |
|||
import androidx.appcompat.widget.AppCompatEditText; |
|||
import androidx.appcompat.widget.AppCompatImageView; |
|||
import androidx.appcompat.widget.AppCompatTextView; |
|||
import androidx.constraintlayout.widget.ConstraintLayout; |
|||
import androidx.core.util.Pair; |
|||
|
|||
import java.util.List; |
|||
|
|||
import awais.instagrabber.R; |
|||
import awais.instagrabber.animations.RoundedRectRevealOutlineProvider; |
|||
import awais.instagrabber.customviews.emoji.Emoji; |
|||
import awais.instagrabber.customviews.emoji.ReactionsManager; |
|||
import awais.instagrabber.databinding.LayoutDirectItemOptionsBinding; |
|||
|
|||
import static android.view.View.MeasureSpec.makeMeasureSpec; |
|||
|
|||
public class DirectItemContextMenu extends PopupWindow { |
|||
private static final String TAG = DirectItemContextMenu.class.getSimpleName(); |
|||
private static final int DO_NOT_UPDATE_FLAG = -1; |
|||
private static final int DURATION = 300; |
|||
|
|||
private final Context context; |
|||
private final boolean showReactions; |
|||
private final ReactionsManager reactionsManager; |
|||
private final int emojiSize; |
|||
private final int emojiMargin; |
|||
private final int emojiMarginHalf; |
|||
private final Rect startRect = new Rect(); |
|||
private final Rect endRect = new Rect(); |
|||
private final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator(); |
|||
private final AnimatorListenerAdapter exitAnimationListener; |
|||
private final TypedValue selectableItemBackgroundBorderless; |
|||
private final TypedValue selectableItemBackground; |
|||
private final int dividerHeight; |
|||
private final int optionHeight; |
|||
private final int optionPadding; |
|||
private final int addAdjust; |
|||
private final boolean hasOptions; |
|||
private final List<MenuItem> options; |
|||
|
|||
/* = ImmutableList.of( |
|||
new MenuItem(R.id.reply, R.string.reply), |
|||
new MenuItem(R.id.unsend, R.string.dms_inbox_unsend) |
|||
);*/ |
|||
private AnimatorSet openCloseAnimator; |
|||
private Point location; |
|||
private Point point; |
|||
private OnReactionClickListener onReactionClickListener; |
|||
private OnOptionSelectListener onOptionSelectListener; |
|||
private OnAddReactionClickListener onAddReactionListener; |
|||
|
|||
public DirectItemContextMenu(@NonNull final Context context, final boolean showReactions, final List<MenuItem> options) { |
|||
super(context); |
|||
this.context = context; |
|||
this.showReactions = showReactions; |
|||
this.options = options; |
|||
if (!showReactions && (options == null || options.isEmpty())) { |
|||
throw new IllegalArgumentException("showReactions is set false and options are empty"); |
|||
} |
|||
reactionsManager = ReactionsManager.getInstance(); |
|||
emojiSize = context.getResources().getDimensionPixelSize(R.dimen.reaction_picker_emoji_size); |
|||
emojiMargin = context.getResources().getDimensionPixelSize(R.dimen.reaction_picker_emoji_margin); |
|||
emojiMarginHalf = emojiMargin / 2; |
|||
addAdjust = context.getResources().getDimensionPixelSize(R.dimen.reaction_picker_add_padding_adjustment); |
|||
dividerHeight = context.getResources().getDimensionPixelSize(R.dimen.horizontal_divider_height); |
|||
optionHeight = context.getResources().getDimensionPixelSize(R.dimen.reaction_picker_option_height); |
|||
optionPadding = context.getResources().getDimensionPixelSize(R.dimen.dm_message_card_radius); |
|||
exitAnimationListener = new AnimatorListenerAdapter() { |
|||
@Override |
|||
public void onAnimationEnd(final Animator animation) { |
|||
openCloseAnimator = null; |
|||
point = null; |
|||
getContentView().post(DirectItemContextMenu.super::dismiss); |
|||
} |
|||
}; |
|||
selectableItemBackgroundBorderless = new TypedValue(); |
|||
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, selectableItemBackgroundBorderless, true); |
|||
selectableItemBackground = new TypedValue(); |
|||
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, selectableItemBackground, true); |
|||
hasOptions = options != null && !options.isEmpty(); |
|||
} |
|||
|
|||
public void show(@NonNull View rootView, @NonNull final Point location) { |
|||
final View content = createContentView(); |
|||
content.measure(makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); |
|||
setup(content); |
|||
// rootView.getParent().requestDisallowInterceptTouchEvent(true); |
|||
// final Point correctedLocation = new Point(location.x, location.y - emojiSize * 2); |
|||
this.location = location; |
|||
showAtLocation(rootView, Gravity.TOP | Gravity.START, location.x, location.y); |
|||
// fixPopupLocation(popupWindow, correctedLocation); |
|||
animateOpen(); |
|||
} |
|||
|
|||
private void setup(final View content) { |
|||
setContentView(content); |
|||
setWindowLayoutMode(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); |
|||
setFocusable(true); |
|||
setOutsideTouchable(true); |
|||
setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); |
|||
setBackgroundDrawable(null); |
|||
} |
|||
|
|||
public void setOnOptionSelectListener(final OnOptionSelectListener onOptionSelectListener) { |
|||
this.onOptionSelectListener = onOptionSelectListener; |
|||
} |
|||
|
|||
public void setOnReactionClickListener(final OnReactionClickListener onReactionClickListener) { |
|||
this.onReactionClickListener = onReactionClickListener; |
|||
} |
|||
|
|||
public void setOnAddReactionListener(final OnAddReactionClickListener onAddReactionListener) { |
|||
this.onAddReactionListener = onAddReactionListener; |
|||
} |
|||
|
|||
private void animateOpen() { |
|||
final View contentView = getContentView(); |
|||
contentView.setVisibility(View.INVISIBLE); |
|||
contentView.post(() -> { |
|||
final AnimatorSet openAnim = new AnimatorSet(); |
|||
// Rectangular reveal. |
|||
final ValueAnimator revealAnim = createOpenCloseOutlineProvider().createRevealAnimator(contentView, false); |
|||
revealAnim.setDuration(DURATION); |
|||
revealAnim.setInterpolator(revealInterpolator); |
|||
|
|||
ValueAnimator fadeIn = ValueAnimator.ofFloat(0, 1); |
|||
fadeIn.setDuration(DURATION); |
|||
fadeIn.setInterpolator(revealInterpolator); |
|||
fadeIn.addUpdateListener(anim -> { |
|||
float alpha = (float) anim.getAnimatedValue(); |
|||
contentView.setAlpha(revealAnim.isStarted() ? alpha : 0); |
|||
}); |
|||
openAnim.play(fadeIn); |
|||
openAnim.addListener(new AnimatorListenerAdapter() { |
|||
@Override |
|||
public void onAnimationEnd(Animator animation) { |
|||
contentView.setAlpha(1f); |
|||
openCloseAnimator = null; |
|||
} |
|||
}); |
|||
|
|||
openCloseAnimator = openAnim; |
|||
openAnim.playSequentially(revealAnim); |
|||
contentView.setVisibility(View.VISIBLE); |
|||
openAnim.start(); |
|||
}); |
|||
} |
|||
|
|||
protected void animateClose() { |
|||
endRect.setEmpty(); |
|||
if (openCloseAnimator != null) { |
|||
openCloseAnimator.cancel(); |
|||
} |
|||
final View contentView = getContentView(); |
|||
final AnimatorSet closeAnim = new AnimatorSet(); |
|||
// Rectangular reveal (reversed). |
|||
final ValueAnimator revealAnim = createOpenCloseOutlineProvider().createRevealAnimator(contentView, true); |
|||
revealAnim.setDuration(DURATION); |
|||
revealAnim.setInterpolator(revealInterpolator); |
|||
closeAnim.play(revealAnim); |
|||
|
|||
ValueAnimator fadeOut = ValueAnimator.ofFloat(contentView.getAlpha(), 0); |
|||
fadeOut.setDuration(DURATION); |
|||
fadeOut.setInterpolator(revealInterpolator); |
|||
fadeOut.addUpdateListener(anim -> { |
|||
float alpha = (float) anim.getAnimatedValue(); |
|||
contentView.setAlpha(revealAnim.isStarted() ? alpha : contentView.getAlpha()); |
|||
}); |
|||
closeAnim.playTogether(fadeOut); |
|||
closeAnim.addListener(exitAnimationListener); |
|||
openCloseAnimator = closeAnim; |
|||
closeAnim.start(); |
|||
} |
|||
|
|||
private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() { |
|||
final View contentView = getContentView(); |
|||
final int radius = context.getResources().getDimensionPixelSize(R.dimen.dm_message_card_radius_small); |
|||
// Log.d(TAG, "createOpenCloseOutlineProvider: " + locationOnScreen(contentView) + " " + contentView.getMeasuredWidth() + " " + contentView |
|||
// .getMeasuredHeight()); |
|||
if (point == null) { |
|||
point = locationOnScreen(contentView); |
|||
} |
|||
final int left = location.x - point.x; |
|||
final int top = location.y - point.y; |
|||
startRect.set(left, top, left, top); |
|||
endRect.set(0, 0, contentView.getMeasuredWidth(), contentView.getMeasuredHeight()); |
|||
return new RoundedRectRevealOutlineProvider(radius, radius, startRect, endRect); |
|||
} |
|||
|
|||
public void dismiss() { |
|||
animateClose(); |
|||
} |
|||
|
|||
private View createContentView() { |
|||
final LayoutInflater layoutInflater = LayoutInflater.from(context); |
|||
final LayoutDirectItemOptionsBinding binding = LayoutDirectItemOptionsBinding.inflate(layoutInflater, null, false); |
|||
Pair<View, View> firstLastEmojiView = null; |
|||
if (showReactions) { |
|||
firstLastEmojiView = addReactions(layoutInflater, binding.container); |
|||
} |
|||
if (hasOptions) { |
|||
View divider = null; |
|||
if (showReactions) { |
|||
if (firstLastEmojiView == null) { |
|||
throw new IllegalStateException("firstLastEmojiView is null even though reactions were added"); |
|||
} |
|||
// add divider if reactions were added |
|||
divider = addDivider(binding.container, |
|||
firstLastEmojiView.first.getId(), |
|||
firstLastEmojiView.first.getId(), |
|||
firstLastEmojiView.second.getId()); |
|||
((ConstraintLayout.LayoutParams) firstLastEmojiView.first.getLayoutParams()).bottomToTop = divider.getId(); |
|||
} |
|||
addOptions(layoutInflater, binding.container, divider); |
|||
} |
|||
return binding.getRoot(); |
|||
} |
|||
|
|||
private Pair<View, View> addReactions(final LayoutInflater layoutInflater, final ConstraintLayout container) { |
|||
final List<Emoji> reactions = reactionsManager.getReactions(); |
|||
AppCompatImageView prevSquareImageView = null; |
|||
View firstImageView = null; |
|||
View lastImageView = null; |
|||
for (int i = 0; i < reactions.size(); i++) { |
|||
final Emoji reaction = reactions.get(i); |
|||
final AppCompatImageView imageView = getEmojiImageView(); |
|||
final ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) imageView.getLayoutParams(); |
|||
if (i == 0 && !hasOptions) { |
|||
// only connect bottom to parent bottom if there are no options |
|||
layoutParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID; |
|||
} |
|||
if (i == 0) { |
|||
layoutParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID; |
|||
layoutParams.startToStart = ConstraintLayout.LayoutParams.PARENT_ID; |
|||
firstImageView = imageView; |
|||
layoutParams.setMargins(emojiMargin, emojiMargin, emojiMarginHalf, emojiMargin); |
|||
} else { |
|||
layoutParams.startToEnd = prevSquareImageView.getId(); |
|||
final ConstraintLayout.LayoutParams prevViewLayoutParams = (ConstraintLayout.LayoutParams) prevSquareImageView.getLayoutParams(); |
|||
prevViewLayoutParams.endToStart = imageView.getId(); |
|||
// always connect the other image view's top and bottom to the first image view top and bottom |
|||
layoutParams.topToTop = firstImageView.getId(); |
|||
layoutParams.bottomToBottom = firstImageView.getId(); |
|||
layoutParams.setMargins(emojiMarginHalf, emojiMargin, emojiMarginHalf, emojiMargin); |
|||
} |
|||
imageView.setImageDrawable(reaction.getDrawable()); |
|||
imageView.setOnClickListener(view -> { |
|||
if (onReactionClickListener != null) { |
|||
onReactionClickListener.onClick(reaction); |
|||
} |
|||
dismiss(); |
|||
}); |
|||
container.addView(imageView); |
|||
prevSquareImageView = imageView; |
|||
} |
|||
// add the + icon |
|||
if (prevSquareImageView != null) { |
|||
final AppCompatImageView imageView = getEmojiImageView(); |
|||
final ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) imageView.getLayoutParams(); |
|||
layoutParams.topToTop = firstImageView.getId(); |
|||
layoutParams.bottomToBottom = firstImageView.getId(); |
|||
layoutParams.startToEnd = prevSquareImageView.getId(); |
|||
final ConstraintLayout.LayoutParams prevViewLayoutParams = (ConstraintLayout.LayoutParams) prevSquareImageView.getLayoutParams(); |
|||
prevViewLayoutParams.endToStart = imageView.getId(); |
|||
layoutParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID; |
|||
layoutParams.setMargins(emojiMarginHalf - addAdjust, emojiMargin - addAdjust, emojiMargin - addAdjust, emojiMargin - addAdjust); |
|||
imageView.setImageResource(R.drawable.ic_add); |
|||
imageView.setOnClickListener(view -> { |
|||
if (onAddReactionListener != null) { |
|||
onAddReactionListener.onAdd(); |
|||
} |
|||
dismiss(); |
|||
}); |
|||
lastImageView = imageView; |
|||
container.addView(imageView); |
|||
} |
|||
return new Pair<>(firstImageView, lastImageView); |
|||
} |
|||
|
|||
@NonNull |
|||
private AppCompatImageView getEmojiImageView() { |
|||
final AppCompatImageView imageView = new AppCompatImageView(context); |
|||
final ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(emojiSize, emojiSize); |
|||
imageView.setBackgroundResource(selectableItemBackgroundBorderless.resourceId); |
|||
imageView.setScaleType(ImageView.ScaleType.FIT_CENTER); |
|||
imageView.setId(SquareImageView.generateViewId()); |
|||
imageView.setLayoutParams(layoutParams); |
|||
return imageView; |
|||
} |
|||
|
|||
private void addOptions(final LayoutInflater layoutInflater, |
|||
final ConstraintLayout container, |
|||
@Nullable final View divider) { |
|||
View prevOptionView = null; |
|||
for (int i = 0; i < options.size(); i++) { |
|||
final MenuItem menuItem = options.get(i); |
|||
final AppCompatTextView textView = getTextView(); |
|||
final ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) textView.getLayoutParams(); |
|||
layoutParams.startToStart = ConstraintLayout.LayoutParams.PARENT_ID; |
|||
layoutParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID; |
|||
if (i == 0) { |
|||
if (divider != null) { |
|||
layoutParams.topToBottom = divider.getId(); |
|||
} else { |
|||
// if divider is null mean reactions were not added, so connect top to top of parent |
|||
layoutParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID; |
|||
} |
|||
((ConstraintLayout.LayoutParams) divider.getLayoutParams()).bottomToTop = textView.getId(); |
|||
} else { |
|||
layoutParams.topToBottom = prevOptionView.getId(); |
|||
final ConstraintLayout.LayoutParams prevLayoutParams = (ConstraintLayout.LayoutParams) prevOptionView.getLayoutParams(); |
|||
prevLayoutParams.bottomToTop = textView.getId(); |
|||
} |
|||
if (i == options.size() - 1) { |
|||
layoutParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID; |
|||
layoutParams.bottomMargin = emojiMargin; // material design spec (https://material.io/components/menus#specs) |
|||
} |
|||
textView.setText(context.getString(menuItem.getTitleRes())); |
|||
textView.setOnClickListener(v -> { |
|||
if (onOptionSelectListener != null) { |
|||
onOptionSelectListener.onSelect(menuItem.getItemId()); |
|||
} |
|||
dismiss(); |
|||
}); |
|||
container.addView(textView); |
|||
prevOptionView = textView; |
|||
} |
|||
} |
|||
|
|||
private AppCompatTextView getTextView() { |
|||
final AppCompatTextView textView = new AppCompatTextView(context); |
|||
textView.setId(AppCompatEditText.generateViewId()); |
|||
textView.setBackgroundResource(selectableItemBackground.resourceId); |
|||
textView.setGravity(Gravity.CENTER_VERTICAL); |
|||
textView.setPaddingRelative(optionPadding, 0, optionPadding, 0); |
|||
textView.setTextAppearance(context, R.style.TextAppearance_MaterialComponents_Body1); |
|||
final ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.MATCH_CONSTRAINT, |
|||
optionHeight); |
|||
textView.setLayoutParams(layoutParams); |
|||
return textView; |
|||
} |
|||
|
|||
private View addDivider(final ConstraintLayout container, |
|||
final int topViewId, |
|||
final int startViewId, |
|||
final int endViewId) { |
|||
final View dividerView = new View(context); |
|||
dividerView.setId(View.generateViewId()); |
|||
dividerView.setBackgroundResource(R.drawable.pref_list_divider_material); |
|||
final ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.MATCH_CONSTRAINT, |
|||
dividerHeight); |
|||
layoutParams.topToBottom = topViewId; |
|||
layoutParams.startToStart = startViewId; |
|||
layoutParams.endToEnd = endViewId; |
|||
dividerView.setLayoutParams(layoutParams); |
|||
container.addView(dividerView); |
|||
return dividerView; |
|||
} |
|||
|
|||
@NonNull |
|||
private Point locationOnScreen(@NonNull final View view) { |
|||
final int[] location = new int[2]; |
|||
view.getLocationOnScreen(location); |
|||
return new Point(location[0], location[1]); |
|||
} |
|||
|
|||
public static class MenuItem { |
|||
@IdRes |
|||
private final int itemId; |
|||
@StringRes |
|||
private final int titleRes; |
|||
|
|||
public MenuItem(@IdRes final int itemId, @StringRes final int titleRes) { |
|||
this.itemId = itemId; |
|||
this.titleRes = titleRes; |
|||
} |
|||
|
|||
public int getItemId() { |
|||
return itemId; |
|||
} |
|||
|
|||
public int getTitleRes() { |
|||
return titleRes; |
|||
} |
|||
} |
|||
|
|||
public interface OnOptionSelectListener { |
|||
void onSelect(int itemId); |
|||
} |
|||
|
|||
public interface OnReactionClickListener { |
|||
void onClick(Emoji emoji); |
|||
} |
|||
|
|||
public interface OnAddReactionClickListener { |
|||
void onAdd(); |
|||
} |
|||
|
|||
// @NonNull |
|||
// private Rect getGlobalVisibleRect(@NonNull final View view) { |
|||
// final Rect rect = new Rect(); |
|||
// view.getGlobalVisibleRect(rect); |
|||
// return rect; |
|||
// } |
|||
|
|||
// private void fixPopupLocation(@NonNull final PopupWindow popupWindow, @NonNull final Point desiredLocation) { |
|||
// popupWindow.getContentView().post(() -> { |
|||
// final Point actualLocation = locationOnScreen(popupWindow.getContentView()); |
|||
// |
|||
// if (!(actualLocation.x == desiredLocation.x && actualLocation.y == desiredLocation.y)) { |
|||
// final int differenceX = actualLocation.x - desiredLocation.x; |
|||
// final int differenceY = actualLocation.y - desiredLocation.y; |
|||
// |
|||
// final int fixedOffsetX; |
|||
// final int fixedOffsetY; |
|||
// |
|||
// if (actualLocation.x > desiredLocation.x) { |
|||
// fixedOffsetX = desiredLocation.x - differenceX; |
|||
// } else { |
|||
// fixedOffsetX = desiredLocation.x + differenceX; |
|||
// } |
|||
// |
|||
// if (actualLocation.y > desiredLocation.y) { |
|||
// fixedOffsetY = desiredLocation.y - differenceY; |
|||
// } else { |
|||
// fixedOffsetY = desiredLocation.y + differenceY; |
|||
// } |
|||
// |
|||
// popupWindow.update(fixedOffsetX, fixedOffsetY, DO_NOT_UPDATE_FLAG, DO_NOT_UPDATE_FLAG); |
|||
// } |
|||
// }); |
|||
// } |
|||
} |
|||
|
@ -0,0 +1,110 @@ |
|||
package awais.instagrabber.customviews; |
|||
|
|||
import android.content.Context; |
|||
import android.os.Handler; |
|||
import android.util.AttributeSet; |
|||
import android.view.MotionEvent; |
|||
import android.view.View; |
|||
import android.view.ViewConfiguration; |
|||
import android.widget.FrameLayout; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.annotation.Nullable; |
|||
|
|||
public class DirectItemFrameLayout extends FrameLayout { |
|||
private static final String TAG = DirectItemFrameLayout.class.getSimpleName(); |
|||
|
|||
private boolean longPressed = false; |
|||
private float touchX; |
|||
private float touchY; |
|||
private OnItemLongClickListener onItemLongClickListener; |
|||
private int touchSlop; |
|||
|
|||
private final Handler handler = new Handler(); |
|||
private final Runnable longPressRunnable = () -> { |
|||
longPressed = true; |
|||
if (onItemLongClickListener != null) { |
|||
onItemLongClickListener.onLongClick(this, touchX, touchY); |
|||
} |
|||
}; |
|||
private final Runnable longPressStartRunnable = () -> { |
|||
if (onItemLongClickListener != null) { |
|||
onItemLongClickListener.onLongClickStart(this); |
|||
} |
|||
}; |
|||
|
|||
public DirectItemFrameLayout(@NonNull final Context context) { |
|||
super(context); |
|||
init(context); |
|||
} |
|||
|
|||
public DirectItemFrameLayout(@NonNull final Context context, @Nullable final AttributeSet attrs) { |
|||
super(context, attrs); |
|||
init(context); |
|||
} |
|||
|
|||
public DirectItemFrameLayout(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) { |
|||
super(context, attrs, defStyleAttr); |
|||
init(context); |
|||
} |
|||
|
|||
public DirectItemFrameLayout(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) { |
|||
super(context, attrs, defStyleAttr, defStyleRes); |
|||
init(context); |
|||
} |
|||
|
|||
private void init(final Context context) { |
|||
ViewConfiguration vc = ViewConfiguration.get(context); |
|||
touchSlop = vc.getScaledTouchSlop(); |
|||
} |
|||
|
|||
public void setOnItemLongClickListener(final OnItemLongClickListener onItemLongClickListener) { |
|||
this.onItemLongClickListener = onItemLongClickListener; |
|||
} |
|||
|
|||
@Override |
|||
public boolean dispatchTouchEvent(final MotionEvent ev) { |
|||
switch (ev.getAction()) { |
|||
case MotionEvent.ACTION_DOWN: |
|||
longPressed = false; |
|||
handler.postDelayed(longPressRunnable, ViewConfiguration.getLongPressTimeout()); |
|||
handler.postDelayed(longPressStartRunnable, ViewConfiguration.getTapTimeout()); |
|||
touchX = ev.getRawX(); |
|||
touchY = ev.getRawY(); |
|||
break; |
|||
case MotionEvent.ACTION_MOVE: |
|||
if (longPressed || Math.abs(touchX - ev.getRawX()) > touchSlop || Math.abs(touchY - ev.getRawY()) > touchSlop) { |
|||
handler.removeCallbacks(longPressStartRunnable); |
|||
handler.removeCallbacks(longPressRunnable); |
|||
} |
|||
break; |
|||
case MotionEvent.ACTION_UP: |
|||
handler.removeCallbacks(longPressRunnable); |
|||
handler.removeCallbacks(longPressStartRunnable); |
|||
if (longPressed) { |
|||
return true; |
|||
} |
|||
if (onItemLongClickListener != null) { |
|||
onItemLongClickListener.onLongClickCancel(this); |
|||
} |
|||
break; |
|||
case MotionEvent.ACTION_CANCEL: |
|||
handler.removeCallbacks(longPressRunnable); |
|||
handler.removeCallbacks(longPressStartRunnable); |
|||
break; |
|||
} |
|||
final boolean dispatchTouchEvent = super.dispatchTouchEvent(ev); |
|||
if (ev.getAction() == MotionEvent.ACTION_DOWN && !dispatchTouchEvent) { |
|||
return true; |
|||
} |
|||
return dispatchTouchEvent; |
|||
} |
|||
|
|||
public interface OnItemLongClickListener { |
|||
void onLongClickStart(View view); |
|||
|
|||
void onLongClickCancel(View view); |
|||
|
|||
void onLongClick(View view, float x, float y); |
|||
} |
|||
} |
@ -1,55 +0,0 @@ |
|||
package awais.instagrabber.customviews; |
|||
|
|||
import android.app.Dialog; |
|||
import android.content.Context; |
|||
import android.graphics.drawable.Drawable; |
|||
import android.os.IBinder; |
|||
import android.view.Gravity; |
|||
import android.view.View; |
|||
import android.view.Window; |
|||
import android.view.WindowManager; |
|||
|
|||
import awais.instagrabber.utils.Utils; |
|||
|
|||
/** |
|||
* https://stackoverflow.com/a/15766097/1436766 |
|||
*/ |
|||
public class PopupDialog extends Dialog { |
|||
private final Context context; |
|||
|
|||
public PopupDialog(Context context) { |
|||
super(context); |
|||
this.context = context; |
|||
requestWindowFeature(Window.FEATURE_NO_TITLE); |
|||
} |
|||
|
|||
public void showAtLocation(final IBinder token, final int gravity, int x, int y) { |
|||
final Window window = getWindow(); |
|||
if (window == null) return; |
|||
WindowManager.LayoutParams layoutParams = window.getAttributes(); |
|||
layoutParams.gravity = gravity; |
|||
layoutParams.x = x; |
|||
layoutParams.y = y; |
|||
// layoutParams.token = token; |
|||
show(); |
|||
} |
|||
|
|||
public void showAsDropDown(View view) { |
|||
float density = Utils.displayMetrics.density; |
|||
final Window window = getWindow(); |
|||
if (window == null) return; |
|||
WindowManager.LayoutParams layoutParams = window.getAttributes(); |
|||
int[] location = new int[2]; |
|||
view.getLocationInWindow(location); |
|||
layoutParams.gravity = Gravity.TOP | Gravity.START; |
|||
layoutParams.x = location[0] + (int) (view.getWidth() / density); |
|||
layoutParams.y = location[1] + (int) (view.getHeight() / density); |
|||
show(); |
|||
} |
|||
|
|||
public void setBackgroundDrawable(final Drawable drawable) { |
|||
final Window window = getWindow(); |
|||
if (window == null) return; |
|||
window.setBackgroundDrawable(drawable); |
|||
} |
|||
} |
@ -0,0 +1,76 @@ |
|||
package awais.instagrabber.customviews.emoji; |
|||
|
|||
import android.util.Log; |
|||
|
|||
import com.google.common.collect.ImmutableList; |
|||
|
|||
import org.json.JSONArray; |
|||
import org.json.JSONException; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
import awais.instagrabber.utils.AppExecutors; |
|||
import awais.instagrabber.utils.TextUtils; |
|||
import awais.instagrabber.utils.Utils; |
|||
import awais.instagrabber.utils.emoji.EmojiParser; |
|||
|
|||
import static awais.instagrabber.utils.Constants.PREF_REACTIONS; |
|||
|
|||
public class ReactionsManager { |
|||
private static final String TAG = ReactionsManager.class.getSimpleName(); |
|||
private static final Object LOCK = new Object(); |
|||
|
|||
private final AppExecutors appExecutors = AppExecutors.getInstance(); |
|||
private final List<Emoji> reactions = new ArrayList<>(); |
|||
|
|||
private static ReactionsManager instance; |
|||
|
|||
public static ReactionsManager getInstance() { |
|||
if (instance == null) { |
|||
synchronized (LOCK) { |
|||
if (instance == null) { |
|||
instance = new ReactionsManager(); |
|||
} |
|||
} |
|||
} |
|||
return instance; |
|||
} |
|||
|
|||
private ReactionsManager() { |
|||
String reactionsJson = Utils.settingsHelper.getString(PREF_REACTIONS); |
|||
if (TextUtils.isEmpty(reactionsJson)) { |
|||
final ImmutableList<String> list = ImmutableList.of("โค๏ธ", "\uD83D\uDE02", "\uD83D\uDE2E", "\uD83D\uDE22", "\uD83D\uDE21", "\uD83D\uDC4D"); |
|||
reactionsJson = new JSONArray(list).toString(); |
|||
} |
|||
final EmojiParser emojiParser = EmojiParser.getInstance(); |
|||
final Map<String, Emoji> allEmojis = emojiParser.getAllEmojis(); |
|||
try { |
|||
final JSONArray reactionsJsonArray = new JSONArray(reactionsJson); |
|||
for (int i = 0; i < reactionsJsonArray.length(); i++) { |
|||
final String emojiUnicode = reactionsJsonArray.optString(i); |
|||
if (emojiUnicode == null) continue; |
|||
final Emoji emoji = allEmojis.get(emojiUnicode); |
|||
if (emoji == null) continue; |
|||
reactions.add(emoji); |
|||
} |
|||
} catch (JSONException e) { |
|||
Log.e(TAG, "ReactionsManager: ", e); |
|||
} |
|||
} |
|||
|
|||
public List<Emoji> getReactions() { |
|||
return reactions; |
|||
} |
|||
|
|||
// public void setVariant(final String parent, final String variant) { |
|||
// if (parent == null || variant == null) return; |
|||
// selectedVariantMap.put(parent, variant); |
|||
// appExecutors.tasksThread().execute(() -> { |
|||
// final JSONObject jsonObject = new JSONObject(selectedVariantMap); |
|||
// final String json = jsonObject.toString(); |
|||
// Utils.settingsHelper.putString(PREF_EMOJI_VARIANTS, json); |
|||
// }); |
|||
// } |
|||
} |
@ -0,0 +1,12 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<shape xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:shape="rectangle"> |
|||
<solid android:color="?colorSurface" /> |
|||
<padding |
|||
android:bottom="4dp" |
|||
android:left="4dp" |
|||
android:right="4dp" |
|||
android:top="4dp" /> |
|||
|
|||
<corners android:radius="8dp" /> |
|||
</shape> |
@ -1,10 +1,10 @@ |
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:width="24dp" |
|||
android:height="24dp" |
|||
android:tint="?attr/colorControlNormal" |
|||
android:viewportWidth="24" |
|||
android:viewportHeight="24"> |
|||
<path |
|||
android:fillColor="@android:color/white" |
|||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" /> |
|||
android:viewportHeight="24" |
|||
android:tint="?attr/colorControlNormal"> |
|||
<path |
|||
android:fillColor="@android:color/white" |
|||
android:pathData="M18,13h-5v5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1v-5H6c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5V6c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v5h5c0.55,0 1,0.45 1,1s-0.45,1 -1,1z"/> |
|||
</vector> |
@ -1,5 +1,5 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<View xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="1dp" |
|||
android:layout_height="@dimen/horizontal_divider_height" |
|||
android:background="@drawable/pref_list_divider_material" /> |
@ -0,0 +1,69 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" |
|||
xmlns:app="http://schemas.android.com/apk/res-auto" |
|||
android:id="@+id/card" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
app:cardCornerRadius="8dp" |
|||
app:cardElevation="2dp"> |
|||
|
|||
<androidx.constraintlayout.widget.ConstraintLayout |
|||
android:id="@+id/container" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content"> |
|||
|
|||
<!--<androidx.appcompat.widget.AppCompatImageView--> |
|||
<!-- android:id="@+id/img1"--> |
|||
<!-- android:layout_width="48dp"--> |
|||
<!-- android:layout_height="0dp"--> |
|||
<!-- android:padding="4dp"--> |
|||
<!-- android:scaleType="fitCenter"--> |
|||
<!-- app:layout_constraintBottom_toTopOf="@id/divider"--> |
|||
<!-- app:layout_constraintDimensionRatio="1"--> |
|||
<!-- app:layout_constraintEnd_toStartOf="@id/img2"--> |
|||
<!-- app:layout_constraintStart_toStartOf="parent"--> |
|||
<!-- app:layout_constraintTop_toTopOf="parent"--> |
|||
<!-- tools:srcCompat="@mipmap/ic_launcher" />--> |
|||
|
|||
<!--<androidx.appcompat.widget.AppCompatImageView--> |
|||
<!-- android:id="@+id/img2"--> |
|||
<!-- android:layout_width="48dp"--> |
|||
<!-- android:layout_height="0dp"--> |
|||
<!-- android:padding="4dp"--> |
|||
<!-- android:scaleType="fitCenter"--> |
|||
<!-- app:layout_constraintDimensionRatio="1"--> |
|||
<!-- app:layout_constraintEnd_toStartOf="@id/img3"--> |
|||
<!-- app:layout_constraintStart_toEndOf="@id/img1"--> |
|||
<!-- app:layout_constraintTop_toTopOf="parent"--> |
|||
<!-- tools:srcCompat="@mipmap/ic_launcher" />--> |
|||
|
|||
<!--<awais.instagrabber.customviews.SquareImageView--> |
|||
<!-- android:id="@+id/img3"--> |
|||
<!-- android:layout_width="48dp"--> |
|||
<!-- android:layout_height="48dp"--> |
|||
<!-- android:padding="4dp"--> |
|||
<!-- app:layout_constraintEnd_toEndOf="parent"--> |
|||
<!-- app:layout_constraintStart_toEndOf="@id/img2"--> |
|||
<!-- app:layout_constraintTop_toTopOf="parent"--> |
|||
<!-- tools:srcCompat="@mipmap/ic_launcher" />--> |
|||
|
|||
<!--<include--> |
|||
<!-- android:id="@+id/divider"--> |
|||
<!-- layout="@layout/item_pref_divider"--> |
|||
<!-- android:layout_width="0dp"--> |
|||
<!-- android:layout_height="1dp"--> |
|||
<!-- app:layout_constraintEnd_toEndOf="@id/img3"--> |
|||
<!-- app:layout_constraintStart_toStartOf="@id/img1"--> |
|||
<!-- app:layout_constraintTop_toBottomOf="@id/img1" />--> |
|||
|
|||
<!--<androidx.appcompat.widget.AppCompatTextView--> |
|||
<!-- android:layout_width="0dp"--> |
|||
<!-- android:layout_height="wrap_content"--> |
|||
<!-- android:text="test"--> |
|||
<!-- app:layout_constraintBottom_toBottomOf="parent"--> |
|||
<!-- app:layout_constraintEnd_toEndOf="parent"--> |
|||
<!-- app:layout_constraintStart_toStartOf="parent"--> |
|||
<!-- app:layout_constraintTop_toBottomOf="@id/divider" />--> |
|||
|
|||
</androidx.constraintlayout.widget.ConstraintLayout> |
|||
</androidx.cardview.widget.CardView> |
@ -0,0 +1,5 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<item name="reply" type="id" /> |
|||
<item name="unsend" type="id" /> |
|||
</resources> |
Write
Preview
Loadingโฆ
Cancel
Save
Reference in new issue