From 83696b5c7f53e7f91c2ef66d60bffeffd692f869 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 26 May 2019 08:46:08 +0200 Subject: [PATCH] Animate gif avatars (#1279) * animate gif avatars * add setting to enable avatar animation * cleanup code --- .../keylesspalace/tusky/AccountActivity.kt | 26 +- .../keylesspalace/tusky/ComposeActivity.java | 43 +-- .../tusky/EditProfileActivity.kt | 28 +- .../com/keylesspalace/tusky/MainActivity.java | 25 +- .../tusky/PreferencesActivity.kt | 8 +- .../tusky/adapter/AccountSelectionAdapter.kt | 20 +- .../tusky/adapter/AccountViewHolder.java | 16 +- .../tusky/adapter/BlocksAdapter.java | 16 +- .../adapter/ComposeAutoCompleteAdapter.java | 22 +- .../tusky/adapter/FollowRequestsAdapter.java | 15 +- .../tusky/adapter/MutesAdapter.java | 15 +- .../tusky/adapter/NotificationsAdapter.java | 68 ++-- .../tusky/adapter/SearchResultsAdapter.java | 45 ++- .../tusky/adapter/StatusBaseViewHolder.java | 72 ++-- .../adapter/StatusDetailedViewHolder.java | 5 +- .../tusky/adapter/StatusViewHolder.java | 7 +- .../tusky/adapter/ThreadAdapter.java | 16 +- .../tusky/adapter/TimelineAdapter.java | 22 +- .../tusky/fragment/NotificationsFragment.java | 7 +- .../tusky/fragment/SearchFragment.kt | 17 +- .../tusky/fragment/TimelineFragment.java | 4 + .../tusky/util/ImageLoadingHelper.kt | 43 +++ .../tusky/view/RoundedImageView.java | 325 ------------------ app/src/main/res/layout/activity_account.xml | 4 +- app/src/main/res/layout/activity_compose.xml | 2 +- .../main/res/layout/activity_edit_profile.xml | 2 +- app/src/main/res/layout/item_account.xml | 4 +- .../res/layout/item_autocomplete_account.xml | 2 +- app/src/main/res/layout/item_blocked_user.xml | 2 +- app/src/main/res/layout/item_conversation.xml | 8 +- app/src/main/res/layout/item_follow.xml | 4 +- .../main/res/layout/item_follow_request.xml | 2 +- app/src/main/res/layout/item_muted_user.xml | 2 +- app/src/main/res/layout/item_status.xml | 4 +- .../main/res/layout/item_status_detailed.xml | 4 +- .../res/layout/item_status_notification.xml | 4 +- .../main/res/layout/view_account_moved.xml | 2 +- app/src/main/res/values/dimens.xml | 8 + app/src/main/res/values/strings.xml | 4 +- app/src/main/res/xml/preferences.xml | 5 + 40 files changed, 381 insertions(+), 547 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/view/RoundedImageView.java diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt index e419375f5..79ea0e938 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt @@ -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) diff --git a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java index 0b2371de8..fbdfdfe11 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java @@ -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, diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt index 516f3be70..60537d89d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt @@ -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> { when(it) { @@ -192,12 +198,26 @@ class EditProfileActivity : BaseActivity(), Injectable { } } - private fun observeImage(liveData: LiveData>, imageView: ImageView, progressBar: View) { + private fun observeImage(liveData: LiveData>, + imageView: ImageView, + progressBar: View, + roundedCorners: Boolean) { liveData.observe(this, Observer> { 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() } diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java index a6bb48956..993113072 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java @@ -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,14 +328,25 @@ 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) { - Glide.with(MainActivity.this) - .asBitmap() - .load(uri) - .placeholder(placeholder) - .into(imageView); + if(animateAvatars) { + Glide.with(MainActivity.this) + .load(uri) + .placeholder(placeholder) + .into(imageView); + } else { + Glide.with(MainActivity.this) + .asBitmap() + .load(uri) + .placeholder(placeholder) + .into(imageView); + } + } @Override diff --git a/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.kt b/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.kt index 9d6e2ec06..39032b28a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.kt @@ -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" -> { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt index d7d8ba852..74beda73a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt @@ -16,19 +16,19 @@ 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.* -class AccountSelectionAdapter(context: Context): ArrayAdapter(context, R.layout.item_autocomplete_account) { +class AccountSelectionAdapter(context: Context) : ArrayAdapter(context, R.layout.item_autocomplete_account) { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { var view = convertView @@ -45,13 +45,13 @@ class AccountSelectionAdapter(context: Context): ArrayAdapter(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 diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java index 87e4fc66a..4fdc68509 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java @@ -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); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java index 78acabf9a..cc861df24 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java @@ -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) { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ComposeAutoCompleteAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/ComposeAutoCompleteAdapter.java index db433ce76..6dede94f7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/ComposeAutoCompleteAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ComposeAutoCompleteAdapter.java @@ -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; diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java index fe0cd9523..a82a10ea4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java @@ -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) { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java index d5dd98a3e..5f494658b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java @@ -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) { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 1029784a7..68ff3af0f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -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 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 diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/SearchResultsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/SearchResultsAdapter.java index 8bef108ba..87978ed8a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/SearchResultsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/SearchResultsAdapter.java @@ -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; @@ -98,22 +104,23 @@ public class SearchResultsAdapter extends RecyclerView.Adapter { @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { - if (position >= accountList.size()) { - if(position >= accountList.size() + concreteStatusList.size()) { - HashtagViewHolder holder = (HashtagViewHolder) viewHolder; - int index = position - accountList.size() - concreteStatusList.size(); - holder.setup(hashtagList.get(index), linkListener); - } else { - StatusViewHolder holder = (StatusViewHolder) viewHolder; - int index = position - accountList.size(); - holder.setupWithStatus(concreteStatusList.get(index), statusListener, mediaPreviewsEnabled); - } + if (position >= accountList.size()) { + if(position >= accountList.size() + concreteStatusList.size()) { + HashtagViewHolder holder = (HashtagViewHolder) viewHolder; + int index = position - accountList.size() - concreteStatusList.size(); + holder.setup(hashtagList.get(index), linkListener); } else { - AccountViewHolder holder = (AccountViewHolder) viewHolder; - holder.setupWithAccount(accountList.get(position)); - holder.setupLinkListener(linkListener); + StatusViewHolder holder = (StatusViewHolder) viewHolder; + int index = position - accountList.size(); + holder.setupWithStatus(concreteStatusList.get(index), statusListener, + mediaPreviewsEnabled, showBotOverlay, animateAvatar); } + } else { + AccountViewHolder holder = (AccountViewHolder) viewHolder; + holder.setupWithAccount(accountList.get(position)); + holder.setupLinkListener(linkListener); } + } @Override public int getItemCount() { @@ -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()); } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index 490b97eaa..3c0c44fdb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -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 attachments = status.getAttachments(); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java index d6403a726..031c2d85e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -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); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java index 2474d394c..d16030d6f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -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); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java index 4f394da00..25376ccc2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java @@ -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) { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java index c7d95d178..2fc90395d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java @@ -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 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 @@ -111,13 +119,21 @@ public final class TimelineAdapter extends RecyclerView.Adapter { } public void setUseAbsoluteTime(boolean useAbsoluteTime){ - this.useAbsoluteTime=useAbsoluteTime; + this.useAbsoluteTime = useAbsoluteTime; } public boolean getMediaPreviewEnabled() { 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(); diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 834548c4d..f7c83d0cd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -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; } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SearchFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/SearchFragment.kt index f190a203b..075b23766 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SearchFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SearchFragment.kt @@ -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 } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index 46ceb0864..957b11b38 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -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; diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt new file mode 100644 index 000000000..b0e51f8b0 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt @@ -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) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/view/RoundedImageView.java b/app/src/main/java/com/keylesspalace/tusky/view/RoundedImageView.java deleted file mode 100644 index 43cac86e4..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/view/RoundedImageView.java +++ /dev/null @@ -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); - } - - } - -} diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml index 22d1d124b..c0311c9b0 100644 --- a/app/src/main/res/layout/activity_account.xml +++ b/app/src/main/res/layout/activity_account.xml @@ -352,7 +352,7 @@ - - \ No newline at end of file + diff --git a/app/src/main/res/layout/activity_compose.xml b/app/src/main/res/layout/activity_compose.xml index 97d78465f..b61c8dfc7 100644 --- a/app/src/main/res/layout/activity_compose.xml +++ b/app/src/main/res/layout/activity_compose.xml @@ -13,7 +13,7 @@ android:layout_marginBottom="8dp" android:background="@android:color/transparent"> - - - - - - - - - - - - - - - - - - - - 20dp 12dp + + 11.75dp + 10dp + 6dp + 5.25dp + 5dp + 4.5dp + 3dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a8730d6ef..6aae71129 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -216,6 +216,9 @@ Use Chrome Custom Tabs Hide compose button while scrolling Language + Show indicator for bots + Animate GIF avatars + Timeline filtering Tabs Show boosts @@ -463,7 +466,6 @@ Compose Toot Compose - Show indicator for bots Are you sure you want to permanently clear all your notifications? Actions for image %s diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index d158569d6..6e7f94677 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -49,6 +49,11 @@ android:defaultValue="true" android:key="showBotOverlay" android:title="@string/pref_title_bot_overlay" /> + +