Animate gif avatars (#1279)
* animate gif avatars * add setting to enable avatar animation * cleanup code
This commit is contained in:
		
					parent
					
						
							
								da1089184c
							
						
					
				
			
			
				commit
				
					
						83696b5c7f
					
				
			
		
					 40 changed files with 381 additions and 547 deletions
				
			
		|  | @ -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) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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() | ||||
|                 } | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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" -> { | ||||
|  |  | |||
|  | @ -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<AccountEntity>(context, R.layout.item_autocomplete_account) { | ||||
| class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(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<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 | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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()); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|     } | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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) | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -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); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -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" | ||||
|  | @ -364,4 +364,4 @@ | |||
|             app:layout_scrollFlags="scroll" | ||||
|             app:srcCompat="@drawable/avatar_default" /> | ||||
|     </androidx.coordinatorlayout.widget.CoordinatorLayout> | ||||
| </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> | ||||
| </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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"> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue