Animate gif avatars (#1279)

* animate gif avatars

* add setting to enable avatar animation

* cleanup code
This commit is contained in:
Konrad Pozniak 2019-05-26 08:46:08 +02:00 committed by GitHub
parent da1089184c
commit 83696b5c7f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 381 additions and 547 deletions

View file

@ -78,6 +78,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
private var showingReblogs: Boolean = false
private var loadedAccount: Account? = null
private var animateAvatar: Boolean = false
// fields for scroll animation
private var hideFab: Boolean = false
private var oldOffset: Int = 0
@ -120,7 +122,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
updateButtons()
}
hideFab = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("fabHide", false)
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false)
hideFab = sharedPrefs.getBoolean("fabHide", false)
loadResources()
setupToolbar()
@ -379,11 +383,16 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
*/
private fun updateAccountAvatar() {
loadedAccount?.let { account ->
loadAvatar(
account.avatar,
accountAvatarImageView,
resources.getDimensionPixelSize(R.dimen.avatar_radius_94dp),
animateAvatar
)
Glide.with(this)
.load(account.avatar)
.placeholder(R.drawable.avatar_default)
.into(accountAvatarImageView)
Glide.with(this)
.asBitmap()
.load(account.header)
.centerCrop()
.into(accountHeaderImageView)
@ -430,10 +439,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
accountMovedDisplayName.text = movedAccount.name
accountMovedUsername.text = getString(R.string.status_username_format, movedAccount.username)
Glide.with(this)
.load(movedAccount.avatar)
.placeholder(R.drawable.avatar_default)
.into(accountMovedAvatar)
val avatarRadius = resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
loadAvatar(movedAccount.avatar, accountMovedAvatar, avatarRadius, animateAvatar)
accountMovedText.text = getString(R.string.account_moved_description, movedAccount.displayName)

View file

@ -26,6 +26,7 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
@ -60,25 +61,6 @@ import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.core.view.inputmethod.InputConnectionCompat;
import androidx.core.view.inputmethod.InputContentInfoCompat;
import androidx.lifecycle.Lifecycle;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.transition.TransitionManager;
import com.bumptech.glide.Glide;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.snackbar.Snackbar;
@ -103,6 +85,7 @@ import com.keylesspalace.tusky.service.SendTootService;
import com.keylesspalace.tusky.util.ComposeTokenizer;
import com.keylesspalace.tusky.util.CountUpDownLatch;
import com.keylesspalace.tusky.util.DownsizeImageTask;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.ListUtils;
import com.keylesspalace.tusky.util.SaveTootHelper;
import com.keylesspalace.tusky.util.SpanUtilsKt;
@ -303,14 +286,20 @@ public final class ComposeActivity
if (activeAccount != null) {
ImageView composeAvatar = findViewById(R.id.composeAvatar);
if (TextUtils.isEmpty(activeAccount.getProfilePictureUrl())) {
composeAvatar.setImageResource(R.drawable.avatar_default);
} else {
Glide.with(this).load(activeAccount.getProfilePictureUrl())
.error(R.drawable.avatar_default)
.placeholder(R.drawable.avatar_default)
.into(composeAvatar);
}
int[] actionBarSizeAttr = new int[] { R.attr.actionBarSize };
TypedArray a = obtainStyledAttributes(null, actionBarSizeAttr);
int avatarSize = a.getDimensionPixelSize(0, 1);
a.recycle();
boolean animateAvatars = preferences.getBoolean("animateGifAvatars", false);
ImageLoadingHelper.loadAvatar(
activeAccount.getProfilePictureUrl(),
composeAvatar,
avatarSize / 8,
animateAvatars
);
composeAvatar.setContentDescription(
getString(R.string.compose_active_account_description,

View file

@ -35,6 +35,8 @@ import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.FitCenter
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
@ -136,6 +138,10 @@ class EditProfileActivity : BaseActivity(), Injectable {
Glide.with(this)
.load(me.avatar)
.placeholder(R.drawable.avatar_default)
.transform(
FitCenter(),
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
)
.into(avatarPreview)
}
@ -158,8 +164,8 @@ class EditProfileActivity : BaseActivity(), Injectable {
}
})
observeImage(viewModel.avatarData, avatarPreview, avatarProgressBar)
observeImage(viewModel.headerData, headerPreview, headerProgressBar)
observeImage(viewModel.avatarData, avatarPreview, avatarProgressBar, true)
observeImage(viewModel.headerData, headerPreview, headerProgressBar, false)
viewModel.saveData.observe(this, Observer<Resource<Nothing>> {
when(it) {
@ -192,12 +198,26 @@ class EditProfileActivity : BaseActivity(), Injectable {
}
}
private fun observeImage(liveData: LiveData<Resource<Bitmap>>, imageView: ImageView, progressBar: View) {
private fun observeImage(liveData: LiveData<Resource<Bitmap>>,
imageView: ImageView,
progressBar: View,
roundedCorners: Boolean) {
liveData.observe(this, Observer<Resource<Bitmap>> {
when (it) {
is Success -> {
imageView.setImageBitmap(it.data)
val glide = Glide.with(imageView)
.load(it.data)
if (roundedCorners) {
glide.transform(
FitCenter(),
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
)
}
glide.into(imageView)
imageView.show()
progressBar.hide()
}

View file

@ -36,6 +36,7 @@ import androidx.viewpager.widget.ViewPager;
import androidx.appcompat.app.AlertDialog;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.KeyEvent;
import android.widget.ImageButton;
@ -78,9 +79,6 @@ import dagger.android.AndroidInjector;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.support.HasSupportFragmentInjector;
import io.reactivex.android.schedulers.AndroidSchedulers;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static com.keylesspalace.tusky.util.MediaUtilsKt.deleteStaleCachedMedia;
import static com.uber.autodispose.AutoDispose.autoDisposable;
@ -330,9 +328,18 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
background.setColorFilter(ContextCompat.getColor(this, R.color.header_background_filter));
background.setBackgroundColor(ContextCompat.getColor(this, R.color.window_background_dark));
final boolean animateAvatars = PreferenceManager.getDefaultSharedPreferences(this)
.getBoolean("animateGifAvatars", false);
DrawerImageLoader.init(new AbstractDrawerImageLoader() {
@Override
public void set(ImageView imageView, Uri uri, Drawable placeholder, String tag) {
if(animateAvatars) {
Glide.with(MainActivity.this)
.load(uri)
.placeholder(placeholder)
.into(imageView);
} else {
Glide.with(MainActivity.this)
.asBitmap()
.load(uri)
@ -340,6 +347,8 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
.into(imageView);
}
}
@Override
public void cancel(ImageView imageView) {
Glide.with(MainActivity.this).clear(imageView);

View file

@ -137,13 +137,7 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
}
//workaround end
}
"statusTextSize" -> {
restartActivitiesOnExit = true
}
"absoluteTimeView" -> {
restartActivitiesOnExit = true
}
"showBotOverlay" -> {
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars" -> {
restartActivitiesOnExit = true
}
"language" -> {

View file

@ -16,15 +16,15 @@
package com.keylesspalace.tusky.adapter
import android.content.Context
import android.text.TextUtils
import android.preference.PreferenceManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import com.bumptech.glide.Glide
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.util.CustomEmojiHelper
import com.keylesspalace.tusky.util.loadAvatar
import kotlinx.android.synthetic.main.item_autocomplete_account.view.*
@ -45,13 +45,13 @@ class AccountSelectionAdapter(context: Context): ArrayAdapter<AccountEntity>(con
val avatar = view.avatar
username.text = account.fullName
displayName.text = CustomEmojiHelper.emojifyString(account.displayName, account.emojis, displayName)
if (!TextUtils.isEmpty(account.profilePictureUrl)) {
Glide.with(avatar)
.asBitmap()
.load(account.profilePictureUrl)
.placeholder(R.drawable.avatar_default)
.into(avatar)
}
val avatarRadius = avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp)
val animateAvatar = PreferenceManager.getDefaultSharedPreferences(avatar.context)
.getBoolean("animateGifAvatars", false)
loadAvatar(account.profilePictureUrl, avatar, avatarRadius, animateAvatar)
}
return view

View file

@ -2,17 +2,18 @@ package com.keylesspalace.tusky.adapter;
import androidx.recyclerview.widget.RecyclerView;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.interfaces.LinkListener;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
class AccountViewHolder extends RecyclerView.ViewHolder {
private TextView username;
@ -21,6 +22,7 @@ class AccountViewHolder extends RecyclerView.ViewHolder {
private ImageView avatarInset;
private String accountId;
private boolean showBotOverlay;
private boolean animateAvatar;
AccountViewHolder(View itemView) {
super(itemView);
@ -28,7 +30,9 @@ class AccountViewHolder extends RecyclerView.ViewHolder {
displayName = itemView.findViewById(R.id.account_display_name);
avatar = itemView.findViewById(R.id.account_avatar);
avatarInset = itemView.findViewById(R.id.account_avatar_inset);
showBotOverlay = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()).getBoolean("showBotOverlay", true);
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(itemView.getContext());
showBotOverlay = sharedPrefs.getBoolean("showBotOverlay", true);
animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false);
}
void setupWithAccount(Account account) {
@ -38,11 +42,9 @@ class AccountViewHolder extends RecyclerView.ViewHolder {
username.setText(formattedUsername);
CharSequence emojifiedName = CustomEmojiHelper.emojifyString(account.getName(), account.getEmojis(), displayName);
displayName.setText(emojifiedName);
Glide.with(avatar)
.asBitmap()
.load(account.getAvatar())
.placeholder(R.drawable.avatar_default)
.into(avatar);
int avatarRadius = avatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_48dp);
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar);
if (showBotOverlay && account.getBot()) {
avatarInset.setVisibility(View.VISIBLE);
avatarInset.setImageResource(R.drawable.ic_bot_24dp);

View file

@ -17,6 +17,8 @@ package com.keylesspalace.tusky.adapter;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -24,11 +26,11 @@ import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
public class BlocksAdapter extends AccountAdapter {
@ -69,6 +71,7 @@ public class BlocksAdapter extends AccountAdapter {
private TextView displayName;
private ImageButton unblock;
private String id;
private boolean animateAvatar;
BlockedUserViewHolder(View itemView) {
super(itemView);
@ -76,6 +79,9 @@ public class BlocksAdapter extends AccountAdapter {
username = itemView.findViewById(R.id.blocked_user_username);
displayName = itemView.findViewById(R.id.blocked_user_display_name);
unblock = itemView.findViewById(R.id.blocked_user_unblock);
animateAvatar = PreferenceManager.getDefaultSharedPreferences(itemView.getContext())
.getBoolean("animateGifAvatars", false);
}
void setupWithAccount(Account account) {
@ -85,11 +91,9 @@ public class BlocksAdapter extends AccountAdapter {
String format = username.getContext().getString(R.string.status_username_format);
String formattedUsername = String.format(format, account.getUsername());
username.setText(formattedUsername);
Glide.with(avatar)
.asBitmap()
.load(account.getAvatar())
.placeholder(R.drawable.avatar_default)
.into(avatar);
int avatarRadius = avatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_48dp);
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar);
}
void setupActionListener(final AccountActionListener listener) {

View file

@ -16,6 +16,7 @@
package com.keylesspalace.tusky.adapter;
import android.content.Context;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -30,6 +31,7 @@ import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
import java.util.ArrayList;
import java.util.List;
@ -146,13 +148,19 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter
CharSequence emojifiedName = CustomEmojiHelper.emojifyString(account.getName(),
account.getEmojis(), accountViewHolder.displayName);
accountViewHolder.displayName.setText(emojifiedName);
if (!account.getAvatar().isEmpty()) {
Glide.with(accountViewHolder.avatar)
.asBitmap()
.load(account.getAvatar())
.placeholder(R.drawable.avatar_default)
.into(accountViewHolder.avatar);
}
int avatarRadius = accountViewHolder.avatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_42dp);
boolean animateAvatar = PreferenceManager.getDefaultSharedPreferences(accountViewHolder.avatar.getContext())
.getBoolean("animateGifAvatars", false);
ImageLoadingHelper.loadAvatar(
account.getAvatar(),
accountViewHolder.avatar,
avatarRadius,
animateAvatar
);
}
break;

View file

@ -17,6 +17,8 @@ package com.keylesspalace.tusky.adapter;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -24,11 +26,11 @@ import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
public class FollowRequestsAdapter extends AccountAdapter {
@ -70,6 +72,7 @@ public class FollowRequestsAdapter extends AccountAdapter {
private ImageButton accept;
private ImageButton reject;
private String id;
private boolean animateAvatar;
FollowRequestViewHolder(View itemView) {
super(itemView);
@ -78,6 +81,8 @@ public class FollowRequestsAdapter extends AccountAdapter {
displayName = itemView.findViewById(R.id.displayNameTextView);
accept = itemView.findViewById(R.id.acceptButton);
reject = itemView.findViewById(R.id.rejectButton);
animateAvatar = PreferenceManager.getDefaultSharedPreferences(itemView.getContext())
.getBoolean("animateGifAvatars", false);
}
void setupWithAccount(Account account) {
@ -87,11 +92,9 @@ public class FollowRequestsAdapter extends AccountAdapter {
String format = username.getContext().getString(R.string.status_username_format);
String formattedUsername = String.format(format, account.getUsername());
username.setText(formattedUsername);
Glide.with(avatar)
.asBitmap()
.load(account.getAvatar())
.placeholder(R.drawable.avatar_default)
.into(avatar);
int avatarRadius = avatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_48dp);
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar);
}
void setupActionListener(final AccountActionListener listener) {

View file

@ -2,6 +2,8 @@ package com.keylesspalace.tusky.adapter;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -9,11 +11,11 @@ import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
public class MutesAdapter extends AccountAdapter {
@ -55,6 +57,7 @@ public class MutesAdapter extends AccountAdapter {
private TextView displayName;
private ImageButton unmute;
private String id;
private boolean animateAvatar;
MutedUserViewHolder(View itemView) {
super(itemView);
@ -62,6 +65,8 @@ public class MutesAdapter extends AccountAdapter {
username = itemView.findViewById(R.id.muted_user_username);
displayName = itemView.findViewById(R.id.muted_user_display_name);
unmute = itemView.findViewById(R.id.muted_user_unmute);
animateAvatar = PreferenceManager.getDefaultSharedPreferences(itemView.getContext())
.getBoolean("animateGifAvatars", false);
}
void setupWithAccount(Account account) {
@ -71,11 +76,9 @@ public class MutesAdapter extends AccountAdapter {
String format = username.getContext().getString(R.string.status_username_format);
String formattedUsername = String.format(format, account.getUsername());
username.setText(formattedUsername);
Glide.with(avatar)
.asBitmap()
.load(account.getAvatar())
.placeholder(R.drawable.avatar_default)
.into(avatar);
int avatarRadius = avatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_48dp);
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar);
}
void setupActionListener(final AccountActionListener listener) {

View file

@ -33,7 +33,6 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.ToggleButton;
import com.bumptech.glide.Glide;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Emoji;
@ -42,6 +41,7 @@ import com.keylesspalace.tusky.interfaces.LinkListener;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.DateUtils;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
import com.keylesspalace.tusky.viewdata.NotificationViewData;
@ -82,6 +82,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private NotificationActionListener notificationActionListener;
private boolean mediaPreviewEnabled;
private boolean useAbsoluteTime;
private boolean showBotOverlay;
private boolean animateAvatar;
private BidiFormatter bidiFormatter;
private AdapterDataSource<NotificationViewData> dataSource;
@ -96,6 +98,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
this.notificationActionListener = notificationActionListener;
mediaPreviewEnabled = true;
useAbsoluteTime = false;
showBotOverlay = true;
animateAvatar = false;
bidiFormatter = BidiFormatter.getInstance();
}
@ -112,12 +116,12 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case VIEW_TYPE_STATUS_NOTIFICATION: {
View view = inflater
.inflate(R.layout.item_status_notification, parent, false);
return new StatusNotificationViewHolder(view, useAbsoluteTime);
return new StatusNotificationViewHolder(view, useAbsoluteTime, animateAvatar);
}
case VIEW_TYPE_FOLLOW: {
View view = inflater
.inflate(R.layout.item_follow, parent, false);
return new FollowViewHolder(view);
return new FollowViewHolder(view, animateAvatar);
}
case VIEW_TYPE_PLACEHOLDER: {
View view = inflater
@ -167,7 +171,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
StatusViewHolder holder = (StatusViewHolder) viewHolder;
StatusViewData.Concrete status = concreteNotificaton.getStatusViewData();
holder.setupWithStatus(status,
statusListener, mediaPreviewEnabled, payloadForHolder);
statusListener, mediaPreviewEnabled, showBotOverlay, animateAvatar, payloadForHolder);
if(concreteNotificaton.getType() == Notification.Type.POLL) {
holder.setPollInfo(accountId.equals(concreteNotificaton.getAccount().getId()));
} else {
@ -266,6 +270,14 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
this.useAbsoluteTime = useAbsoluteTime;
}
public void setShowBotOverlay(boolean showBotOverlay) {
this.showBotOverlay = showBotOverlay;
}
public void setAnimateAvatar(boolean animateAvatar) {
this.animateAvatar = animateAvatar;
}
public interface NotificationActionListener {
void onViewAccount(String id);
@ -288,13 +300,15 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private TextView usernameView;
private TextView displayNameView;
private ImageView avatar;
private boolean animateAvatar;
FollowViewHolder(View itemView) {
FollowViewHolder(View itemView, boolean animateAvatar) {
super(itemView);
message = itemView.findViewById(R.id.notification_text);
usernameView = itemView.findViewById(R.id.notification_username);
displayNameView = itemView.findViewById(R.id.notification_display_name);
avatar = itemView.findViewById(R.id.notification_avatar);
this.animateAvatar = animateAvatar;
}
void setMessage(Account account, BidiFormatter bidiFormatter) {
@ -313,15 +327,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
displayNameView.setText(emojifiedDisplayName);
if (TextUtils.isEmpty(account.getAvatar())) {
avatar.setImageResource(R.drawable.avatar_default);
} else {
Glide.with(avatar)
.asBitmap()
.load(account.getAvatar())
.placeholder(R.drawable.avatar_default)
.into(avatar);
}
int avatarRadius = avatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_24dp);
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar);
}
void setupButtons(final NotificationActionListener listener, final String accountId) {
@ -349,10 +359,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private StatusViewData.Concrete statusViewData;
private boolean useAbsoluteTime;
private boolean animateAvatar;
private SimpleDateFormat shortSdf;
private SimpleDateFormat longSdf;
StatusNotificationViewHolder(View itemView, boolean useAbsoluteTime) {
StatusNotificationViewHolder(View itemView, boolean useAbsoluteTime, boolean animateAvatar) {
super(itemView);
message = itemView.findViewById(R.id.notification_top_text);
statusNameBar = itemView.findViewById(R.id.status_name_bar);
@ -376,6 +387,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
contentWarningButton.setOnCheckedChangeListener(this);
this.useAbsoluteTime = useAbsoluteTime;
this.animateAvatar = animateAvatar;
shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
}
@ -495,23 +507,17 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
void setAvatars(@Nullable String statusAvatarUrl, @Nullable String notificationAvatarUrl) {
if (TextUtils.isEmpty(statusAvatarUrl)) {
statusAvatar.setImageResource(R.drawable.avatar_default);
} else {
Glide.with(statusAvatar)
.load(statusAvatarUrl)
.placeholder(R.drawable.avatar_default)
.into(statusAvatar);
}
int statusAvatarRadius = statusAvatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_48dp);
if (TextUtils.isEmpty(notificationAvatarUrl)) {
notificationAvatar.setImageResource(R.drawable.avatar_default);
} else {
Glide.with(notificationAvatar)
.load(notificationAvatarUrl)
.placeholder(R.drawable.avatar_default)
.into(notificationAvatar);
}
ImageLoadingHelper.loadAvatar(statusAvatarUrl,
statusAvatar, statusAvatarRadius, animateAvatar);
int notificationAvatarRadius = statusAvatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_24dp);
ImageLoadingHelper.loadAvatar(notificationAvatarUrl,
notificationAvatar, notificationAvatarRadius, animateAvatar);
}
@Override

View file

@ -49,15 +49,19 @@ public class SearchResultsAdapter extends RecyclerView.Adapter {
private boolean mediaPreviewsEnabled;
private boolean alwaysShowSensitiveMedia;
private boolean useAbsoluteTime;
private boolean showBotOverlay;
private boolean animateAvatar;
private LinkListener linkListener;
private StatusActionListener statusListener;
public SearchResultsAdapter(boolean mediaPreviewsEnabled,
boolean alwaysShowSensitiveMedia,
LinkListener linkListener,
public SearchResultsAdapter(LinkListener linkListener,
StatusActionListener statusListener,
boolean useAbsoluteTime) {
boolean mediaPreviewsEnabled,
boolean alwaysShowSensitiveMedia,
boolean useAbsoluteTime,
boolean showBotOverlay,
boolean animateAvatar) {
this.accountList = Collections.emptyList();
this.statusList = Collections.emptyList();
@ -67,6 +71,8 @@ public class SearchResultsAdapter extends RecyclerView.Adapter {
this.mediaPreviewsEnabled = mediaPreviewsEnabled;
this.alwaysShowSensitiveMedia = alwaysShowSensitiveMedia;
this.useAbsoluteTime = useAbsoluteTime;
this.showBotOverlay = showBotOverlay;
this.animateAvatar = animateAvatar;
this.linkListener = linkListener;
this.statusListener = statusListener;
@ -106,7 +112,8 @@ public class SearchResultsAdapter extends RecyclerView.Adapter {
} else {
StatusViewHolder holder = (StatusViewHolder) viewHolder;
int index = position - accountList.size();
holder.setupWithStatus(concreteStatusList.get(index), statusListener, mediaPreviewsEnabled);
holder.setupWithStatus(concreteStatusList.get(index), statusListener,
mediaPreviewsEnabled, showBotOverlay, animateAvatar);
}
} else {
AccountViewHolder holder = (AccountViewHolder) viewHolder;
@ -133,11 +140,11 @@ public class SearchResultsAdapter extends RecyclerView.Adapter {
}
}
public @Nullable Status getStatusAtPosition(int position) {
@Nullable public Status getStatusAtPosition(int position) {
return statusList.get(position - accountList.size());
}
public @Nullable StatusViewData.Concrete getConcreteStatusAtPosition(int position) {
@Nullable public StatusViewData.Concrete getConcreteStatusAtPosition(int position) {
return concreteStatusList.get(position - accountList.size());
}

View file

@ -2,7 +2,6 @@ package com.keylesspalace.tusky.adapter;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.preference.PreferenceManager;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.View;
@ -16,6 +15,13 @@ import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.ToggleButton;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.emoji.text.EmojiCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Attachment;
@ -29,6 +35,7 @@ import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.DateUtils;
import com.keylesspalace.tusky.util.HtmlUtils;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.view.MediaPreviewImageView;
@ -43,12 +50,6 @@ import java.util.Date;
import java.util.List;
import java.util.Locale;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.emoji.text.EmojiCompat;
import androidx.recyclerview.widget.RecyclerView;
import at.connyduck.sparkbutton.SparkButton;
import at.connyduck.sparkbutton.SparkEventListener;
import kotlin.collections.CollectionsKt;
@ -89,11 +90,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private boolean useAbsoluteTime;
private SimpleDateFormat shortSdf;
private SimpleDateFormat longSdf;
private boolean showBotOverlay;
private final NumberFormat numberFormat = NumberFormat.getNumberInstance();
protected StatusBaseViewHolder(View itemView, boolean useAbsoluteTime) {
private int avatarRadius48dp;
private int avatarRadius36dp;
private int avatarRadius24dp;
protected StatusBaseViewHolder(View itemView,
boolean useAbsoluteTime) {
super(itemView);
displayName = itemView.findViewById(R.id.status_display_name);
username = itemView.findViewById(R.id.status_username);
@ -104,8 +109,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
reblogButton = itemView.findViewById(R.id.status_inset);
favouriteButton = itemView.findViewById(R.id.status_favourite);
moreButton = itemView.findViewById(R.id.status_more);
reblogged = false;
favourited = false;
mediaPreviews = new MediaPreviewImageView[]{
itemView.findViewById(R.id.status_media_preview_0),
itemView.findViewById(R.id.status_media_preview_1),
@ -153,7 +157,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
this.useAbsoluteTime = useAbsoluteTime;
shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
showBotOverlay = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()).getBoolean("showBotOverlay", true);
this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp);
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp);
}
protected abstract int getMediaPreviewHeight(Context context);
@ -219,8 +226,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
}
private void setAvatar(String url, @Nullable String rebloggedUrl, boolean isBot) {
private void setAvatar(String url,
@Nullable String rebloggedUrl,
boolean isBot,
boolean showBotOverlay,
boolean animateAvatar) {
int avatarRadius;
if(TextUtils.isEmpty(rebloggedUrl)) {
avatar.setPaddingRelative(0, 0, 0, 0);
@ -235,28 +247,20 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
avatarInset.setVisibility(View.GONE);
}
avatarRadius = avatarRadius48dp;
} else {
int padding = Utils.convertDpToPx(avatar.getContext(), 12);
avatar.setPaddingRelative(0, 0, padding, padding);
avatarInset.setVisibility(View.VISIBLE);
avatarInset.setBackground(null);
Glide.with(avatarInset)
.asBitmap()
.load(rebloggedUrl)
.placeholder(R.drawable.avatar_default)
.into(avatarInset);
ImageLoadingHelper.loadAvatar(rebloggedUrl, avatarInset, avatarRadius24dp, animateAvatar);
avatarRadius = avatarRadius36dp;
}
if (TextUtils.isEmpty(url)) {
avatar.setImageResource(R.drawable.avatar_default);
} else {
Glide.with(avatar)
.asBitmap()
.load(url)
.placeholder(R.drawable.avatar_default)
.into(avatar);
}
ImageLoadingHelper.loadAvatar(url, avatar, avatarRadius, animateAvatar);
}
@ -610,18 +614,22 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
protected void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
boolean mediaPreviewEnabled) {
this.setupWithStatus(status, listener, mediaPreviewEnabled, null);
boolean mediaPreviewEnabled, boolean showBotOverlay, boolean animateAvatar) {
this.setupWithStatus(status, listener, mediaPreviewEnabled, showBotOverlay, animateAvatar, null);
}
protected void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
boolean mediaPreviewEnabled, @Nullable Object payloads) {
protected void setupWithStatus(StatusViewData.Concrete status,
final StatusActionListener listener,
boolean mediaPreviewEnabled,
boolean showBotOverlay,
boolean animateAvatar,
@Nullable Object payloads) {
if (payloads == null) {
setDisplayName(status.getUserFullName(), status.getAccountEmojis());
setUsername(status.getNickname());
setCreatedAt(status.getCreatedAt());
setIsReply(status.getInReplyToId() != null);
setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot());
setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot(), showBotOverlay, animateAvatar);
setReblogged(status.isReblogged());
setFavourited(status.isFavourited());
List<Attachment> attachments = status.getAttachments();

View file

@ -125,8 +125,9 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
@Override
protected void setupWithStatus(final StatusViewData.Concrete status, final StatusActionListener listener,
boolean mediaPreviewEnabled, @Nullable Object payloads) {
super.setupWithStatus(status, listener, mediaPreviewEnabled, payloads);
boolean mediaPreviewEnabled, boolean showBotOverlay, boolean animateAvatar,
@Nullable Object payloads) {
super.setupWithStatus(status, listener, mediaPreviewEnabled, showBotOverlay, animateAvatar, payloads);
if (payloads == null) {
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);

View file

@ -51,14 +51,15 @@ public class StatusViewHolder extends StatusBaseViewHolder {
@Override
protected void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
boolean mediaPreviewEnabled, @Nullable Object payloads) {
boolean mediaPreviewEnabled, boolean showBotOverlay, boolean animateAvatar,
@Nullable Object payloads) {
if (status == null || payloads == null) {
if (status == null) {
showContent(false);
} else {
showContent(true);
setupCollapsedState(status, listener);
super.setupWithStatus(status, listener, mediaPreviewEnabled, null);
super.setupWithStatus(status, listener, mediaPreviewEnabled, showBotOverlay, animateAvatar, null);
String rebloggedByDisplayName = status.getRebloggedByUsername();
if (rebloggedByDisplayName == null) {
@ -70,7 +71,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
}
} else {
super.setupWithStatus(status, listener, mediaPreviewEnabled, payloads);
super.setupWithStatus(status, listener, mediaPreviewEnabled, showBotOverlay, animateAvatar, payloads);
}
}

View file

@ -37,6 +37,8 @@ public class ThreadAdapter extends RecyclerView.Adapter {
private StatusActionListener statusActionListener;
private boolean mediaPreviewEnabled;
private boolean useAbsoluteTime;
private boolean showBotOverlay;
private boolean animateAvatar;
private int detailedStatusPosition;
public ThreadAdapter(StatusActionListener listener) {
@ -44,6 +46,8 @@ public class ThreadAdapter extends RecyclerView.Adapter {
this.statuses = new ArrayList<>();
mediaPreviewEnabled = true;
useAbsoluteTime = false;
showBotOverlay = true;
animateAvatar = false;
detailedStatusPosition = RecyclerView.NO_POSITION;
}
@ -70,10 +74,10 @@ public class ThreadAdapter extends RecyclerView.Adapter {
StatusViewData.Concrete status = statuses.get(position);
if (position == detailedStatusPosition) {
StatusDetailedViewHolder holder = (StatusDetailedViewHolder) viewHolder;
holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled);
holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled, showBotOverlay, animateAvatar);
} else {
StatusViewHolder holder = (StatusViewHolder) viewHolder;
holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled);
holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled, showBotOverlay, animateAvatar);
}
}
@ -155,6 +159,14 @@ public class ThreadAdapter extends RecyclerView.Adapter {
this.useAbsoluteTime = useAbsoluteTime;
}
public void setShowBotOverlay(boolean showBotOverlay) {
this.showBotOverlay = showBotOverlay;
}
public void setAnimateAvatar(boolean animateAvatar) {
this.animateAvatar = animateAvatar;
}
public void setDetailedStatusPosition(int position) {
if (position != detailedStatusPosition
&& detailedStatusPosition != RecyclerView.NO_POSITION) {

View file

@ -43,14 +43,17 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
private final StatusActionListener statusListener;
private boolean mediaPreviewEnabled;
private boolean useAbsoluteTime;
private boolean showBotOverlay;
private boolean animateAvatar;
public TimelineAdapter(AdapterDataSource<StatusViewData> dataSource,
StatusActionListener statusListener) {
super();
this.dataSource = dataSource;
this.statusListener = statusListener;
mediaPreviewEnabled = true;
useAbsoluteTime = false;
showBotOverlay = true;
animateAvatar = false;
}
@NonNull
@ -89,7 +92,12 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
holder.setup(statusListener, ((StatusViewData.Placeholder) status).isLoading());
} else if (status instanceof StatusViewData.Concrete) {
StatusViewHolder holder = (StatusViewHolder) viewHolder;
holder.setupWithStatus((StatusViewData.Concrete)status,statusListener, mediaPreviewEnabled,payloads!=null&&!payloads.isEmpty()?payloads.get(0):null);
holder.setupWithStatus((StatusViewData.Concrete) status,
statusListener,
mediaPreviewEnabled,
showBotOverlay,
animateAvatar,
payloads != null && !payloads.isEmpty() ? payloads.get(0) : null);
}
}
@Override
@ -118,6 +126,14 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
return mediaPreviewEnabled;
}
public void setShowBotOverlay(boolean showBotOverlay) {
this.showBotOverlay = showBotOverlay;
}
public void setAnimateAvatar(boolean animateAvatar) {
this.animateAvatar = animateAvatar;
}
@Override
public long getItemId(int position) {
return dataSource.getItemAt(position).getViewDataId();

View file

@ -230,6 +230,10 @@ public class NotificationsFragment extends SFragment implements
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
adapter.setUseAbsoluteTime(useAbsoluteTime);
boolean showBotOverlay = preferences.getBoolean("showBotOverlay", true);
adapter.setShowBotOverlay(showBotOverlay);
boolean animateAvatar = preferences.getBoolean("animateGifAvatars", false);
adapter.setAnimateAvatar(animateAvatar);
recyclerView.setAdapter(adapter);
topLoading = false;
@ -734,7 +738,7 @@ public class NotificationsFragment extends SFragment implements
Log.w(TAG, "Didn't find a notification for ID: " + notificationId);
}
public void onPreferenceChanged(String key) {
private void onPreferenceChanged(String key) {
switch (key) {
case "fabHide": {
hideFab = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("fabHide", false);
@ -746,7 +750,6 @@ public class NotificationsFragment extends SFragment implements
adapter.setMediaPreviewEnabled(enabled);
fullyRefresh();
}
break;
}
}
}

View file

@ -46,8 +46,6 @@ class SearchFragment : SFragment(), StatusActionListener {
private lateinit var searchAdapter: SearchResultsAdapter
private var alwaysShowSensitiveMedia = false
private var mediaPreviewEnabled = true
private var useAbsoluteTime = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_search, container, false)
@ -55,20 +53,25 @@ class SearchFragment : SFragment(), StatusActionListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val preferences = PreferenceManager.getDefaultSharedPreferences(view.context)
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false)
val useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false)
val showBotOverlay = preferences.getBoolean("showBotOverlay", true)
val animateAvatar = preferences.getBoolean("animateGifAvatars", false)
val account = accountManager.activeAccount
alwaysShowSensitiveMedia = account?.alwaysShowSensitiveMedia ?: false
mediaPreviewEnabled = account?.mediaPreviewEnabled ?: true
val mediaPreviewEnabled = account?.mediaPreviewEnabled ?: true
searchRecyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
searchRecyclerView.layoutManager = LinearLayoutManager(view.context)
searchAdapter = SearchResultsAdapter(
this,
this,
mediaPreviewEnabled,
alwaysShowSensitiveMedia,
this,
this,
useAbsoluteTime)
useAbsoluteTime,
showBotOverlay,
animateAvatar
)
searchRecyclerView.adapter = searchAdapter
}

View file

@ -354,6 +354,10 @@ public class TimelineFragment extends SFragment implements
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
adapter.setUseAbsoluteTime(useAbsoluteTime);
boolean showBotOverlay = preferences.getBoolean("showBotOverlay", true);
adapter.setShowBotOverlay(showBotOverlay);
boolean animateAvatar = preferences.getBoolean("animateGifAvatars", false);
adapter.setAnimateAvatar(animateAvatar);
boolean filter = preferences.getBoolean("tabFilterHomeReplies", true);
filterRemoveReplies = kind == Kind.HOME && !filter;

View file

@ -0,0 +1,43 @@
@file:JvmName("ImageLoadingHelper")
package com.keylesspalace.tusky.util
import android.widget.ImageView
import androidx.annotation.Px
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.FitCenter
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.keylesspalace.tusky.R
private val fitCenterTransformation = FitCenter()
fun loadAvatar(url: String?, imageView: ImageView, @Px radius: Int, animate: Boolean) {
if(url.isNullOrBlank()) {
Glide.with(imageView)
.load(R.drawable.avatar_default)
.into(imageView)
} else {
if (animate) {
Glide.with(imageView)
.load(url)
.transform(
fitCenterTransformation,
RoundedCorners(radius)
)
.into(imageView)
} else {
Glide.with(imageView)
.asBitmap()
.load(url)
.transform(
fitCenterTransformation,
RoundedCorners(radius)
)
.into(imageView)
}
}
}

View file

@ -1,325 +0,0 @@
package com.keylesspalace.tusky.view;
/*
* Original CircleImageView Copyright 2014 - 2018 Henning Dodenhof
* Adapted to RoundedImageView by charlag in 2018
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import androidx.annotation.DrawableRes;
import androidx.appcompat.widget.AppCompatImageView;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewOutlineProvider;
public class RoundedImageView extends AppCompatImageView {
private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP;
private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
private static final int COLORDRAWABLE_DIMENSION = 2;
private static final int DEFAULT_BORDER_WIDTH = 0;
private static final int DEFAULT_BORDER_COLOR = Color.BLACK;
private static final int DEFAULT_CIRCLE_BACKGROUND_COLOR = Color.TRANSPARENT;
private static float ROUNDED_PERCENT = 25;
private final RectF mDrawableRect = new RectF();
private final RectF mBorderRect = new RectF();
private final Matrix mShaderMatrix = new Matrix();
private final Paint mBitmapPaint = new Paint();
private final Paint mBorderPaint = new Paint();
private final Paint mCircleBackgroundPaint = new Paint();
private int mCircleBackgroundColor = DEFAULT_CIRCLE_BACKGROUND_COLOR;
private Bitmap mBitmap;
private BitmapShader mBitmapShader;
private int mBitmapWidth;
private int mBitmapHeight;
private float mDrawableRadius;
private float mBorderRadius;
private ColorFilter mColorFilter;
private boolean mReady;
private boolean mSetupPending;
public RoundedImageView(Context context) {
super(context);
init();
}
public RoundedImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RoundedImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
super.setScaleType(SCALE_TYPE);
mReady = true;
setOutlineProvider(new OutlineProvider());
if (mSetupPending) {
setup();
mSetupPending = false;
}
}
@Override
public ScaleType getScaleType() {
return SCALE_TYPE;
}
@Override
public void setScaleType(ScaleType scaleType) {
if (scaleType != SCALE_TYPE) {
throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType));
}
}
@Override
public void setAdjustViewBounds(boolean adjustViewBounds) {
if (adjustViewBounds) {
throw new IllegalArgumentException("adjustViewBounds not supported.");
}
}
@Override
protected void onDraw(Canvas canvas) {
if (mBitmap == null) {
return;
}
if (mCircleBackgroundColor != Color.TRANSPARENT) {
canvas.drawRoundRect(mDrawableRect, mDrawableRadius, mDrawableRadius,
mCircleBackgroundPaint);
}
canvas.drawRoundRect(mDrawableRect, mDrawableRadius, mDrawableRadius, mBitmapPaint);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
setup();
}
@Override
public void setPadding(int left, int top, int right, int bottom) {
super.setPadding(left, top, right, bottom);
setup();
}
@Override
public void setPaddingRelative(int start, int top, int end, int bottom) {
super.setPaddingRelative(start, top, end, bottom);
setup();
}
@Override
public void setImageBitmap(Bitmap bm) {
super.setImageBitmap(bm);
initializeBitmap();
}
@Override
public void setImageDrawable(Drawable drawable) {
super.setImageDrawable(drawable);
initializeBitmap();
}
@Override
public void setImageResource(@DrawableRes int resId) {
super.setImageResource(resId);
initializeBitmap();
}
@Override
public void setImageURI(Uri uri) {
super.setImageURI(uri);
initializeBitmap();
}
@Override
public void setColorFilter(ColorFilter cf) {
if (cf == mColorFilter) {
return;
}
mColorFilter = cf;
applyColorFilter();
invalidate();
}
@Override
public ColorFilter getColorFilter() {
return mColorFilter;
}
private void applyColorFilter() {
mBitmapPaint.setColorFilter(mColorFilter);
}
private static Bitmap getBitmapFromDrawable(Drawable drawable) {
if (drawable == null) {
return null;
}
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
try {
Bitmap bitmap;
if (drawable instanceof ColorDrawable) {
bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG);
} else {
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG);
}
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private void initializeBitmap() {
mBitmap = getBitmapFromDrawable(getDrawable());
setup();
}
private void setup() {
if (!mReady) {
mSetupPending = true;
return;
}
if (getWidth() == 0 && getHeight() == 0) {
return;
}
if (mBitmap == null) {
invalidate();
return;
}
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mBitmapPaint.setAntiAlias(true);
mBitmapPaint.setShader(mBitmapShader);
mBorderPaint.setStyle(Paint.Style.STROKE);
mBorderPaint.setAntiAlias(true);
mBorderPaint.setColor(DEFAULT_BORDER_COLOR);
mBorderPaint.setStrokeWidth(DEFAULT_BORDER_WIDTH);
mCircleBackgroundPaint.setStyle(Paint.Style.FILL);
mCircleBackgroundPaint.setAntiAlias(true);
mCircleBackgroundPaint.setColor(mCircleBackgroundColor);
mBitmapHeight = mBitmap.getHeight();
mBitmapWidth = mBitmap.getWidth();
mBorderRect.set(calculateBounds());
float shorterSideBorder = Math.min(mBorderRect.width(), mBorderRect.height());
mBorderRadius = shorterSideBorder / 2 * ROUNDED_PERCENT / 100;
mDrawableRect.set(mBorderRect);
float shorterSide = Math.min(mDrawableRect.width(), mDrawableRect.height());
mDrawableRadius = shorterSide / 2 * ROUNDED_PERCENT / 100;
applyColorFilter();
updateShaderMatrix();
invalidate();
}
private RectF calculateBounds() {
int availableWidth = getWidth() - getPaddingLeft() - getPaddingRight();
int availableHeight = getHeight() - getPaddingTop() - getPaddingBottom();
int sideLength = Math.min(availableWidth, availableHeight);
float left = getPaddingLeft() + (availableWidth - sideLength) / 2f;
float top = getPaddingTop() + (availableHeight - sideLength) / 2f;
return new RectF(left, top, left + sideLength, top + sideLength);
}
private void updateShaderMatrix() {
float scale;
float dx = 0;
float dy = 0;
mShaderMatrix.set(null);
if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) {
scale = mDrawableRect.height() / (float) mBitmapHeight;
dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;
} else {
scale = mDrawableRect.width() / (float) mBitmapWidth;
dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f;
}
mShaderMatrix.setScale(scale, scale);
mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top);
mBitmapShader.setLocalMatrix(mShaderMatrix);
}
private class OutlineProvider extends ViewOutlineProvider {
@Override
public void getOutline(View view, Outline outline) {
Rect bounds = new Rect();
mBorderRect.roundOut(bounds);
outline.setRoundRect(bounds, mBorderRadius);
}
}
}

View file

@ -352,7 +352,7 @@
<include layout="@layout/item_status_bottom_sheet" />
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/accountAvatarImageView"
android:layout_width="@dimen/account_activity_avatar_size"
android:layout_height="@dimen/account_activity_avatar_size"

View file

@ -13,7 +13,7 @@
android:layout_marginBottom="8dp"
android:background="@android:color/transparent">
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/composeAvatar"
android:layout_width="?attr/actionBarSize"
android:layout_height="?attr/actionBarSize"

View file

@ -46,7 +46,7 @@
app:layout_constraintStart_toStartOf="@id/headerPreview"
app:layout_constraintTop_toTopOf="@id/headerPreview" />
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/avatarPreview"
android:layout_width="80dp"
android:layout_height="80dp"

View file

@ -8,7 +8,7 @@
android:paddingStart="16dp"
android:paddingEnd="16dp">
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/account_avatar"
android:layout_width="48dp"
android:layout_height="48dp"
@ -19,7 +19,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/avatar_default" />
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/account_avatar_inset"
android:layout_width="24dp"
android:layout_height="24dp"

View file

@ -6,7 +6,7 @@
android:gravity="center_vertical"
android:padding="8dp">
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/avatar"
android:layout_width="42dp"
android:layout_height="42dp"

View file

@ -8,7 +8,7 @@
android:paddingLeft="16dp"
android:paddingRight="16dp">
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/blocked_user_avatar"
android:layout_width="48dp"
android:layout_height="48dp"

View file

@ -29,7 +29,7 @@
tools:text="ConnyDuck boosted"
tools:visibility="visible" />
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/status_avatar_2"
android:layout_width="52dp"
android:layout_height="52dp"
@ -42,7 +42,7 @@
app:layout_constraintTop_toTopOf="@id/status_avatar_1"
tools:src="@drawable/avatar_default" />
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/status_avatar_1"
android:layout_width="52dp"
android:layout_height="52dp"
@ -55,7 +55,7 @@
app:layout_constraintTop_toTopOf="@id/status_avatar"
tools:src="@drawable/avatar_default" />
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/status_avatar"
android:layout_width="52dp"
android:layout_height="52dp"
@ -68,7 +68,7 @@
app:layout_constraintTop_toBottomOf="@id/conversation_name"
tools:src="@drawable/avatar_default" />
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/status_avatar_inset"
android:layout_width="24dp"
android:layout_height="24dp"

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!--
* This is the for folnotificationsEnabledions, the layout for the follows/following listings on account
* This is the for follow notifications, the layout for the follows/following listings on account
* pages are instead in item_account.xml.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
@ -27,7 +27,7 @@
android:textSize="?attr/status_text_medium"
tools:text="Someone followed you" />
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/notification_avatar"
android:layout_width="40dp"
android:layout_height="40dp"

View file

@ -8,7 +8,7 @@
android:paddingLeft="16dp"
android:paddingRight="16dp">
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/avatar"
android:layout_width="48dp"
android:layout_height="48dp"

View file

@ -8,7 +8,7 @@
android:paddingLeft="16dp"
android:paddingRight="16dp">
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/muted_user_avatar"
android:layout_width="48dp"
android:layout_height="48dp"

View file

@ -31,7 +31,7 @@
tools:text="ConnyDuck boosted"
tools:visibility="visible" />
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/status_avatar"
android:layout_width="48dp"
android:layout_height="48dp"
@ -43,7 +43,7 @@
app:layout_constraintTop_toBottomOf="@id/status_info"
tools:src="@drawable/avatar_default" />
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/status_avatar_inset"
android:layout_width="24dp"
android:layout_height="24dp"

View file

@ -11,7 +11,7 @@
android:paddingLeft="14dp"
android:paddingRight="14dp">
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/status_avatar"
android:layout_width="48dp"
android:layout_height="48dp"
@ -24,7 +24,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/avatar_default" />
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/status_avatar_inset"
android:layout_width="24dp"
android:layout_height="24dp"

View file

@ -137,7 +137,7 @@
android:textSize="?attr/status_text_medium"
android:visibility="gone" />
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/notification_status_avatar"
android:layout_width="48dp"
android:layout_height="48dp"
@ -153,7 +153,7 @@
tools:ignore="RtlHardcoded,RtlSymmetry"
tools:src="@drawable/avatar_default" />
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/notification_notification_avatar"
android:layout_width="24dp"
android:layout_height="24dp"

View file

@ -17,7 +17,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:text="Account has moved" />
<com.keylesspalace.tusky.view.RoundedImageView
<ImageView
android:id="@+id/accountMovedAvatar"
android:layout_width="48dp"
android:layout_height="48dp"

View file

@ -32,4 +32,12 @@
<dimen name="preference_icon_size">20dp</dimen>
<dimen name="selected_drag_item_elevation">12dp</dimen>
<dimen name="avatar_radius_94dp">11.75dp</dimen> <!-- 1/8 of 100dp - 2 * 3dp padding -->
<dimen name="avatar_radius_80dp">10dp</dimen> <!-- 1/8 of 80dp -->
<dimen name="avatar_radius_48dp">6dp</dimen> <!-- 1/8 of 48dp -->
<dimen name="avatar_radius_42dp">5.25dp</dimen> <!-- 1/8 of 42dp -->
<dimen name="avatar_radius_40dp">5dp</dimen> <!-- 1/8 of 40dp -->
<dimen name="avatar_radius_36dp">4.5dp</dimen> <!-- 1/8 of 36dp -->
<dimen name="avatar_radius_24dp">3dp</dimen> <!-- 1/8 of 24dp -->
</resources>

View file

@ -216,6 +216,9 @@
<string name="pref_title_custom_tabs">Use Chrome Custom Tabs</string>
<string name="pref_title_hide_follow_button">Hide compose button while scrolling</string>
<string name="pref_title_language">Language</string>
<string name="pref_title_bot_overlay">Show indicator for bots</string>
<string name="pref_title_animate_gif_avatars">Animate GIF avatars</string>
<string name="pref_title_status_filter">Timeline filtering</string>
<string name="pref_title_status_tabs">Tabs</string>
<string name="pref_title_show_boosts">Show boosts</string>
@ -463,7 +466,6 @@
<string name="compose_shortcut_long_label">Compose Toot</string>
<string name="compose_shortcut_short_label">Compose</string>
<string name="pref_title_bot_overlay">Show indicator for bots</string>
<string name="notification_clear_text">Are you sure you want to permanently clear all your notifications?</string>
<string name="compose_preview_image_description">Actions for image %s</string>

View file

@ -49,6 +49,11 @@
android:defaultValue="true"
android:key="showBotOverlay"
android:title="@string/pref_title_bot_overlay" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="animateGifAvatars"
android:title="@string/pref_title_animate_gif_avatars" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/pref_title_browser_settings">