2017-08-03 23:26:26 +02:00
|
|
|
package com.keylesspalace.tusky.adapter;
|
|
|
|
|
|
|
|
import android.content.Context;
|
|
|
|
import android.graphics.drawable.Drawable;
|
2019-04-09 20:13:25 +02:00
|
|
|
import android.preference.PreferenceManager;
|
2017-08-03 23:26:26 +02:00
|
|
|
import android.text.Spanned;
|
2017-11-30 20:12:09 +01:00
|
|
|
import android.text.TextUtils;
|
2017-08-03 23:26:26 +02:00
|
|
|
import android.view.View;
|
2018-03-01 21:10:10 +01:00
|
|
|
import android.view.ViewGroup;
|
2019-04-22 10:11:00 +02:00
|
|
|
import android.widget.Button;
|
2019-04-28 09:58:17 +02:00
|
|
|
import android.widget.CheckBox;
|
2017-08-03 23:26:26 +02:00
|
|
|
import android.widget.ImageButton;
|
|
|
|
import android.widget.ImageView;
|
2019-04-22 10:11:00 +02:00
|
|
|
import android.widget.RadioButton;
|
|
|
|
import android.widget.RadioGroup;
|
2017-08-03 23:26:26 +02:00
|
|
|
import android.widget.TextView;
|
|
|
|
import android.widget.ToggleButton;
|
|
|
|
|
2019-04-16 21:39:12 +02:00
|
|
|
import com.bumptech.glide.Glide;
|
2017-08-03 23:26:26 +02:00
|
|
|
import com.keylesspalace.tusky.R;
|
2017-11-30 20:12:09 +01:00
|
|
|
import com.keylesspalace.tusky.entity.Attachment;
|
Set image previews correctly according to their focal points (#899)
* Add serialization of the meta-data and focus objects
These objects are added in some attachments. This commit adds data
classes which are able to serialize these (partially) in preparation
for the ability to honour the focal point information in image
previews.
* Implement correctly honouring the focal point meta-data in previews
This commit adds code which ensures that the image previews of media
attachments to toots are correctly cropped to always show the focal
point of the image (if it is specified). It should not in any way
influence how previews of media without a focal point are shown.
To achieve the correct crop on the image a few components were
needed:
First of all we needed a way to influence how the image is cropped
into the ImageView. It turns out that the preferred way to do this is
by setting the ScaleType to MATRIX and adjusting the matrix of the
image as needed. This matrix allows us to scale and transform the
image in the way we need to make sure that the focal point is visible
within the view. For this purpose we have the FocalPointEnforcer which
can calculate and set the appropriate matrix on an ImageView as soon
as the image is loaded.
However a second problem is that we need to make sure that this matrix
is updated whenever the size of the ImageView changes. The size might
change for example because the orientation of the device changed from
portrait to landscape or vice versas, or for a number of other reasons
such as the screen being split vertically or something like that.
To be able to hook onto this event we need to create a new extended
version of the ImageView class, which we call
MediaPreviewImageView. This class behaves exactly the same as a normal
ImageView, however if the focalPointEnforcer of this view is set, then
it will call this enforcer to update the image matrix any time the
size is changed.
So this commit changes all media previews in the item_status.xml and
item_status_detailled.xml layout files to the new
MediaPreviewImageView class. Additionally in the code for loading the
images into the previews a new case is added which tests if there is a
focus attribute in the meta-data. If so it makes sure to create and
set the FocalPointEnforcer.
* Fix typos in documentation comment
"to" -> "too"
* Use static imports to remove clutter in FocalPointEnforcerTest
Instead of duplication Assert. in front of every assertEquals, simply
statically import it.
* Move the MetaData and Focus classes into the Attachment class
Since they are very strongly linked to the attachment class and are
themselves very small.
* Refactor the focal point handling code
- All the code modifying the actual members of the
MediaPreviewImageView is now in this class itself. This class still
uses the FocalPointUtil to calculate the new Matrix, but it now
handles setting this new Matrix itself.
- The FocalPointEnforcer has been renamed to the FocalPointUtil to
reflect that it only calculates the correct matrix, but doesn't set
anything on the MediaPreviewImageView.
- The Matrix used to control the cropping of the
MediaPreviewImageViews is now only allocated a single time per view
instead of each time the view is resized. This is done by caching
the Matrix and passing it to the FocalPointUtil to update on each
resize.
* Only reallocate focalMatrix if it is not yet initialized
This helps prevent unnecessary allocations in the case where
setFocalPoint is called multiple times.
* Change checking of availability of objects to use != null
As pointed out, the 'is' keyword is meant for checking types, not for
checking non-nullness.
* Make updateFocalPointMatrix() return nothing
This makes it clearer that it actually mutates the matrix it is
given.
* Fix bug with transitions crashing the PhotoView
Due to the android transitions for some reason copying the scaletype
from the MediaPreviewImageView to the PhotoView during the transition,
the PhotoView would crash on pictures with a focal point, since
PhotoView doesn't support ScaleType.MATRIX.
This is solved by the workaround of overriding both the getScaleType
and setScaleType methods to ensure that we use the MATRIX type in the
preview and the center_crop type in the PhotoView.
Additionally this commit also makes sure to remove the focal point
when the MediaPreviewImageView is recycled.
* Fix bug in overriden getScaleType
Instead of simply returning the scaleType we need to return the
super.getScaleType() method, to avoid crashing.
* Merge changes from master
Mainly the migration to androidx.
2018-12-28 16:32:07 +01:00
|
|
|
import com.keylesspalace.tusky.entity.Attachment.Focus;
|
|
|
|
import com.keylesspalace.tusky.entity.Attachment.MetaData;
|
2018-04-13 22:37:21 +02:00
|
|
|
import com.keylesspalace.tusky.entity.Emoji;
|
2019-04-22 10:11:00 +02:00
|
|
|
import com.keylesspalace.tusky.entity.Poll;
|
|
|
|
import com.keylesspalace.tusky.entity.PollOption;
|
2017-08-03 23:26:26 +02:00
|
|
|
import com.keylesspalace.tusky.entity.Status;
|
|
|
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
2017-11-30 20:12:09 +01:00
|
|
|
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
2017-08-03 23:26:26 +02:00
|
|
|
import com.keylesspalace.tusky.util.DateUtils;
|
2017-11-30 20:12:09 +01:00
|
|
|
import com.keylesspalace.tusky.util.HtmlUtils;
|
2017-08-03 23:26:26 +02:00
|
|
|
import com.keylesspalace.tusky.util.LinkHelper;
|
|
|
|
import com.keylesspalace.tusky.util.ThemeUtils;
|
Set image previews correctly according to their focal points (#899)
* Add serialization of the meta-data and focus objects
These objects are added in some attachments. This commit adds data
classes which are able to serialize these (partially) in preparation
for the ability to honour the focal point information in image
previews.
* Implement correctly honouring the focal point meta-data in previews
This commit adds code which ensures that the image previews of media
attachments to toots are correctly cropped to always show the focal
point of the image (if it is specified). It should not in any way
influence how previews of media without a focal point are shown.
To achieve the correct crop on the image a few components were
needed:
First of all we needed a way to influence how the image is cropped
into the ImageView. It turns out that the preferred way to do this is
by setting the ScaleType to MATRIX and adjusting the matrix of the
image as needed. This matrix allows us to scale and transform the
image in the way we need to make sure that the focal point is visible
within the view. For this purpose we have the FocalPointEnforcer which
can calculate and set the appropriate matrix on an ImageView as soon
as the image is loaded.
However a second problem is that we need to make sure that this matrix
is updated whenever the size of the ImageView changes. The size might
change for example because the orientation of the device changed from
portrait to landscape or vice versas, or for a number of other reasons
such as the screen being split vertically or something like that.
To be able to hook onto this event we need to create a new extended
version of the ImageView class, which we call
MediaPreviewImageView. This class behaves exactly the same as a normal
ImageView, however if the focalPointEnforcer of this view is set, then
it will call this enforcer to update the image matrix any time the
size is changed.
So this commit changes all media previews in the item_status.xml and
item_status_detailled.xml layout files to the new
MediaPreviewImageView class. Additionally in the code for loading the
images into the previews a new case is added which tests if there is a
focus attribute in the meta-data. If so it makes sure to create and
set the FocalPointEnforcer.
* Fix typos in documentation comment
"to" -> "too"
* Use static imports to remove clutter in FocalPointEnforcerTest
Instead of duplication Assert. in front of every assertEquals, simply
statically import it.
* Move the MetaData and Focus classes into the Attachment class
Since they are very strongly linked to the attachment class and are
themselves very small.
* Refactor the focal point handling code
- All the code modifying the actual members of the
MediaPreviewImageView is now in this class itself. This class still
uses the FocalPointUtil to calculate the new Matrix, but it now
handles setting this new Matrix itself.
- The FocalPointEnforcer has been renamed to the FocalPointUtil to
reflect that it only calculates the correct matrix, but doesn't set
anything on the MediaPreviewImageView.
- The Matrix used to control the cropping of the
MediaPreviewImageViews is now only allocated a single time per view
instead of each time the view is resized. This is done by caching
the Matrix and passing it to the FocalPointUtil to update on each
resize.
* Only reallocate focalMatrix if it is not yet initialized
This helps prevent unnecessary allocations in the case where
setFocalPoint is called multiple times.
* Change checking of availability of objects to use != null
As pointed out, the 'is' keyword is meant for checking types, not for
checking non-nullness.
* Make updateFocalPointMatrix() return nothing
This makes it clearer that it actually mutates the matrix it is
given.
* Fix bug with transitions crashing the PhotoView
Due to the android transitions for some reason copying the scaletype
from the MediaPreviewImageView to the PhotoView during the transition,
the PhotoView would crash on pictures with a focal point, since
PhotoView doesn't support ScaleType.MATRIX.
This is solved by the workaround of overriding both the getScaleType
and setScaleType methods to ensure that we use the MATRIX type in the
preview and the center_crop type in the PhotoView.
Additionally this commit also makes sure to remove the focal point
when the MediaPreviewImageView is recycled.
* Fix bug in overriden getScaleType
Instead of simply returning the scaleType we need to return the
super.getScaleType() method, to avoid crashing.
* Merge changes from master
Mainly the migration to androidx.
2018-12-28 16:32:07 +01:00
|
|
|
import com.keylesspalace.tusky.view.MediaPreviewImageView;
|
2017-08-03 23:26:26 +02:00
|
|
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
2018-03-01 21:10:10 +01:00
|
|
|
import com.mikepenz.iconics.utils.Utils;
|
2018-05-10 20:13:25 +02:00
|
|
|
|
2019-03-04 19:24:27 +01:00
|
|
|
import java.text.NumberFormat;
|
2018-08-16 15:51:23 +02:00
|
|
|
import java.text.SimpleDateFormat;
|
2019-04-28 09:58:17 +02:00
|
|
|
import java.util.ArrayList;
|
2019-04-22 10:11:00 +02:00
|
|
|
import java.util.Collections;
|
2017-08-03 23:26:26 +02:00
|
|
|
import java.util.Date;
|
2017-10-19 15:25:04 +02:00
|
|
|
import java.util.List;
|
2018-08-16 15:51:23 +02:00
|
|
|
import java.util.Locale;
|
2017-08-03 23:26:26 +02:00
|
|
|
|
2019-03-04 19:24:27 +01:00
|
|
|
import androidx.annotation.DrawableRes;
|
|
|
|
import androidx.annotation.NonNull;
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
import androidx.appcompat.content.res.AppCompatResources;
|
|
|
|
import androidx.recyclerview.widget.RecyclerView;
|
2018-07-30 15:36:22 +02:00
|
|
|
import at.connyduck.sparkbutton.SparkButton;
|
|
|
|
import at.connyduck.sparkbutton.SparkEventListener;
|
2019-03-04 19:24:27 +01:00
|
|
|
import kotlin.collections.CollectionsKt;
|
2018-07-30 15:36:22 +02:00
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
2019-03-25 13:44:31 +01:00
|
|
|
public static class Key {
|
2019-03-16 14:38:29 +01:00
|
|
|
public static final String KEY_CREATED = "created";
|
|
|
|
}
|
2019-03-25 13:44:31 +01:00
|
|
|
|
2017-08-03 23:26:26 +02:00
|
|
|
private TextView displayName;
|
|
|
|
private TextView username;
|
|
|
|
private ImageButton replyButton;
|
|
|
|
private SparkButton reblogButton;
|
|
|
|
private SparkButton favouriteButton;
|
|
|
|
private ImageButton moreButton;
|
|
|
|
private boolean favourited;
|
|
|
|
private boolean reblogged;
|
2019-02-12 19:22:37 +01:00
|
|
|
protected MediaPreviewImageView[] mediaPreviews;
|
2018-09-28 17:15:01 +02:00
|
|
|
private ImageView[] mediaOverlays;
|
2017-11-30 20:12:09 +01:00
|
|
|
private TextView sensitiveMediaWarning;
|
2017-08-30 14:22:12 +02:00
|
|
|
private View sensitiveMediaShow;
|
2019-02-12 19:22:37 +01:00
|
|
|
protected TextView mediaLabel;
|
2017-08-03 23:26:26 +02:00
|
|
|
private ToggleButton contentWarningButton;
|
2019-04-09 20:13:25 +02:00
|
|
|
protected ImageView avatarInset;
|
2017-08-03 23:26:26 +02:00
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
public ImageView avatar;
|
|
|
|
public TextView timestampInfo;
|
|
|
|
public TextView content;
|
|
|
|
public TextView contentWarningDescription;
|
2017-08-03 23:26:26 +02:00
|
|
|
|
2019-04-22 10:11:00 +02:00
|
|
|
private TextView[] pollResults;
|
|
|
|
private TextView pollDescription;
|
|
|
|
private RadioGroup pollRadioGroup;
|
|
|
|
private RadioButton[] pollRadioOptions;
|
2019-04-28 09:58:17 +02:00
|
|
|
private CheckBox[] pollCheckboxOptions;
|
2019-04-22 10:11:00 +02:00
|
|
|
private Button pollButton;
|
|
|
|
|
2018-08-17 04:53:38 +02:00
|
|
|
private boolean useAbsoluteTime;
|
2018-08-20 15:51:30 +02:00
|
|
|
private SimpleDateFormat shortSdf;
|
|
|
|
private SimpleDateFormat longSdf;
|
2019-04-09 20:13:25 +02:00
|
|
|
private boolean showBotOverlay;
|
2018-08-17 04:53:38 +02:00
|
|
|
|
2019-03-04 19:24:27 +01:00
|
|
|
private final NumberFormat numberFormat = NumberFormat.getNumberInstance();
|
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
protected StatusBaseViewHolder(View itemView, boolean useAbsoluteTime) {
|
2017-08-03 23:26:26 +02:00
|
|
|
super(itemView);
|
2017-10-18 00:20:26 +02:00
|
|
|
displayName = itemView.findViewById(R.id.status_display_name);
|
|
|
|
username = itemView.findViewById(R.id.status_username);
|
2017-11-30 20:12:09 +01:00
|
|
|
timestampInfo = itemView.findViewById(R.id.status_timestamp_info);
|
2017-10-18 00:20:26 +02:00
|
|
|
content = itemView.findViewById(R.id.status_content);
|
|
|
|
avatar = itemView.findViewById(R.id.status_avatar);
|
|
|
|
replyButton = itemView.findViewById(R.id.status_reply);
|
2019-04-09 20:13:25 +02:00
|
|
|
reblogButton = itemView.findViewById(R.id.status_inset);
|
2017-10-18 00:20:26 +02:00
|
|
|
favouriteButton = itemView.findViewById(R.id.status_favourite);
|
|
|
|
moreButton = itemView.findViewById(R.id.status_more);
|
2017-08-03 23:26:26 +02:00
|
|
|
reblogged = false;
|
|
|
|
favourited = false;
|
2019-03-04 19:24:27 +01:00
|
|
|
mediaPreviews = new MediaPreviewImageView[]{
|
2018-09-28 17:15:01 +02:00
|
|
|
itemView.findViewById(R.id.status_media_preview_0),
|
|
|
|
itemView.findViewById(R.id.status_media_preview_1),
|
|
|
|
itemView.findViewById(R.id.status_media_preview_2),
|
|
|
|
itemView.findViewById(R.id.status_media_preview_3)
|
|
|
|
};
|
2018-11-28 19:46:10 +01:00
|
|
|
mediaOverlays = new ImageView[]{
|
2018-09-28 17:15:01 +02:00
|
|
|
itemView.findViewById(R.id.status_media_overlay_0),
|
|
|
|
itemView.findViewById(R.id.status_media_overlay_1),
|
|
|
|
itemView.findViewById(R.id.status_media_overlay_2),
|
|
|
|
itemView.findViewById(R.id.status_media_overlay_3)
|
|
|
|
};
|
2017-08-03 23:26:26 +02:00
|
|
|
sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning);
|
2017-08-30 14:22:12 +02:00
|
|
|
sensitiveMediaShow = itemView.findViewById(R.id.status_sensitive_media_button);
|
2017-10-18 00:20:26 +02:00
|
|
|
mediaLabel = itemView.findViewById(R.id.status_media_label);
|
2017-10-19 15:25:04 +02:00
|
|
|
contentWarningDescription = itemView.findViewById(R.id.status_content_warning_description);
|
|
|
|
contentWarningButton = itemView.findViewById(R.id.status_content_warning_button);
|
2019-04-09 20:13:25 +02:00
|
|
|
avatarInset = itemView.findViewById(R.id.status_avatar_inset);
|
2018-08-17 04:53:38 +02:00
|
|
|
|
2019-04-22 10:11:00 +02:00
|
|
|
pollResults = new TextView[] {
|
|
|
|
itemView.findViewById(R.id.status_poll_option_result_0),
|
|
|
|
itemView.findViewById(R.id.status_poll_option_result_1),
|
|
|
|
itemView.findViewById(R.id.status_poll_option_result_2),
|
|
|
|
itemView.findViewById(R.id.status_poll_option_result_3)
|
|
|
|
};
|
|
|
|
|
|
|
|
pollDescription = itemView.findViewById(R.id.status_poll_description);
|
|
|
|
|
|
|
|
pollRadioGroup = itemView.findViewById(R.id.status_poll_radio_group);
|
|
|
|
pollRadioOptions = new RadioButton[] {
|
|
|
|
pollRadioGroup.findViewById(R.id.status_poll_radio_button_0),
|
|
|
|
pollRadioGroup.findViewById(R.id.status_poll_radio_button_1),
|
|
|
|
pollRadioGroup.findViewById(R.id.status_poll_radio_button_2),
|
|
|
|
pollRadioGroup.findViewById(R.id.status_poll_radio_button_3)
|
|
|
|
};
|
2019-04-28 09:58:17 +02:00
|
|
|
pollCheckboxOptions = new CheckBox[] {
|
|
|
|
itemView.findViewById(R.id.status_poll_checkbox_0),
|
|
|
|
itemView.findViewById(R.id.status_poll_checkbox_1),
|
|
|
|
itemView.findViewById(R.id.status_poll_checkbox_2),
|
|
|
|
itemView.findViewById(R.id.status_poll_checkbox_3)
|
|
|
|
};
|
2019-04-22 10:11:00 +02:00
|
|
|
|
|
|
|
pollButton = itemView.findViewById(R.id.status_poll_button);
|
|
|
|
|
2018-08-17 04:53:38 +02:00
|
|
|
this.useAbsoluteTime = useAbsoluteTime;
|
2018-08-20 15:51:30 +02:00
|
|
|
shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
|
|
|
|
longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
|
2019-04-09 20:13:25 +02:00
|
|
|
showBotOverlay = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()).getBoolean("showBotOverlay", true);
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
|
|
|
|
2017-11-30 20:12:09 +01:00
|
|
|
protected abstract int getMediaPreviewHeight(Context context);
|
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
protected void setDisplayName(String name, List<Emoji> customEmojis) {
|
2018-06-24 09:53:23 +02:00
|
|
|
CharSequence emojifiedName = CustomEmojiHelper.emojifyString(name, customEmojis, displayName);
|
|
|
|
displayName.setText(emojifiedName);
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
protected void setUsername(String name) {
|
2017-08-03 23:26:26 +02:00
|
|
|
Context context = username.getContext();
|
|
|
|
String format = context.getString(R.string.status_username_format);
|
|
|
|
String usernameText = String.format(format, name);
|
|
|
|
username.setText(usernameText);
|
|
|
|
}
|
|
|
|
|
2019-03-30 09:10:34 +01:00
|
|
|
public void toggleContentWarning() {
|
|
|
|
contentWarningButton.toggle();
|
|
|
|
}
|
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
protected void setSpoilerAndContent(boolean expanded,
|
2019-03-04 19:24:27 +01:00
|
|
|
@NonNull Spanned content,
|
2019-02-12 19:22:37 +01:00
|
|
|
@Nullable String spoilerText,
|
2019-03-04 19:24:27 +01:00
|
|
|
@Nullable Status.Mention[] mentions,
|
|
|
|
@NonNull List<Emoji> emojis,
|
2019-02-12 19:22:37 +01:00
|
|
|
final StatusActionListener listener) {
|
|
|
|
if (TextUtils.isEmpty(spoilerText)) {
|
2018-12-16 10:30:40 +01:00
|
|
|
contentWarningDescription.setVisibility(View.GONE);
|
|
|
|
contentWarningButton.setVisibility(View.GONE);
|
2019-02-12 19:22:37 +01:00
|
|
|
this.setTextVisible(true, content, mentions, emojis, listener);
|
2018-12-16 10:30:40 +01:00
|
|
|
} else {
|
2019-02-12 19:22:37 +01:00
|
|
|
CharSequence emojiSpoiler = CustomEmojiHelper.emojifyString(spoilerText, emojis, contentWarningDescription);
|
2018-12-16 10:30:40 +01:00
|
|
|
contentWarningDescription.setText(emojiSpoiler);
|
|
|
|
contentWarningDescription.setVisibility(View.VISIBLE);
|
|
|
|
contentWarningButton.setVisibility(View.VISIBLE);
|
|
|
|
contentWarningButton.setChecked(expanded);
|
|
|
|
contentWarningButton.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
|
|
|
contentWarningDescription.invalidate();
|
|
|
|
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
|
|
|
listener.onExpandedChange(isChecked, getAdapterPosition());
|
|
|
|
}
|
2019-02-12 19:22:37 +01:00
|
|
|
this.setTextVisible(isChecked, content, mentions, emojis, listener);
|
2018-12-16 10:30:40 +01:00
|
|
|
});
|
2019-02-12 19:22:37 +01:00
|
|
|
this.setTextVisible(expanded, content, mentions, emojis, listener);
|
2018-12-16 10:30:40 +01:00
|
|
|
}
|
|
|
|
}
|
2017-10-19 15:25:04 +02:00
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
private void setTextVisible(boolean expanded,
|
|
|
|
Spanned content,
|
|
|
|
Status.Mention[] mentions,
|
|
|
|
List<Emoji> emojis,
|
2018-12-16 10:30:40 +01:00
|
|
|
final StatusActionListener listener) {
|
2018-12-27 22:32:14 +01:00
|
|
|
if (expanded) {
|
2019-02-12 19:22:37 +01:00
|
|
|
Spanned emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, this.content);
|
2018-12-16 10:30:40 +01:00
|
|
|
LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener);
|
|
|
|
} else {
|
2019-03-04 19:24:27 +01:00
|
|
|
LinkHelper.setClickableMentions(this.content, mentions, listener);
|
2018-12-27 22:32:14 +01:00
|
|
|
}
|
2019-03-04 19:24:27 +01:00
|
|
|
if (TextUtils.isEmpty(this.content.getText())) {
|
2018-12-27 22:32:14 +01:00
|
|
|
this.content.setVisibility(View.GONE);
|
|
|
|
} else {
|
|
|
|
this.content.setVisibility(View.VISIBLE);
|
2018-12-16 10:30:40 +01:00
|
|
|
}
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
|
|
|
|
2019-04-09 20:13:25 +02:00
|
|
|
protected void setAvatar(String url, @Nullable String rebloggedUrl, boolean isBot) {
|
2018-03-15 22:08:12 +01:00
|
|
|
if (TextUtils.isEmpty(url)) {
|
2017-08-03 23:26:26 +02:00
|
|
|
avatar.setImageResource(R.drawable.avatar_default);
|
|
|
|
} else {
|
2019-04-16 21:39:12 +02:00
|
|
|
Glide.with(avatar)
|
2017-08-03 23:26:26 +02:00
|
|
|
.load(url)
|
|
|
|
.placeholder(R.drawable.avatar_default)
|
|
|
|
.into(avatar);
|
|
|
|
}
|
2019-04-09 20:13:25 +02:00
|
|
|
|
|
|
|
if (showBotOverlay && isBot && TextUtils.isEmpty(rebloggedUrl)) {
|
|
|
|
avatarInset.setVisibility(View.VISIBLE);
|
|
|
|
avatarInset.setImageResource(R.drawable.ic_bot_24dp);
|
|
|
|
avatarInset.setBackgroundColor(0x50ffffff);
|
|
|
|
} else {
|
2019-04-21 22:59:58 +02:00
|
|
|
avatarInset.setBackground(null);
|
2019-04-09 20:13:25 +02:00
|
|
|
avatarInset.setVisibility(View.GONE);
|
|
|
|
}
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
protected void setCreatedAt(@Nullable Date createdAt) {
|
2018-08-17 04:53:38 +02:00
|
|
|
if (useAbsoluteTime) {
|
2019-03-04 19:24:27 +01:00
|
|
|
timestampInfo.setText(getAbsoluteTime(createdAt));
|
|
|
|
} else {
|
|
|
|
String readout;
|
2018-08-16 15:51:23 +02:00
|
|
|
if (createdAt != null) {
|
2019-03-04 19:24:27 +01:00
|
|
|
long then = createdAt.getTime();
|
|
|
|
long now = new Date().getTime();
|
|
|
|
readout = DateUtils.getRelativeTimeSpanString(timestampInfo.getContext(), then, now);
|
2018-08-20 12:29:58 +02:00
|
|
|
} else {
|
2019-03-04 19:24:27 +01:00
|
|
|
// unknown minutes~
|
|
|
|
readout = "?m";
|
|
|
|
}
|
|
|
|
timestampInfo.setText(readout);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private String getAbsoluteTime(@Nullable Date createdAt) {
|
|
|
|
String time;
|
|
|
|
if (createdAt != null) {
|
2019-04-22 10:11:00 +02:00
|
|
|
if (android.text.format.DateUtils.isToday(createdAt.getTime())) {
|
2019-03-04 19:24:27 +01:00
|
|
|
time = shortSdf.format(createdAt);
|
2019-04-22 10:11:00 +02:00
|
|
|
} else {
|
|
|
|
time = longSdf.format(createdAt);
|
2018-08-16 15:51:23 +02:00
|
|
|
}
|
2017-08-03 23:26:26 +02:00
|
|
|
} else {
|
2019-03-04 19:24:27 +01:00
|
|
|
time = "??:??:??";
|
|
|
|
}
|
|
|
|
return time;
|
|
|
|
}
|
|
|
|
|
|
|
|
private CharSequence getCreatedAtDescription(@Nullable Date createdAt) {
|
|
|
|
if (useAbsoluteTime) {
|
|
|
|
return getAbsoluteTime(createdAt);
|
|
|
|
} else {
|
2018-08-16 15:51:23 +02:00
|
|
|
/* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m"
|
|
|
|
* as 17 meters instead of minutes. */
|
2019-03-04 19:24:27 +01:00
|
|
|
|
2018-08-16 15:51:23 +02:00
|
|
|
if (createdAt != null) {
|
|
|
|
long then = createdAt.getTime();
|
|
|
|
long now = new Date().getTime();
|
2019-03-04 19:24:27 +01:00
|
|
|
return android.text.format.DateUtils.getRelativeTimeSpanString(then, now,
|
2018-08-16 15:51:23 +02:00
|
|
|
android.text.format.DateUtils.SECOND_IN_MILLIS,
|
|
|
|
android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE);
|
|
|
|
} else {
|
|
|
|
// unknown minutes~
|
2019-03-04 19:24:27 +01:00
|
|
|
return "? minutes";
|
2018-08-16 15:51:23 +02:00
|
|
|
}
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
2017-11-30 20:12:09 +01:00
|
|
|
}
|
|
|
|
|
2018-03-01 21:10:10 +01:00
|
|
|
protected void showContent(boolean show) {
|
2018-05-10 20:13:25 +02:00
|
|
|
if (show) {
|
2018-09-28 17:15:01 +02:00
|
|
|
itemView.setVisibility(View.VISIBLE);
|
|
|
|
itemView.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
2018-03-01 21:10:10 +01:00
|
|
|
} else {
|
2018-09-28 17:15:01 +02:00
|
|
|
itemView.setVisibility(View.INVISIBLE);
|
|
|
|
itemView.getLayoutParams().height = Utils.convertDpToPx(itemView.getContext(), 24);
|
2018-03-01 21:10:10 +01:00
|
|
|
}
|
|
|
|
}
|
2017-11-30 20:12:09 +01:00
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
protected void setIsReply(boolean isReply) {
|
2018-05-10 20:13:25 +02:00
|
|
|
if (isReply) {
|
2017-11-30 20:12:09 +01:00
|
|
|
replyButton.setImageResource(R.drawable.ic_reply_all_24dp);
|
|
|
|
} else {
|
|
|
|
replyButton.setImageResource(R.drawable.ic_reply_24dp);
|
|
|
|
}
|
|
|
|
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private void setReblogged(boolean reblogged) {
|
|
|
|
this.reblogged = reblogged;
|
|
|
|
reblogButton.setChecked(reblogged);
|
|
|
|
}
|
|
|
|
|
|
|
|
// This should only be called after setReblogged, in order to override the tint correctly.
|
|
|
|
private void setRebloggingEnabled(boolean enabled, Status.Visibility visibility) {
|
2018-05-28 21:29:06 +02:00
|
|
|
reblogButton.setEnabled(enabled && visibility != Status.Visibility.PRIVATE);
|
2017-08-03 23:26:26 +02:00
|
|
|
|
|
|
|
if (enabled) {
|
2018-05-28 21:29:06 +02:00
|
|
|
int inactiveId;
|
|
|
|
int activeId;
|
|
|
|
if (visibility == Status.Visibility.PRIVATE) {
|
|
|
|
inactiveId = ThemeUtils.getDrawableId(reblogButton.getContext(),
|
|
|
|
R.attr.status_reblog_disabled_drawable, R.drawable.reblog_private_dark);
|
|
|
|
activeId = R.drawable.reblog_private_active;
|
|
|
|
} else {
|
|
|
|
inactiveId = ThemeUtils.getDrawableId(reblogButton.getContext(),
|
|
|
|
R.attr.status_reblog_inactive_drawable, R.drawable.reblog_inactive_dark);
|
|
|
|
activeId = R.drawable.reblog_active;
|
|
|
|
}
|
2017-08-03 23:26:26 +02:00
|
|
|
reblogButton.setInactiveImage(inactiveId);
|
2018-05-28 21:29:06 +02:00
|
|
|
reblogButton.setActiveImage(activeId);
|
2017-08-03 23:26:26 +02:00
|
|
|
} else {
|
|
|
|
int disabledId;
|
|
|
|
if (visibility == Status.Visibility.DIRECT) {
|
|
|
|
disabledId = ThemeUtils.getDrawableId(reblogButton.getContext(),
|
|
|
|
R.attr.status_reblog_direct_drawable, R.drawable.reblog_direct_dark);
|
|
|
|
} else {
|
|
|
|
disabledId = ThemeUtils.getDrawableId(reblogButton.getContext(),
|
2018-05-28 21:29:06 +02:00
|
|
|
R.attr.status_reblog_disabled_drawable, R.drawable.reblog_private_dark);
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
|
|
|
reblogButton.setInactiveImage(disabledId);
|
|
|
|
reblogButton.setActiveImage(disabledId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
protected void setFavourited(boolean favourited) {
|
2017-08-03 23:26:26 +02:00
|
|
|
this.favourited = favourited;
|
|
|
|
favouriteButton.setChecked(favourited);
|
|
|
|
}
|
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
protected void setMediaPreviews(final List<Attachment> attachments, boolean sensitive,
|
|
|
|
final StatusActionListener listener, boolean showingContent) {
|
2018-09-28 17:15:01 +02:00
|
|
|
|
|
|
|
Context context = itemView.getContext();
|
2017-08-03 23:26:26 +02:00
|
|
|
|
2017-10-19 15:25:04 +02:00
|
|
|
int mediaPreviewUnloadedId =
|
|
|
|
ThemeUtils.getDrawableId(itemView.getContext(), R.attr.media_preview_unloaded_drawable,
|
|
|
|
android.R.color.black);
|
2017-08-03 23:26:26 +02:00
|
|
|
|
2018-05-10 20:13:25 +02:00
|
|
|
final int n = Math.min(attachments.size(), Status.MAX_MEDIA_ATTACHMENTS);
|
2017-08-03 23:26:26 +02:00
|
|
|
|
|
|
|
for (int i = 0; i < n; i++) {
|
2018-05-10 20:13:25 +02:00
|
|
|
String previewUrl = attachments.get(i).getPreviewUrl();
|
|
|
|
String description = attachments.get(i).getDescription();
|
2017-11-30 20:12:09 +01:00
|
|
|
|
2018-05-10 20:13:25 +02:00
|
|
|
if (TextUtils.isEmpty(description)) {
|
2018-09-28 17:15:01 +02:00
|
|
|
mediaPreviews[i].setContentDescription(context.getString(R.string.action_view_media));
|
2017-11-30 20:12:09 +01:00
|
|
|
} else {
|
2018-09-28 17:15:01 +02:00
|
|
|
mediaPreviews[i].setContentDescription(description);
|
2017-11-30 20:12:09 +01:00
|
|
|
}
|
2017-08-03 23:26:26 +02:00
|
|
|
|
2018-09-28 17:15:01 +02:00
|
|
|
mediaPreviews[i].setVisibility(View.VISIBLE);
|
2017-08-03 23:26:26 +02:00
|
|
|
|
2018-03-30 19:46:36 +02:00
|
|
|
if (TextUtils.isEmpty(previewUrl)) {
|
2019-04-16 21:39:12 +02:00
|
|
|
Glide.with(mediaPreviews[i])
|
2018-03-30 19:46:36 +02:00
|
|
|
.load(mediaPreviewUnloadedId)
|
2019-02-16 14:31:41 +01:00
|
|
|
.centerInside()
|
2018-09-28 17:15:01 +02:00
|
|
|
.into(mediaPreviews[i]);
|
2017-08-03 23:26:26 +02:00
|
|
|
} else {
|
Set image previews correctly according to their focal points (#899)
* Add serialization of the meta-data and focus objects
These objects are added in some attachments. This commit adds data
classes which are able to serialize these (partially) in preparation
for the ability to honour the focal point information in image
previews.
* Implement correctly honouring the focal point meta-data in previews
This commit adds code which ensures that the image previews of media
attachments to toots are correctly cropped to always show the focal
point of the image (if it is specified). It should not in any way
influence how previews of media without a focal point are shown.
To achieve the correct crop on the image a few components were
needed:
First of all we needed a way to influence how the image is cropped
into the ImageView. It turns out that the preferred way to do this is
by setting the ScaleType to MATRIX and adjusting the matrix of the
image as needed. This matrix allows us to scale and transform the
image in the way we need to make sure that the focal point is visible
within the view. For this purpose we have the FocalPointEnforcer which
can calculate and set the appropriate matrix on an ImageView as soon
as the image is loaded.
However a second problem is that we need to make sure that this matrix
is updated whenever the size of the ImageView changes. The size might
change for example because the orientation of the device changed from
portrait to landscape or vice versas, or for a number of other reasons
such as the screen being split vertically or something like that.
To be able to hook onto this event we need to create a new extended
version of the ImageView class, which we call
MediaPreviewImageView. This class behaves exactly the same as a normal
ImageView, however if the focalPointEnforcer of this view is set, then
it will call this enforcer to update the image matrix any time the
size is changed.
So this commit changes all media previews in the item_status.xml and
item_status_detailled.xml layout files to the new
MediaPreviewImageView class. Additionally in the code for loading the
images into the previews a new case is added which tests if there is a
focus attribute in the meta-data. If so it makes sure to create and
set the FocalPointEnforcer.
* Fix typos in documentation comment
"to" -> "too"
* Use static imports to remove clutter in FocalPointEnforcerTest
Instead of duplication Assert. in front of every assertEquals, simply
statically import it.
* Move the MetaData and Focus classes into the Attachment class
Since they are very strongly linked to the attachment class and are
themselves very small.
* Refactor the focal point handling code
- All the code modifying the actual members of the
MediaPreviewImageView is now in this class itself. This class still
uses the FocalPointUtil to calculate the new Matrix, but it now
handles setting this new Matrix itself.
- The FocalPointEnforcer has been renamed to the FocalPointUtil to
reflect that it only calculates the correct matrix, but doesn't set
anything on the MediaPreviewImageView.
- The Matrix used to control the cropping of the
MediaPreviewImageViews is now only allocated a single time per view
instead of each time the view is resized. This is done by caching
the Matrix and passing it to the FocalPointUtil to update on each
resize.
* Only reallocate focalMatrix if it is not yet initialized
This helps prevent unnecessary allocations in the case where
setFocalPoint is called multiple times.
* Change checking of availability of objects to use != null
As pointed out, the 'is' keyword is meant for checking types, not for
checking non-nullness.
* Make updateFocalPointMatrix() return nothing
This makes it clearer that it actually mutates the matrix it is
given.
* Fix bug with transitions crashing the PhotoView
Due to the android transitions for some reason copying the scaletype
from the MediaPreviewImageView to the PhotoView during the transition,
the PhotoView would crash on pictures with a focal point, since
PhotoView doesn't support ScaleType.MATRIX.
This is solved by the workaround of overriding both the getScaleType
and setScaleType methods to ensure that we use the MATRIX type in the
preview and the center_crop type in the PhotoView.
Additionally this commit also makes sure to remove the focal point
when the MediaPreviewImageView is recycled.
* Fix bug in overriden getScaleType
Instead of simply returning the scaleType we need to return the
super.getScaleType() method, to avoid crashing.
* Merge changes from master
Mainly the migration to androidx.
2018-12-28 16:32:07 +01:00
|
|
|
MetaData meta = attachments.get(i).getMeta();
|
|
|
|
Focus focus = meta != null ? meta.getFocus() : null;
|
|
|
|
|
|
|
|
if (focus != null) { // If there is a focal point for this attachment:
|
|
|
|
mediaPreviews[i].setFocalPoint(focus);
|
|
|
|
|
2019-04-16 21:39:12 +02:00
|
|
|
Glide.with(mediaPreviews[i])
|
Set image previews correctly according to their focal points (#899)
* Add serialization of the meta-data and focus objects
These objects are added in some attachments. This commit adds data
classes which are able to serialize these (partially) in preparation
for the ability to honour the focal point information in image
previews.
* Implement correctly honouring the focal point meta-data in previews
This commit adds code which ensures that the image previews of media
attachments to toots are correctly cropped to always show the focal
point of the image (if it is specified). It should not in any way
influence how previews of media without a focal point are shown.
To achieve the correct crop on the image a few components were
needed:
First of all we needed a way to influence how the image is cropped
into the ImageView. It turns out that the preferred way to do this is
by setting the ScaleType to MATRIX and adjusting the matrix of the
image as needed. This matrix allows us to scale and transform the
image in the way we need to make sure that the focal point is visible
within the view. For this purpose we have the FocalPointEnforcer which
can calculate and set the appropriate matrix on an ImageView as soon
as the image is loaded.
However a second problem is that we need to make sure that this matrix
is updated whenever the size of the ImageView changes. The size might
change for example because the orientation of the device changed from
portrait to landscape or vice versas, or for a number of other reasons
such as the screen being split vertically or something like that.
To be able to hook onto this event we need to create a new extended
version of the ImageView class, which we call
MediaPreviewImageView. This class behaves exactly the same as a normal
ImageView, however if the focalPointEnforcer of this view is set, then
it will call this enforcer to update the image matrix any time the
size is changed.
So this commit changes all media previews in the item_status.xml and
item_status_detailled.xml layout files to the new
MediaPreviewImageView class. Additionally in the code for loading the
images into the previews a new case is added which tests if there is a
focus attribute in the meta-data. If so it makes sure to create and
set the FocalPointEnforcer.
* Fix typos in documentation comment
"to" -> "too"
* Use static imports to remove clutter in FocalPointEnforcerTest
Instead of duplication Assert. in front of every assertEquals, simply
statically import it.
* Move the MetaData and Focus classes into the Attachment class
Since they are very strongly linked to the attachment class and are
themselves very small.
* Refactor the focal point handling code
- All the code modifying the actual members of the
MediaPreviewImageView is now in this class itself. This class still
uses the FocalPointUtil to calculate the new Matrix, but it now
handles setting this new Matrix itself.
- The FocalPointEnforcer has been renamed to the FocalPointUtil to
reflect that it only calculates the correct matrix, but doesn't set
anything on the MediaPreviewImageView.
- The Matrix used to control the cropping of the
MediaPreviewImageViews is now only allocated a single time per view
instead of each time the view is resized. This is done by caching
the Matrix and passing it to the FocalPointUtil to update on each
resize.
* Only reallocate focalMatrix if it is not yet initialized
This helps prevent unnecessary allocations in the case where
setFocalPoint is called multiple times.
* Change checking of availability of objects to use != null
As pointed out, the 'is' keyword is meant for checking types, not for
checking non-nullness.
* Make updateFocalPointMatrix() return nothing
This makes it clearer that it actually mutates the matrix it is
given.
* Fix bug with transitions crashing the PhotoView
Due to the android transitions for some reason copying the scaletype
from the MediaPreviewImageView to the PhotoView during the transition,
the PhotoView would crash on pictures with a focal point, since
PhotoView doesn't support ScaleType.MATRIX.
This is solved by the workaround of overriding both the getScaleType
and setScaleType methods to ensure that we use the MATRIX type in the
preview and the center_crop type in the PhotoView.
Additionally this commit also makes sure to remove the focal point
when the MediaPreviewImageView is recycled.
* Fix bug in overriden getScaleType
Instead of simply returning the scaleType we need to return the
super.getScaleType() method, to avoid crashing.
* Merge changes from master
Mainly the migration to androidx.
2018-12-28 16:32:07 +01:00
|
|
|
.load(previewUrl)
|
|
|
|
.placeholder(mediaPreviewUnloadedId)
|
2019-02-16 14:31:41 +01:00
|
|
|
.centerInside()
|
2019-04-16 21:39:12 +02:00
|
|
|
.addListener(mediaPreviews[i])
|
|
|
|
.into(mediaPreviews[i]);
|
Set image previews correctly according to their focal points (#899)
* Add serialization of the meta-data and focus objects
These objects are added in some attachments. This commit adds data
classes which are able to serialize these (partially) in preparation
for the ability to honour the focal point information in image
previews.
* Implement correctly honouring the focal point meta-data in previews
This commit adds code which ensures that the image previews of media
attachments to toots are correctly cropped to always show the focal
point of the image (if it is specified). It should not in any way
influence how previews of media without a focal point are shown.
To achieve the correct crop on the image a few components were
needed:
First of all we needed a way to influence how the image is cropped
into the ImageView. It turns out that the preferred way to do this is
by setting the ScaleType to MATRIX and adjusting the matrix of the
image as needed. This matrix allows us to scale and transform the
image in the way we need to make sure that the focal point is visible
within the view. For this purpose we have the FocalPointEnforcer which
can calculate and set the appropriate matrix on an ImageView as soon
as the image is loaded.
However a second problem is that we need to make sure that this matrix
is updated whenever the size of the ImageView changes. The size might
change for example because the orientation of the device changed from
portrait to landscape or vice versas, or for a number of other reasons
such as the screen being split vertically or something like that.
To be able to hook onto this event we need to create a new extended
version of the ImageView class, which we call
MediaPreviewImageView. This class behaves exactly the same as a normal
ImageView, however if the focalPointEnforcer of this view is set, then
it will call this enforcer to update the image matrix any time the
size is changed.
So this commit changes all media previews in the item_status.xml and
item_status_detailled.xml layout files to the new
MediaPreviewImageView class. Additionally in the code for loading the
images into the previews a new case is added which tests if there is a
focus attribute in the meta-data. If so it makes sure to create and
set the FocalPointEnforcer.
* Fix typos in documentation comment
"to" -> "too"
* Use static imports to remove clutter in FocalPointEnforcerTest
Instead of duplication Assert. in front of every assertEquals, simply
statically import it.
* Move the MetaData and Focus classes into the Attachment class
Since they are very strongly linked to the attachment class and are
themselves very small.
* Refactor the focal point handling code
- All the code modifying the actual members of the
MediaPreviewImageView is now in this class itself. This class still
uses the FocalPointUtil to calculate the new Matrix, but it now
handles setting this new Matrix itself.
- The FocalPointEnforcer has been renamed to the FocalPointUtil to
reflect that it only calculates the correct matrix, but doesn't set
anything on the MediaPreviewImageView.
- The Matrix used to control the cropping of the
MediaPreviewImageViews is now only allocated a single time per view
instead of each time the view is resized. This is done by caching
the Matrix and passing it to the FocalPointUtil to update on each
resize.
* Only reallocate focalMatrix if it is not yet initialized
This helps prevent unnecessary allocations in the case where
setFocalPoint is called multiple times.
* Change checking of availability of objects to use != null
As pointed out, the 'is' keyword is meant for checking types, not for
checking non-nullness.
* Make updateFocalPointMatrix() return nothing
This makes it clearer that it actually mutates the matrix it is
given.
* Fix bug with transitions crashing the PhotoView
Due to the android transitions for some reason copying the scaletype
from the MediaPreviewImageView to the PhotoView during the transition,
the PhotoView would crash on pictures with a focal point, since
PhotoView doesn't support ScaleType.MATRIX.
This is solved by the workaround of overriding both the getScaleType
and setScaleType methods to ensure that we use the MATRIX type in the
preview and the center_crop type in the PhotoView.
Additionally this commit also makes sure to remove the focal point
when the MediaPreviewImageView is recycled.
* Fix bug in overriden getScaleType
Instead of simply returning the scaleType we need to return the
super.getScaleType() method, to avoid crashing.
* Merge changes from master
Mainly the migration to androidx.
2018-12-28 16:32:07 +01:00
|
|
|
} else {
|
|
|
|
mediaPreviews[i].removeFocalPoint();
|
|
|
|
|
2019-04-16 21:39:12 +02:00
|
|
|
Glide.with(mediaPreviews[i])
|
Set image previews correctly according to their focal points (#899)
* Add serialization of the meta-data and focus objects
These objects are added in some attachments. This commit adds data
classes which are able to serialize these (partially) in preparation
for the ability to honour the focal point information in image
previews.
* Implement correctly honouring the focal point meta-data in previews
This commit adds code which ensures that the image previews of media
attachments to toots are correctly cropped to always show the focal
point of the image (if it is specified). It should not in any way
influence how previews of media without a focal point are shown.
To achieve the correct crop on the image a few components were
needed:
First of all we needed a way to influence how the image is cropped
into the ImageView. It turns out that the preferred way to do this is
by setting the ScaleType to MATRIX and adjusting the matrix of the
image as needed. This matrix allows us to scale and transform the
image in the way we need to make sure that the focal point is visible
within the view. For this purpose we have the FocalPointEnforcer which
can calculate and set the appropriate matrix on an ImageView as soon
as the image is loaded.
However a second problem is that we need to make sure that this matrix
is updated whenever the size of the ImageView changes. The size might
change for example because the orientation of the device changed from
portrait to landscape or vice versas, or for a number of other reasons
such as the screen being split vertically or something like that.
To be able to hook onto this event we need to create a new extended
version of the ImageView class, which we call
MediaPreviewImageView. This class behaves exactly the same as a normal
ImageView, however if the focalPointEnforcer of this view is set, then
it will call this enforcer to update the image matrix any time the
size is changed.
So this commit changes all media previews in the item_status.xml and
item_status_detailled.xml layout files to the new
MediaPreviewImageView class. Additionally in the code for loading the
images into the previews a new case is added which tests if there is a
focus attribute in the meta-data. If so it makes sure to create and
set the FocalPointEnforcer.
* Fix typos in documentation comment
"to" -> "too"
* Use static imports to remove clutter in FocalPointEnforcerTest
Instead of duplication Assert. in front of every assertEquals, simply
statically import it.
* Move the MetaData and Focus classes into the Attachment class
Since they are very strongly linked to the attachment class and are
themselves very small.
* Refactor the focal point handling code
- All the code modifying the actual members of the
MediaPreviewImageView is now in this class itself. This class still
uses the FocalPointUtil to calculate the new Matrix, but it now
handles setting this new Matrix itself.
- The FocalPointEnforcer has been renamed to the FocalPointUtil to
reflect that it only calculates the correct matrix, but doesn't set
anything on the MediaPreviewImageView.
- The Matrix used to control the cropping of the
MediaPreviewImageViews is now only allocated a single time per view
instead of each time the view is resized. This is done by caching
the Matrix and passing it to the FocalPointUtil to update on each
resize.
* Only reallocate focalMatrix if it is not yet initialized
This helps prevent unnecessary allocations in the case where
setFocalPoint is called multiple times.
* Change checking of availability of objects to use != null
As pointed out, the 'is' keyword is meant for checking types, not for
checking non-nullness.
* Make updateFocalPointMatrix() return nothing
This makes it clearer that it actually mutates the matrix it is
given.
* Fix bug with transitions crashing the PhotoView
Due to the android transitions for some reason copying the scaletype
from the MediaPreviewImageView to the PhotoView during the transition,
the PhotoView would crash on pictures with a focal point, since
PhotoView doesn't support ScaleType.MATRIX.
This is solved by the workaround of overriding both the getScaleType
and setScaleType methods to ensure that we use the MATRIX type in the
preview and the center_crop type in the PhotoView.
Additionally this commit also makes sure to remove the focal point
when the MediaPreviewImageView is recycled.
* Fix bug in overriden getScaleType
Instead of simply returning the scaleType we need to return the
super.getScaleType() method, to avoid crashing.
* Merge changes from master
Mainly the migration to androidx.
2018-12-28 16:32:07 +01:00
|
|
|
.load(previewUrl)
|
|
|
|
.placeholder(mediaPreviewUnloadedId)
|
2019-02-16 14:31:41 +01:00
|
|
|
.centerInside()
|
Set image previews correctly according to their focal points (#899)
* Add serialization of the meta-data and focus objects
These objects are added in some attachments. This commit adds data
classes which are able to serialize these (partially) in preparation
for the ability to honour the focal point information in image
previews.
* Implement correctly honouring the focal point meta-data in previews
This commit adds code which ensures that the image previews of media
attachments to toots are correctly cropped to always show the focal
point of the image (if it is specified). It should not in any way
influence how previews of media without a focal point are shown.
To achieve the correct crop on the image a few components were
needed:
First of all we needed a way to influence how the image is cropped
into the ImageView. It turns out that the preferred way to do this is
by setting the ScaleType to MATRIX and adjusting the matrix of the
image as needed. This matrix allows us to scale and transform the
image in the way we need to make sure that the focal point is visible
within the view. For this purpose we have the FocalPointEnforcer which
can calculate and set the appropriate matrix on an ImageView as soon
as the image is loaded.
However a second problem is that we need to make sure that this matrix
is updated whenever the size of the ImageView changes. The size might
change for example because the orientation of the device changed from
portrait to landscape or vice versas, or for a number of other reasons
such as the screen being split vertically or something like that.
To be able to hook onto this event we need to create a new extended
version of the ImageView class, which we call
MediaPreviewImageView. This class behaves exactly the same as a normal
ImageView, however if the focalPointEnforcer of this view is set, then
it will call this enforcer to update the image matrix any time the
size is changed.
So this commit changes all media previews in the item_status.xml and
item_status_detailled.xml layout files to the new
MediaPreviewImageView class. Additionally in the code for loading the
images into the previews a new case is added which tests if there is a
focus attribute in the meta-data. If so it makes sure to create and
set the FocalPointEnforcer.
* Fix typos in documentation comment
"to" -> "too"
* Use static imports to remove clutter in FocalPointEnforcerTest
Instead of duplication Assert. in front of every assertEquals, simply
statically import it.
* Move the MetaData and Focus classes into the Attachment class
Since they are very strongly linked to the attachment class and are
themselves very small.
* Refactor the focal point handling code
- All the code modifying the actual members of the
MediaPreviewImageView is now in this class itself. This class still
uses the FocalPointUtil to calculate the new Matrix, but it now
handles setting this new Matrix itself.
- The FocalPointEnforcer has been renamed to the FocalPointUtil to
reflect that it only calculates the correct matrix, but doesn't set
anything on the MediaPreviewImageView.
- The Matrix used to control the cropping of the
MediaPreviewImageViews is now only allocated a single time per view
instead of each time the view is resized. This is done by caching
the Matrix and passing it to the FocalPointUtil to update on each
resize.
* Only reallocate focalMatrix if it is not yet initialized
This helps prevent unnecessary allocations in the case where
setFocalPoint is called multiple times.
* Change checking of availability of objects to use != null
As pointed out, the 'is' keyword is meant for checking types, not for
checking non-nullness.
* Make updateFocalPointMatrix() return nothing
This makes it clearer that it actually mutates the matrix it is
given.
* Fix bug with transitions crashing the PhotoView
Due to the android transitions for some reason copying the scaletype
from the MediaPreviewImageView to the PhotoView during the transition,
the PhotoView would crash on pictures with a focal point, since
PhotoView doesn't support ScaleType.MATRIX.
This is solved by the workaround of overriding both the getScaleType
and setScaleType methods to ensure that we use the MATRIX type in the
preview and the center_crop type in the PhotoView.
Additionally this commit also makes sure to remove the focal point
when the MediaPreviewImageView is recycled.
* Fix bug in overriden getScaleType
Instead of simply returning the scaleType we need to return the
super.getScaleType() method, to avoid crashing.
* Merge changes from master
Mainly the migration to androidx.
2018-12-28 16:32:07 +01:00
|
|
|
.into(mediaPreviews[i]);
|
|
|
|
}
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
|
|
|
|
2018-05-10 20:13:25 +02:00
|
|
|
final Attachment.Type type = attachments.get(i).getType();
|
2017-11-30 20:12:09 +01:00
|
|
|
if (type == Attachment.Type.VIDEO | type == Attachment.Type.GIFV) {
|
2018-09-28 17:15:01 +02:00
|
|
|
mediaOverlays[i].setVisibility(View.VISIBLE);
|
2017-11-30 20:12:09 +01:00
|
|
|
} else {
|
2018-09-28 17:15:01 +02:00
|
|
|
mediaOverlays[i].setVisibility(View.GONE);
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
|
|
|
|
2018-05-10 20:13:25 +02:00
|
|
|
final int urlIndex = i;
|
2018-09-28 17:15:01 +02:00
|
|
|
mediaPreviews[i].setOnClickListener(v -> {
|
2018-08-16 15:51:23 +02:00
|
|
|
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
2018-08-15 11:20:57 +02:00
|
|
|
listener.onViewMedia(getAdapterPosition(), urlIndex, v);
|
|
|
|
}
|
|
|
|
});
|
2017-11-30 20:12:09 +01:00
|
|
|
|
2018-05-10 20:13:25 +02:00
|
|
|
if (n <= 2) {
|
2018-09-28 17:15:01 +02:00
|
|
|
mediaPreviews[0].getLayoutParams().height = getMediaPreviewHeight(context) * 2;
|
|
|
|
mediaPreviews[1].getLayoutParams().height = getMediaPreviewHeight(context) * 2;
|
2017-11-30 20:12:09 +01:00
|
|
|
} else {
|
2018-09-28 17:15:01 +02:00
|
|
|
mediaPreviews[0].getLayoutParams().height = getMediaPreviewHeight(context);
|
|
|
|
mediaPreviews[1].getLayoutParams().height = getMediaPreviewHeight(context);
|
|
|
|
mediaPreviews[2].getLayoutParams().height = getMediaPreviewHeight(context);
|
|
|
|
mediaPreviews[3].getLayoutParams().height = getMediaPreviewHeight(context);
|
2017-11-30 20:12:09 +01:00
|
|
|
}
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
2017-11-30 20:12:09 +01:00
|
|
|
|
|
|
|
String hiddenContentText;
|
2018-05-10 20:13:25 +02:00
|
|
|
if (sensitive) {
|
2017-11-30 20:12:09 +01:00
|
|
|
hiddenContentText = context.getString(R.string.status_sensitive_media_template,
|
|
|
|
context.getString(R.string.status_sensitive_media_title),
|
|
|
|
context.getString(R.string.status_sensitive_media_directions));
|
|
|
|
} else {
|
|
|
|
hiddenContentText = context.getString(R.string.status_sensitive_media_template,
|
|
|
|
context.getString(R.string.status_media_hidden_title),
|
|
|
|
context.getString(R.string.status_sensitive_media_directions));
|
|
|
|
}
|
|
|
|
|
|
|
|
sensitiveMediaWarning.setText(HtmlUtils.fromHtml(hiddenContentText));
|
|
|
|
|
|
|
|
sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE);
|
|
|
|
sensitiveMediaShow.setVisibility(showingContent ? View.VISIBLE : View.GONE);
|
2018-05-10 20:13:25 +02:00
|
|
|
sensitiveMediaShow.setOnClickListener(v -> {
|
|
|
|
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
|
|
|
listener.onContentHiddenChange(false, getAdapterPosition());
|
2017-11-30 20:12:09 +01:00
|
|
|
}
|
2018-05-10 20:13:25 +02:00
|
|
|
v.setVisibility(View.GONE);
|
|
|
|
sensitiveMediaWarning.setVisibility(View.VISIBLE);
|
2017-11-30 20:12:09 +01:00
|
|
|
});
|
2018-05-10 20:13:25 +02:00
|
|
|
sensitiveMediaWarning.setOnClickListener(v -> {
|
|
|
|
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
|
|
|
listener.onContentHiddenChange(true, getAdapterPosition());
|
2017-11-30 20:12:09 +01:00
|
|
|
}
|
2018-05-10 20:13:25 +02:00
|
|
|
v.setVisibility(View.GONE);
|
|
|
|
sensitiveMediaShow.setVisibility(View.VISIBLE);
|
2017-11-30 20:12:09 +01:00
|
|
|
});
|
|
|
|
|
2017-08-03 23:26:26 +02:00
|
|
|
|
|
|
|
// Hide any of the placeholder previews beyond the ones set.
|
|
|
|
for (int i = n; i < Status.MAX_MEDIA_ATTACHMENTS; i++) {
|
2018-09-28 17:15:01 +02:00
|
|
|
mediaPreviews[i].setVisibility(View.GONE);
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@NonNull
|
2017-11-30 20:12:09 +01:00
|
|
|
private static String getLabelTypeText(Context context, Attachment.Type type) {
|
2017-08-03 23:26:26 +02:00
|
|
|
switch (type) {
|
|
|
|
default:
|
|
|
|
case IMAGE:
|
|
|
|
return context.getString(R.string.status_media_images);
|
|
|
|
case GIFV:
|
|
|
|
case VIDEO:
|
|
|
|
return context.getString(R.string.status_media_video);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@DrawableRes
|
2017-11-30 20:12:09 +01:00
|
|
|
private static int getLabelIcon(Attachment.Type type) {
|
2017-08-03 23:26:26 +02:00
|
|
|
switch (type) {
|
|
|
|
default:
|
|
|
|
case IMAGE:
|
|
|
|
return R.drawable.ic_photo_24dp;
|
|
|
|
case GIFV:
|
|
|
|
case VIDEO:
|
|
|
|
return R.drawable.ic_videocam_24dp;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
protected void setMediaLabel(List<Attachment> attachments, boolean sensitive,
|
|
|
|
final StatusActionListener listener) {
|
2018-05-10 20:13:25 +02:00
|
|
|
if (attachments.size() == 0) {
|
2017-08-03 23:26:26 +02:00
|
|
|
mediaLabel.setVisibility(View.GONE);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
mediaLabel.setVisibility(View.VISIBLE);
|
|
|
|
|
|
|
|
// Set the label's text.
|
|
|
|
Context context = itemView.getContext();
|
2018-05-10 20:13:25 +02:00
|
|
|
String labelText = getLabelTypeText(context, attachments.get(0).getType());
|
2017-08-03 23:26:26 +02:00
|
|
|
if (sensitive) {
|
|
|
|
String sensitiveText = context.getString(R.string.status_sensitive_media_title);
|
|
|
|
labelText += String.format(" (%s)", sensitiveText);
|
|
|
|
}
|
|
|
|
mediaLabel.setText(labelText);
|
|
|
|
|
|
|
|
// Set the icon next to the label.
|
2018-05-10 20:13:25 +02:00
|
|
|
int drawableId = getLabelIcon(attachments.get(0).getType());
|
2017-08-03 23:26:26 +02:00
|
|
|
Drawable drawable = AppCompatResources.getDrawable(context, drawableId);
|
|
|
|
ThemeUtils.setDrawableTint(context, drawable, android.R.attr.textColorTertiary);
|
|
|
|
mediaLabel.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
|
|
|
|
|
2018-05-10 20:13:25 +02:00
|
|
|
mediaLabel.setOnClickListener(v -> listener.onViewMedia(getAdapterPosition(), 0, null));
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
protected void hideSensitiveMediaWarning() {
|
2017-08-03 23:26:26 +02:00
|
|
|
sensitiveMediaWarning.setVisibility(View.GONE);
|
2017-08-30 14:22:12 +02:00
|
|
|
sensitiveMediaShow.setVisibility(View.GONE);
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
protected void setupButtons(final StatusActionListener listener, final String accountId) {
|
2017-08-03 23:26:26 +02:00
|
|
|
/* Originally position was passed through to all these listeners, but it caused several
|
|
|
|
* bugs where other statuses in the list would be removed or added and cause the position
|
|
|
|
* here to become outdated. So, getting the adapter position at the time the listener is
|
|
|
|
* actually called is the appropriate solution. */
|
2018-06-24 09:53:23 +02:00
|
|
|
avatar.setOnClickListener(v -> listener.onViewAccount(accountId));
|
|
|
|
replyButton.setOnClickListener(v -> {
|
|
|
|
int position = getAdapterPosition();
|
|
|
|
if (position != RecyclerView.NO_POSITION) {
|
|
|
|
listener.onReply(position);
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
|
|
|
});
|
2019-03-04 19:24:27 +01:00
|
|
|
if (reblogButton != null) {
|
2019-02-12 19:22:37 +01:00
|
|
|
reblogButton.setEventListener(new SparkEventListener() {
|
|
|
|
@Override
|
|
|
|
public void onEvent(ImageView button, boolean buttonState) {
|
|
|
|
int position = getAdapterPosition();
|
|
|
|
if (position != RecyclerView.NO_POSITION) {
|
|
|
|
listener.onReblog(!reblogged, position);
|
|
|
|
}
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
@Override
|
|
|
|
public void onEventAnimationEnd(ImageView button, boolean buttonState) {
|
|
|
|
}
|
2017-08-03 23:26:26 +02:00
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
@Override
|
|
|
|
public void onEventAnimationStart(ImageView button, boolean buttonState) {
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2017-08-03 23:26:26 +02:00
|
|
|
favouriteButton.setEventListener(new SparkEventListener() {
|
|
|
|
@Override
|
|
|
|
public void onEvent(ImageView button, boolean buttonState) {
|
|
|
|
int position = getAdapterPosition();
|
|
|
|
if (position != RecyclerView.NO_POSITION) {
|
|
|
|
listener.onFavourite(!favourited, position);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onEventAnimationEnd(ImageView button, boolean buttonState) {
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onEventAnimationStart(ImageView button, boolean buttonState) {
|
|
|
|
}
|
|
|
|
});
|
2018-06-24 09:53:23 +02:00
|
|
|
moreButton.setOnClickListener(v -> {
|
|
|
|
int position = getAdapterPosition();
|
|
|
|
if (position != RecyclerView.NO_POSITION) {
|
|
|
|
listener.onMore(v, position);
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
/* Even though the content TextView is a child of the container, it won't respond to clicks
|
|
|
|
* if it contains URLSpans without also setting its listener. The surrounding spans will
|
|
|
|
* just eat the clicks instead of deferring to the parent listener, but WILL respond to a
|
|
|
|
* listener directly on the TextView, for whatever reason. */
|
2018-05-10 20:13:25 +02:00
|
|
|
View.OnClickListener viewThreadListener = v -> {
|
|
|
|
int position = getAdapterPosition();
|
|
|
|
if (position != RecyclerView.NO_POSITION) {
|
|
|
|
listener.onViewThread(position);
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
content.setOnClickListener(viewThreadListener);
|
2018-09-28 17:15:01 +02:00
|
|
|
itemView.setOnClickListener(viewThreadListener);
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
protected void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
|
|
|
|
boolean mediaPreviewEnabled) {
|
2019-03-25 13:44:31 +01:00
|
|
|
this.setupWithStatus(status, listener, mediaPreviewEnabled, null);
|
2019-03-16 14:38:29 +01:00
|
|
|
}
|
2019-03-25 13:44:31 +01:00
|
|
|
|
2019-03-16 14:38:29 +01:00
|
|
|
protected void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
|
|
|
|
boolean mediaPreviewEnabled, @Nullable Object payloads) {
|
|
|
|
if (payloads == null) {
|
|
|
|
setDisplayName(status.getUserFullName(), status.getAccountEmojis());
|
|
|
|
setUsername(status.getNickname());
|
|
|
|
setCreatedAt(status.getCreatedAt());
|
|
|
|
setIsReply(status.getInReplyToId() != null);
|
2019-04-09 20:13:25 +02:00
|
|
|
setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot());
|
2019-03-16 14:38:29 +01:00
|
|
|
setReblogged(status.isReblogged());
|
|
|
|
setFavourited(status.isFavourited());
|
|
|
|
List<Attachment> attachments = status.getAttachments();
|
|
|
|
boolean sensitive = status.isSensitive();
|
|
|
|
if (mediaPreviewEnabled) {
|
|
|
|
setMediaPreviews(attachments, sensitive, listener, status.isShowingContent());
|
|
|
|
|
|
|
|
if (attachments.size() == 0) {
|
|
|
|
hideSensitiveMediaWarning();
|
|
|
|
}
|
|
|
|
// Hide the unused label.
|
|
|
|
mediaLabel.setVisibility(View.GONE);
|
|
|
|
} else {
|
|
|
|
setMediaLabel(attachments, sensitive, listener);
|
|
|
|
// Hide all unused views.
|
|
|
|
mediaPreviews[0].setVisibility(View.GONE);
|
|
|
|
mediaPreviews[1].setVisibility(View.GONE);
|
|
|
|
mediaPreviews[2].setVisibility(View.GONE);
|
|
|
|
mediaPreviews[3].setVisibility(View.GONE);
|
2017-11-30 20:12:09 +01:00
|
|
|
hideSensitiveMediaWarning();
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|
|
|
|
|
2019-03-16 14:38:29 +01:00
|
|
|
setupButtons(listener, status.getSenderId());
|
|
|
|
setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility());
|
2018-12-16 10:30:40 +01:00
|
|
|
|
2019-03-16 14:38:29 +01:00
|
|
|
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), listener);
|
2017-10-19 15:25:04 +02:00
|
|
|
|
2019-03-16 14:38:29 +01:00
|
|
|
setContentDescription(status);
|
2019-04-22 10:11:00 +02:00
|
|
|
|
|
|
|
setupPoll(status.getPoll(),status.getStatusEmojis(), listener);
|
|
|
|
|
2019-03-16 14:38:29 +01:00
|
|
|
// Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0
|
|
|
|
// RecyclerView tries to set AccessibilityDelegateCompat to null
|
|
|
|
// but ViewCompat code replaces is with the default one. RecyclerView never
|
|
|
|
// fetches another one from its delegate because it checks that it's set so we remove it
|
|
|
|
// and let RecyclerView ask for a new delegate.
|
|
|
|
itemView.setAccessibilityDelegate(null);
|
2019-03-25 13:44:31 +01:00
|
|
|
} else {
|
2019-03-16 14:38:29 +01:00
|
|
|
if (payloads instanceof List)
|
2019-03-25 13:44:31 +01:00
|
|
|
for (Object item : (List) payloads) {
|
|
|
|
if (Key.KEY_CREATED.equals(item)) {
|
|
|
|
setCreatedAt(status.getCreatedAt());
|
|
|
|
}
|
2019-03-16 14:38:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2019-03-04 19:24:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void setContentDescription(@Nullable StatusViewData.Concrete status) {
|
|
|
|
if (status == null) {
|
|
|
|
itemView.setContentDescription(
|
|
|
|
itemView.getContext().getString(R.string.load_more_placeholder_text));
|
|
|
|
} else {
|
|
|
|
setDescriptionForStatus(status);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
private void setDescriptionForStatus(@NonNull StatusViewData.Concrete status) {
|
|
|
|
Context context = itemView.getContext();
|
|
|
|
|
|
|
|
String description = context.getString(R.string.description_status,
|
|
|
|
status.getUserFullName(),
|
|
|
|
getContentWarningDescription(context, status),
|
|
|
|
(!status.isSensitive() || status.isExpanded() ? status.getContent() : ""),
|
|
|
|
getCreatedAtDescription(status.getCreatedAt()),
|
|
|
|
getReblogDescription(context, status),
|
|
|
|
status.getNickname(),
|
|
|
|
status.isReblogged() ? context.getString(R.string.description_status_reblogged) : "",
|
|
|
|
status.isFavourited() ? context.getString(R.string.description_status_favourited) : "",
|
|
|
|
getMediaDescription(context, status),
|
|
|
|
getVisibilityDescription(context, status.getVisibility()),
|
|
|
|
getFavsText(context, status.getFavouritesCount()),
|
|
|
|
getReblogsText(context, status.getReblogsCount())
|
|
|
|
);
|
|
|
|
itemView.setContentDescription(description);
|
|
|
|
}
|
|
|
|
|
|
|
|
private CharSequence getReblogDescription(Context context,
|
|
|
|
@NonNull StatusViewData.Concrete status) {
|
|
|
|
CharSequence reblogDescriontion;
|
|
|
|
String rebloggedUsername = status.getRebloggedByUsername();
|
|
|
|
if (rebloggedUsername != null) {
|
|
|
|
reblogDescriontion = context
|
|
|
|
.getString(R.string.status_boosted_format, rebloggedUsername);
|
|
|
|
} else {
|
|
|
|
reblogDescriontion = "";
|
|
|
|
}
|
|
|
|
return reblogDescriontion;
|
|
|
|
}
|
|
|
|
|
|
|
|
private CharSequence getMediaDescription(Context context,
|
|
|
|
@NonNull StatusViewData.Concrete status) {
|
|
|
|
if (status.getAttachments().isEmpty()) {
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
StringBuilder mediaDescriptions = CollectionsKt.fold(
|
|
|
|
status.getAttachments(),
|
|
|
|
new StringBuilder(),
|
|
|
|
(builder, a) -> {
|
|
|
|
if (a.getDescription() == null) {
|
|
|
|
String placeholder =
|
|
|
|
context.getString(R.string.description_status_media_no_description_placeholder);
|
|
|
|
return builder.append(placeholder);
|
|
|
|
} else {
|
|
|
|
builder.append("; ");
|
|
|
|
return builder.append(a.getDescription());
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return context.getString(R.string.description_status_media, mediaDescriptions);
|
|
|
|
}
|
|
|
|
|
|
|
|
private CharSequence getContentWarningDescription(Context context,
|
|
|
|
@NonNull StatusViewData.Concrete status) {
|
|
|
|
if (!TextUtils.isEmpty(status.getSpoilerText())) {
|
|
|
|
return context.getString(R.string.description_status_cw, status.getSpoilerText());
|
|
|
|
} else {
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private CharSequence getVisibilityDescription(Context context, Status.Visibility visibility) {
|
|
|
|
int resource;
|
|
|
|
switch (visibility) {
|
|
|
|
case PUBLIC:
|
|
|
|
resource = R.string.description_visiblity_public;
|
|
|
|
break;
|
|
|
|
case UNLISTED:
|
|
|
|
resource = R.string.description_visiblity_unlisted;
|
|
|
|
break;
|
|
|
|
case PRIVATE:
|
|
|
|
resource = R.string.description_visiblity_private;
|
|
|
|
break;
|
|
|
|
case DIRECT:
|
|
|
|
resource = R.string.description_visiblity_direct;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
return context.getString(resource);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected CharSequence getFavsText(Context context, int count) {
|
|
|
|
if (count > 0) {
|
|
|
|
String countString = numberFormat.format(count);
|
|
|
|
return HtmlUtils.fromHtml(context.getResources().getQuantityString(R.plurals.favs, count, countString));
|
|
|
|
} else {
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected CharSequence getReblogsText(Context context, int count) {
|
|
|
|
if (count > 0) {
|
|
|
|
String countString = numberFormat.format(count);
|
|
|
|
return HtmlUtils.fromHtml(context.getResources().getQuantityString(R.plurals.reblogs, count, countString));
|
|
|
|
} else {
|
|
|
|
return "";
|
|
|
|
}
|
Add support for collapsible statuses when they exceed 500 characters (#825)
* Update Gradle plugin to work with Android Studio 3.3 Canary
Android Studio 3.1.4 Stable doesn't render layout previews in this project
for whatever reason. Switching to the latest 3.3 Canary release fixes the
issue without affecting Gradle scripts but requires the new Android Gradle
plugin to match the new Android Studio release.
This commit will be reverted once development on the feature is done.
* Update gradle build script to allow installing debug builds alongside store version
This will allow developers, testers, etc to work on Tusky will not having to worry
about overwriting, uninstalling, fiddling with a preinstalled application which would
mean having to login again every time the development cycle starts/finishes and
manually reinstalling the app.
* Add UI changes to support collapsing statuses
The button uses subtle styling to not be distracting like the CW button on the timeline
The button is toggleable, full width to match the status textbox hitbox width and also
is shorter to not be too intrusive between the status text and images, or the post below
* Update status data model to store whether the message has been collapsed
* Update status action listener to notify of collapsed state changing
Provide stubs in all implementing classes and mark as TODO the stubs that
require a proper implementation for the feature to work.
* Add implementation code to handle status collapse/expand in timeline
Code has not been added elsewhere to simplify testing.
Once the code will be considered stable it will be also included in other
status action listener implementers.
* Add preferences so that users can toggle the collapsing of long posts
This is currently limited to a simple toggle, it would be nice to implement
a more advanced UI to offer the user more control over the feature.
* Update Gradle plugin to work with latest Android Studio 3.3 Canary 8
Just like the other commit, this will be reverted once the feature is working.
I simply don't want to deal with what changes in my installation of Android
Studio 3.1.4 Stable which breaks the layout preview rendering.
* Update data models and utils for statuses to better handle collapsing
I forgot that data isn't available from the API and can't really be built
from scratch using existing data due to preferences.
A new, extra boolean should fix the issue.
* Fix search breaking due to newly introduced variables in utils classes
* Fix timeline breaking due to newly introduced variables in utils classes
* Fix item status text for collapsed toggle being shown in the wrong state
* Update timeline fragment to refresh the list when collapsed settings change
* Add support for status content collapse in timeline viewholder
* Fix view holder truncating posts using temporary debug settings at 50 chars
* Add toggle support to notification layout as well
* Add support for collapsed statuses to search results
* Add support for expandable content to notifications too
* Update codebase with some suggested changes by @charlang
* Update more code with more suggestions and move null-safety into view data
* Update even more code with even more suggested code changes
* Revert a0a41ca and 0ee004d (Android Studio 3.1 to Android Studio 3.3 updates)
* Add an input filter utility class to reuse code for trimming statuses
* Update UI of statuses to show a taller collapsible button
* Update notification fragment logging to simplify null checks
* Add smartness to SmartLengthInputFilter such as word trimming and runway
* Fix posts with show more button even if bad ratio didn't collapse
* Fix thread view showing button but not collapsing by implementing the feature
* Fix spannable losing spans when collapsed and restore length to 500 characters
* Remove debug build suffix as per request
* Fix all the merging happened in f66d689, 623cad2 and 7056ba5
* Fix notification button spanning full width rather than content width
* Add a way to access a singleton to smart filter and use clearer code
* Update view holders using smart input filters to use more singletons
* Fix code style lacking spaces before boolean checks in ifs and others
* Remove all code related to collapsibility preferences, strings included
* Update style to match content warning toggle button
* Update strings to give cleaner differentiation between CW and collapse
* Update smart filter code to use fully qualified names to avoid confusion
2018-09-19 19:51:20 +02:00
|
|
|
}
|
2019-04-22 10:11:00 +02:00
|
|
|
|
|
|
|
protected void setupPoll(Poll poll, List<Emoji> emojis, StatusActionListener listener) {
|
|
|
|
if(poll == null) {
|
|
|
|
for(TextView pollResult: pollResults) {
|
|
|
|
pollResult.setVisibility(View.GONE);
|
|
|
|
}
|
|
|
|
pollDescription.setVisibility(View.GONE);
|
|
|
|
pollRadioGroup.setVisibility(View.GONE);
|
|
|
|
|
2019-04-28 09:58:17 +02:00
|
|
|
for(CheckBox checkBox: pollCheckboxOptions) {
|
|
|
|
checkBox.setVisibility(View.GONE);
|
2019-04-22 10:11:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
pollButton.setVisibility(View.GONE);
|
|
|
|
} else {
|
|
|
|
Context context = pollDescription.getContext();
|
|
|
|
|
|
|
|
if(poll.getExpired() || poll.getVoted()) {
|
|
|
|
// no voting possible
|
2019-04-28 09:58:17 +02:00
|
|
|
setupPollResult(poll, emojis);
|
2019-04-22 10:11:00 +02:00
|
|
|
} else {
|
|
|
|
// voting possible
|
2019-04-28 09:58:17 +02:00
|
|
|
setupPollVoting(poll, emojis, listener);
|
2019-04-22 10:11:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
pollDescription.setVisibility(View.VISIBLE);
|
|
|
|
|
|
|
|
String votes = numberFormat.format(poll.getVotesCount());
|
|
|
|
String votesText = context.getResources().getQuantityString(R.plurals.poll_info_votes, poll.getVotesCount(), votes);
|
|
|
|
|
|
|
|
CharSequence pollDurationInfo;
|
|
|
|
if(poll.getExpired()) {
|
|
|
|
pollDurationInfo = context.getString(R.string.poll_info_closed);
|
|
|
|
} else {
|
|
|
|
if(useAbsoluteTime) {
|
|
|
|
pollDurationInfo = context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.getExpiresAt()));
|
|
|
|
} else {
|
|
|
|
String pollDuration = DateUtils.formatDuration(pollDescription.getContext(), poll.getExpiresAt().getTime(), System.currentTimeMillis());
|
|
|
|
pollDurationInfo = context.getString(R.string.poll_info_time_relative, pollDuration);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
String pollInfo = pollDescription.getContext().getString(R.string.poll_info_format, votesText, pollDurationInfo);
|
|
|
|
|
|
|
|
pollDescription.setText(pollInfo);
|
|
|
|
|
2019-04-28 09:58:17 +02:00
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void setupPollResult(Poll poll, List<Emoji> emojis) {
|
|
|
|
List<PollOption> options = poll.getOptions();
|
|
|
|
|
|
|
|
for(int i = 0; i < Status.MAX_POLL_OPTIONS; i++) {
|
|
|
|
if(i < options.size()) {
|
|
|
|
long percent = calculatePollPercent(options.get(i).getVotesCount(), poll.getVotesCount());
|
|
|
|
|
|
|
|
String pollOptionText = pollResults[i].getContext().getString(R.string.poll_option_format, percent, options.get(i).getTitle());
|
|
|
|
pollResults[i].setText(CustomEmojiHelper.emojifyText(HtmlUtils.fromHtml(pollOptionText), emojis, pollResults[i]));
|
|
|
|
pollResults[i].setVisibility(View.VISIBLE);
|
|
|
|
|
|
|
|
int level = (int) percent * 100;
|
|
|
|
|
|
|
|
pollResults[i].getBackground().setLevel(level);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
pollResults[i].setVisibility(View.GONE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pollRadioGroup.setVisibility(View.GONE);
|
|
|
|
|
|
|
|
for(CheckBox checkBox: pollCheckboxOptions) {
|
|
|
|
checkBox.setVisibility(View.GONE);
|
|
|
|
}
|
|
|
|
|
|
|
|
pollButton.setVisibility(View.GONE);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void setupPollVoting(Poll poll, List<Emoji> emojis, StatusActionListener listener) {
|
|
|
|
List<PollOption> options = poll.getOptions();
|
|
|
|
|
|
|
|
pollButton.setVisibility(View.VISIBLE);
|
|
|
|
|
|
|
|
for(TextView pollResult: pollResults) {
|
|
|
|
pollResult.setVisibility(View.GONE);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(poll.getMultiple()) {
|
|
|
|
|
|
|
|
pollRadioGroup.setVisibility(View.GONE);
|
|
|
|
|
|
|
|
for(int i = 0; i < Status.MAX_POLL_OPTIONS; i++) {
|
|
|
|
if(i < options.size()) {
|
|
|
|
pollCheckboxOptions[i].setText(CustomEmojiHelper.emojifyString(options.get(i).getTitle(), emojis, pollCheckboxOptions[i]));
|
|
|
|
pollCheckboxOptions[i].setVisibility(View.VISIBLE);
|
|
|
|
pollCheckboxOptions[i].setChecked(false);
|
|
|
|
} else {
|
|
|
|
pollCheckboxOptions[i].setVisibility(View.GONE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pollButton.setOnClickListener(v -> {
|
|
|
|
|
|
|
|
List<Integer> pollResult = new ArrayList<>(options.size());
|
|
|
|
for(int i = 0; i < options.size(); i++) {
|
|
|
|
if(pollCheckboxOptions[i].isChecked()) {
|
|
|
|
pollResult.add(i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(pollResult.size() == 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
listener.onVoteInPoll(getAdapterPosition(), pollResult);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
|
|
|
|
for(CheckBox pollCheckbox: pollCheckboxOptions) {
|
|
|
|
pollCheckbox.setVisibility(View.GONE);
|
|
|
|
}
|
|
|
|
|
|
|
|
pollRadioGroup.setVisibility(View.VISIBLE);
|
|
|
|
pollRadioGroup.clearCheck();
|
|
|
|
|
|
|
|
for(int i = 0; i < Status.MAX_POLL_OPTIONS; i++) {
|
|
|
|
if(i < options.size()) {
|
|
|
|
pollRadioOptions[i].setText(CustomEmojiHelper.emojifyString(options.get(i).getTitle(), emojis, pollRadioOptions[i]));
|
|
|
|
pollRadioOptions[i].setVisibility(View.VISIBLE);
|
|
|
|
} else {
|
|
|
|
pollRadioOptions[i].setVisibility(View.GONE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-22 10:11:00 +02:00
|
|
|
pollButton.setOnClickListener(v -> {
|
|
|
|
|
|
|
|
int selectedRadioButtonIndex;
|
|
|
|
switch (pollRadioGroup.getCheckedRadioButtonId()) {
|
|
|
|
case R.id.status_poll_radio_button_0:
|
|
|
|
selectedRadioButtonIndex = 0;
|
|
|
|
break;
|
|
|
|
case R.id.status_poll_radio_button_1:
|
|
|
|
selectedRadioButtonIndex = 1;
|
|
|
|
break;
|
|
|
|
case R.id.status_poll_radio_button_2:
|
|
|
|
selectedRadioButtonIndex = 2;
|
|
|
|
break;
|
|
|
|
case R.id.status_poll_radio_button_3:
|
|
|
|
selectedRadioButtonIndex = 3;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
listener.onVoteInPoll(getAdapterPosition(), Collections.singletonList(selectedRadioButtonIndex));
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static long calculatePollPercent(int votes, int totalVotes) {
|
|
|
|
if(votes == 0) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
return Math.round(votes / (double) totalVotes * 100);
|
|
|
|
}
|
2017-08-03 23:26:26 +02:00
|
|
|
}
|