Content warnings
This commit is contained in:
parent
1a238d79e0
commit
e13eaf5ae5
|
@ -8,6 +8,7 @@ import android.graphics.Rect;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
@ -21,12 +22,15 @@ import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||||
import org.joinmastodon.android.model.Poll;
|
import org.joinmastodon.android.model.Poll;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||||
|
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.PhotoStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.PhotoStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||||
|
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
||||||
|
|
||||||
|
@ -34,9 +38,11 @@ import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
|
@ -283,6 +289,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
list.setItemAnimator(new BetterItemAnimator());
|
||||||
updateToolbar();
|
updateToolbar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -423,6 +430,73 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onRevealSpoilerClick(TextStatusDisplayItem.Holder holder){
|
||||||
|
Status status=holder.getItem().status;
|
||||||
|
revealSpoiler(status, holder.getItemID());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onRevealSpoilerClick(ImageStatusDisplayItem.Holder<?> holder){
|
||||||
|
Status status=holder.getItem().status;
|
||||||
|
revealSpoiler(status, holder.getItemID());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void revealSpoiler(Status status, String itemID){
|
||||||
|
status.spoilerRevealed=true;
|
||||||
|
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||||
|
if(text!=null)
|
||||||
|
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
|
||||||
|
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
||||||
|
if(header!=null)
|
||||||
|
header.rebind();
|
||||||
|
for(ImageStatusDisplayItem.Holder photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){
|
||||||
|
photo.setRevealed(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder){
|
||||||
|
Status status=holder.getItem().status;
|
||||||
|
status.spoilerRevealed=!status.spoilerRevealed;
|
||||||
|
if(!TextUtils.isEmpty(status.spoilerText)){
|
||||||
|
TextStatusDisplayItem.Holder text=findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class);
|
||||||
|
if(text!=null){
|
||||||
|
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder.rebind();
|
||||||
|
for(ImageStatusDisplayItem.Holder<?> photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(holder.getItemID(), ImageStatusDisplayItem.Holder.class)){
|
||||||
|
photo.setRevealed(status.spoilerRevealed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
protected <I extends StatusDisplayItem> I findItemOfType(String id, Class<I> type){
|
||||||
|
for(StatusDisplayItem item:displayItems){
|
||||||
|
if(item.parentID.equals(id) && type.isInstance(item))
|
||||||
|
return type.cast(item);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
protected <I extends StatusDisplayItem, H extends StatusDisplayItem.Holder<I>> H findHolderOfType(String id, Class<H> type){
|
||||||
|
for(int i=0;i<list.getChildCount();i++){
|
||||||
|
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||||
|
if(holder instanceof StatusDisplayItem.Holder && ((StatusDisplayItem.Holder<?>) holder).getItemID().equals(id) && type.isInstance(holder))
|
||||||
|
return type.cast(holder);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <I extends StatusDisplayItem, H extends StatusDisplayItem.Holder<I>> List<H> findAllHoldersOfType(String id, Class<H> type){
|
||||||
|
ArrayList<H> holders=new ArrayList<>();
|
||||||
|
for(int i=0;i<list.getChildCount();i++){
|
||||||
|
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||||
|
if(holder instanceof StatusDisplayItem.Holder && ((StatusDisplayItem.Holder<?>) holder).getItemID().equals(id) && type.isInstance(holder))
|
||||||
|
holders.add(type.cast(holder));
|
||||||
|
}
|
||||||
|
return holders;
|
||||||
|
}
|
||||||
|
|
||||||
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
||||||
|
|
||||||
public DisplayItemsAdapter(){
|
public DisplayItemsAdapter(){
|
||||||
|
|
|
@ -53,6 +53,8 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
||||||
public boolean bookmarked;
|
public boolean bookmarked;
|
||||||
public boolean pinned;
|
public boolean pinned;
|
||||||
|
|
||||||
|
public transient boolean spoilerRevealed;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postprocess() throws ObjectValidationException{
|
public void postprocess() throws ObjectValidationException{
|
||||||
super.postprocess();
|
super.postprocess();
|
||||||
|
@ -73,6 +75,8 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
||||||
card.postprocess();
|
card.postprocess();
|
||||||
if(reblog!=null)
|
if(reblog!=null)
|
||||||
reblog.postprocess();
|
reblog.postprocess();
|
||||||
|
|
||||||
|
spoilerRevealed=!sensitive;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -0,0 +1,670 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.joinmastodon.android.ui;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.AnimatorListenerAdapter;
|
||||||
|
import android.animation.TimeInterpolator;
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewPropertyAnimator;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator;
|
||||||
|
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This differs from DefaultItemAnimator by running all animations at once without delays.
|
||||||
|
*
|
||||||
|
* @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
|
||||||
|
*/
|
||||||
|
public class BetterItemAnimator extends SimpleItemAnimator{
|
||||||
|
private static final boolean DEBUG = false;
|
||||||
|
|
||||||
|
private static TimeInterpolator sDefaultInterpolator;
|
||||||
|
|
||||||
|
private ArrayList<RecyclerView.ViewHolder> mPendingRemovals = new ArrayList<>();
|
||||||
|
private ArrayList<RecyclerView.ViewHolder> mPendingAdditions = new ArrayList<>();
|
||||||
|
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
|
||||||
|
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
|
||||||
|
|
||||||
|
ArrayList<ArrayList<RecyclerView.ViewHolder>> mAdditionsList = new ArrayList<>();
|
||||||
|
ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
|
||||||
|
ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();
|
||||||
|
|
||||||
|
ArrayList<RecyclerView.ViewHolder> mAddAnimations = new ArrayList<>();
|
||||||
|
ArrayList<RecyclerView.ViewHolder> mMoveAnimations = new ArrayList<>();
|
||||||
|
ArrayList<RecyclerView.ViewHolder> mRemoveAnimations = new ArrayList<>();
|
||||||
|
ArrayList<RecyclerView.ViewHolder> mChangeAnimations = new ArrayList<>();
|
||||||
|
|
||||||
|
private static class MoveInfo {
|
||||||
|
public RecyclerView.ViewHolder holder;
|
||||||
|
public int fromX, fromY, toX, toY;
|
||||||
|
|
||||||
|
MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
|
||||||
|
this.holder = holder;
|
||||||
|
this.fromX = fromX;
|
||||||
|
this.fromY = fromY;
|
||||||
|
this.toX = toX;
|
||||||
|
this.toY = toY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ChangeInfo {
|
||||||
|
public RecyclerView.ViewHolder oldHolder, newHolder;
|
||||||
|
public int fromX, fromY, toX, toY;
|
||||||
|
private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) {
|
||||||
|
this.oldHolder = oldHolder;
|
||||||
|
this.newHolder = newHolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
|
||||||
|
int fromX, int fromY, int toX, int toY) {
|
||||||
|
this(oldHolder, newHolder);
|
||||||
|
this.fromX = fromX;
|
||||||
|
this.fromY = fromY;
|
||||||
|
this.toX = toX;
|
||||||
|
this.toY = toY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ChangeInfo{"
|
||||||
|
+ "oldHolder=" + oldHolder
|
||||||
|
+ ", newHolder=" + newHolder
|
||||||
|
+ ", fromX=" + fromX
|
||||||
|
+ ", fromY=" + fromY
|
||||||
|
+ ", toX=" + toX
|
||||||
|
+ ", toY=" + toY
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BetterItemAnimator(){
|
||||||
|
setAddDuration(250);
|
||||||
|
setRemoveDuration(250);
|
||||||
|
setChangeDuration(250);
|
||||||
|
setMoveDuration(250);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runPendingAnimations() {
|
||||||
|
boolean removalsPending = !mPendingRemovals.isEmpty();
|
||||||
|
boolean movesPending = !mPendingMoves.isEmpty();
|
||||||
|
boolean changesPending = !mPendingChanges.isEmpty();
|
||||||
|
boolean additionsPending = !mPendingAdditions.isEmpty();
|
||||||
|
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
|
||||||
|
// nothing to animate
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// First, remove stuff
|
||||||
|
for (RecyclerView.ViewHolder holder : mPendingRemovals) {
|
||||||
|
animateRemoveImpl(holder);
|
||||||
|
}
|
||||||
|
mPendingRemovals.clear();
|
||||||
|
// Next, move stuff
|
||||||
|
if (movesPending) {
|
||||||
|
final ArrayList<MoveInfo> moves = new ArrayList<>();
|
||||||
|
moves.addAll(mPendingMoves);
|
||||||
|
mMovesList.add(moves);
|
||||||
|
mPendingMoves.clear();
|
||||||
|
Runnable mover = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
for (MoveInfo moveInfo : moves) {
|
||||||
|
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
|
||||||
|
moveInfo.toX, moveInfo.toY);
|
||||||
|
}
|
||||||
|
moves.clear();
|
||||||
|
mMovesList.remove(moves);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (removalsPending) {
|
||||||
|
View view = moves.get(0).holder.itemView;
|
||||||
|
view.postOnAnimation(mover);
|
||||||
|
} else {
|
||||||
|
mover.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Next, change stuff, to run in parallel with move animations
|
||||||
|
if (changesPending) {
|
||||||
|
final ArrayList<ChangeInfo> changes = new ArrayList<>();
|
||||||
|
changes.addAll(mPendingChanges);
|
||||||
|
mChangesList.add(changes);
|
||||||
|
mPendingChanges.clear();
|
||||||
|
Runnable changer = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
for (ChangeInfo change : changes) {
|
||||||
|
animateChangeImpl(change);
|
||||||
|
}
|
||||||
|
changes.clear();
|
||||||
|
mChangesList.remove(changes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (removalsPending) {
|
||||||
|
RecyclerView.ViewHolder holder = changes.get(0).oldHolder;
|
||||||
|
holder.itemView.postOnAnimation(changer);
|
||||||
|
} else {
|
||||||
|
changer.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Next, add stuff
|
||||||
|
if (additionsPending) {
|
||||||
|
final ArrayList<RecyclerView.ViewHolder> additions = new ArrayList<>();
|
||||||
|
additions.addAll(mPendingAdditions);
|
||||||
|
mAdditionsList.add(additions);
|
||||||
|
mPendingAdditions.clear();
|
||||||
|
Runnable adder = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
for (RecyclerView.ViewHolder holder : additions) {
|
||||||
|
animateAddImpl(holder);
|
||||||
|
}
|
||||||
|
additions.clear();
|
||||||
|
mAdditionsList.remove(additions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (removalsPending || movesPending || changesPending) {
|
||||||
|
View view = additions.get(0).itemView;
|
||||||
|
view.postOnAnimation(adder);
|
||||||
|
} else {
|
||||||
|
adder.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean animateRemove(final RecyclerView.ViewHolder holder) {
|
||||||
|
resetAnimation(holder);
|
||||||
|
mPendingRemovals.add(holder);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
|
||||||
|
final View view = holder.itemView;
|
||||||
|
final ViewPropertyAnimator animation = view.animate();
|
||||||
|
mRemoveAnimations.add(holder);
|
||||||
|
animation.setDuration(getRemoveDuration()).alpha(0).setListener(
|
||||||
|
new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationStart(Animator animator) {
|
||||||
|
dispatchRemoveStarting(holder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animator) {
|
||||||
|
animation.setListener(null);
|
||||||
|
view.setAlpha(1);
|
||||||
|
dispatchRemoveFinished(holder);
|
||||||
|
mRemoveAnimations.remove(holder);
|
||||||
|
dispatchFinishedWhenDone();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean animateAdd(final RecyclerView.ViewHolder holder) {
|
||||||
|
resetAnimation(holder);
|
||||||
|
holder.itemView.setAlpha(0);
|
||||||
|
mPendingAdditions.add(holder);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void animateAddImpl(final RecyclerView.ViewHolder holder) {
|
||||||
|
final View view = holder.itemView;
|
||||||
|
final ViewPropertyAnimator animation = view.animate();
|
||||||
|
mAddAnimations.add(holder);
|
||||||
|
animation.alpha(1).setDuration(getAddDuration())
|
||||||
|
.setListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationStart(Animator animator) {
|
||||||
|
dispatchAddStarting(holder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationCancel(Animator animator) {
|
||||||
|
view.setAlpha(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animator) {
|
||||||
|
animation.setListener(null);
|
||||||
|
dispatchAddFinished(holder);
|
||||||
|
mAddAnimations.remove(holder);
|
||||||
|
dispatchFinishedWhenDone();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY,
|
||||||
|
int toX, int toY) {
|
||||||
|
final View view = holder.itemView;
|
||||||
|
fromX += (int) holder.itemView.getTranslationX();
|
||||||
|
fromY += (int) holder.itemView.getTranslationY();
|
||||||
|
resetAnimation(holder);
|
||||||
|
int deltaX = toX - fromX;
|
||||||
|
int deltaY = toY - fromY;
|
||||||
|
if (deltaX == 0 && deltaY == 0) {
|
||||||
|
dispatchMoveFinished(holder);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (deltaX != 0) {
|
||||||
|
view.setTranslationX(-deltaX);
|
||||||
|
}
|
||||||
|
if (deltaY != 0) {
|
||||||
|
view.setTranslationY(-deltaY);
|
||||||
|
}
|
||||||
|
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
|
||||||
|
final View view = holder.itemView;
|
||||||
|
final int deltaX = toX - fromX;
|
||||||
|
final int deltaY = toY - fromY;
|
||||||
|
if (deltaX != 0) {
|
||||||
|
view.animate().translationX(0);
|
||||||
|
}
|
||||||
|
if (deltaY != 0) {
|
||||||
|
view.animate().translationY(0);
|
||||||
|
}
|
||||||
|
// TODO: make EndActions end listeners instead, since end actions aren't called when
|
||||||
|
// vpas are canceled (and can't end them. why?)
|
||||||
|
// need listener functionality in VPACompat for this. Ick.
|
||||||
|
final ViewPropertyAnimator animation = view.animate();
|
||||||
|
mMoveAnimations.add(holder);
|
||||||
|
animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationStart(Animator animator) {
|
||||||
|
dispatchMoveStarting(holder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationCancel(Animator animator) {
|
||||||
|
if (deltaX != 0) {
|
||||||
|
view.setTranslationX(0);
|
||||||
|
}
|
||||||
|
if (deltaY != 0) {
|
||||||
|
view.setTranslationY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animator) {
|
||||||
|
animation.setListener(null);
|
||||||
|
dispatchMoveFinished(holder);
|
||||||
|
mMoveAnimations.remove(holder);
|
||||||
|
dispatchFinishedWhenDone();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
|
||||||
|
int fromX, int fromY, int toX, int toY) {
|
||||||
|
if (oldHolder == newHolder) {
|
||||||
|
// Don't know how to run change animations when the same view holder is re-used.
|
||||||
|
// run a move animation to handle position changes.
|
||||||
|
return animateMove(oldHolder, fromX, fromY, toX, toY);
|
||||||
|
}
|
||||||
|
final float prevTranslationX = oldHolder.itemView.getTranslationX();
|
||||||
|
final float prevTranslationY = oldHolder.itemView.getTranslationY();
|
||||||
|
final float prevAlpha = oldHolder.itemView.getAlpha();
|
||||||
|
resetAnimation(oldHolder);
|
||||||
|
int deltaX = (int) (toX - fromX - prevTranslationX);
|
||||||
|
int deltaY = (int) (toY - fromY - prevTranslationY);
|
||||||
|
// recover prev translation state after ending animation
|
||||||
|
oldHolder.itemView.setTranslationX(prevTranslationX);
|
||||||
|
oldHolder.itemView.setTranslationY(prevTranslationY);
|
||||||
|
oldHolder.itemView.setAlpha(prevAlpha);
|
||||||
|
if (newHolder != null) {
|
||||||
|
// carry over translation values
|
||||||
|
resetAnimation(newHolder);
|
||||||
|
newHolder.itemView.setTranslationX(-deltaX);
|
||||||
|
newHolder.itemView.setTranslationY(-deltaY);
|
||||||
|
newHolder.itemView.setAlpha(0);
|
||||||
|
}
|
||||||
|
mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void animateChangeImpl(final ChangeInfo changeInfo) {
|
||||||
|
final RecyclerView.ViewHolder holder = changeInfo.oldHolder;
|
||||||
|
final View view = holder == null ? null : holder.itemView;
|
||||||
|
final RecyclerView.ViewHolder newHolder = changeInfo.newHolder;
|
||||||
|
final View newView = newHolder != null ? newHolder.itemView : null;
|
||||||
|
if (view != null) {
|
||||||
|
final ViewPropertyAnimator oldViewAnim = view.animate().setDuration(
|
||||||
|
getChangeDuration());
|
||||||
|
mChangeAnimations.add(changeInfo.oldHolder);
|
||||||
|
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
|
||||||
|
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
|
||||||
|
oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationStart(Animator animator) {
|
||||||
|
dispatchChangeStarting(changeInfo.oldHolder, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animator) {
|
||||||
|
oldViewAnim.setListener(null);
|
||||||
|
view.setAlpha(1);
|
||||||
|
view.setTranslationX(0);
|
||||||
|
view.setTranslationY(0);
|
||||||
|
dispatchChangeFinished(changeInfo.oldHolder, true);
|
||||||
|
mChangeAnimations.remove(changeInfo.oldHolder);
|
||||||
|
dispatchFinishedWhenDone();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
if (newView != null) {
|
||||||
|
final ViewPropertyAnimator newViewAnimation = newView.animate();
|
||||||
|
mChangeAnimations.add(changeInfo.newHolder);
|
||||||
|
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration())
|
||||||
|
.alpha(1).setListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationStart(Animator animator) {
|
||||||
|
dispatchChangeStarting(changeInfo.newHolder, false);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animator) {
|
||||||
|
newViewAnimation.setListener(null);
|
||||||
|
newView.setAlpha(1);
|
||||||
|
newView.setTranslationX(0);
|
||||||
|
newView.setTranslationY(0);
|
||||||
|
dispatchChangeFinished(changeInfo.newHolder, false);
|
||||||
|
mChangeAnimations.remove(changeInfo.newHolder);
|
||||||
|
dispatchFinishedWhenDone();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void endChangeAnimation(List<ChangeInfo> infoList, RecyclerView.ViewHolder item) {
|
||||||
|
for (int i = infoList.size() - 1; i >= 0; i--) {
|
||||||
|
ChangeInfo changeInfo = infoList.get(i);
|
||||||
|
if (endChangeAnimationIfNecessary(changeInfo, item)) {
|
||||||
|
if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
|
||||||
|
infoList.remove(changeInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
|
||||||
|
if (changeInfo.oldHolder != null) {
|
||||||
|
endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
|
||||||
|
}
|
||||||
|
if (changeInfo.newHolder != null) {
|
||||||
|
endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) {
|
||||||
|
boolean oldItem = false;
|
||||||
|
if (changeInfo.newHolder == item) {
|
||||||
|
changeInfo.newHolder = null;
|
||||||
|
} else if (changeInfo.oldHolder == item) {
|
||||||
|
changeInfo.oldHolder = null;
|
||||||
|
oldItem = true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
item.itemView.setAlpha(1);
|
||||||
|
item.itemView.setTranslationX(0);
|
||||||
|
item.itemView.setTranslationY(0);
|
||||||
|
dispatchChangeFinished(item, oldItem);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endAnimation(RecyclerView.ViewHolder item) {
|
||||||
|
final View view = item.itemView;
|
||||||
|
// this will trigger end callback which should set properties to their target values.
|
||||||
|
view.animate().cancel();
|
||||||
|
// TODO if some other animations are chained to end, how do we cancel them as well?
|
||||||
|
for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
|
||||||
|
MoveInfo moveInfo = mPendingMoves.get(i);
|
||||||
|
if (moveInfo.holder == item) {
|
||||||
|
view.setTranslationY(0);
|
||||||
|
view.setTranslationX(0);
|
||||||
|
dispatchMoveFinished(item);
|
||||||
|
mPendingMoves.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endChangeAnimation(mPendingChanges, item);
|
||||||
|
if (mPendingRemovals.remove(item)) {
|
||||||
|
view.setAlpha(1);
|
||||||
|
dispatchRemoveFinished(item);
|
||||||
|
}
|
||||||
|
if (mPendingAdditions.remove(item)) {
|
||||||
|
view.setAlpha(1);
|
||||||
|
dispatchAddFinished(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = mChangesList.size() - 1; i >= 0; i--) {
|
||||||
|
ArrayList<ChangeInfo> changes = mChangesList.get(i);
|
||||||
|
endChangeAnimation(changes, item);
|
||||||
|
if (changes.isEmpty()) {
|
||||||
|
mChangesList.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = mMovesList.size() - 1; i >= 0; i--) {
|
||||||
|
ArrayList<MoveInfo> moves = mMovesList.get(i);
|
||||||
|
for (int j = moves.size() - 1; j >= 0; j--) {
|
||||||
|
MoveInfo moveInfo = moves.get(j);
|
||||||
|
if (moveInfo.holder == item) {
|
||||||
|
view.setTranslationY(0);
|
||||||
|
view.setTranslationX(0);
|
||||||
|
dispatchMoveFinished(item);
|
||||||
|
moves.remove(j);
|
||||||
|
if (moves.isEmpty()) {
|
||||||
|
mMovesList.remove(i);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
|
||||||
|
ArrayList<RecyclerView.ViewHolder> additions = mAdditionsList.get(i);
|
||||||
|
if (additions.remove(item)) {
|
||||||
|
view.setAlpha(1);
|
||||||
|
dispatchAddFinished(item);
|
||||||
|
if (additions.isEmpty()) {
|
||||||
|
mAdditionsList.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// animations should be ended by the cancel above.
|
||||||
|
//noinspection PointlessBooleanExpression,ConstantConditions
|
||||||
|
if (mRemoveAnimations.remove(item) && DEBUG) {
|
||||||
|
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||||
|
+ "mRemoveAnimations list");
|
||||||
|
}
|
||||||
|
|
||||||
|
//noinspection PointlessBooleanExpression,ConstantConditions
|
||||||
|
if (mAddAnimations.remove(item) && DEBUG) {
|
||||||
|
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||||
|
+ "mAddAnimations list");
|
||||||
|
}
|
||||||
|
|
||||||
|
//noinspection PointlessBooleanExpression,ConstantConditions
|
||||||
|
if (mChangeAnimations.remove(item) && DEBUG) {
|
||||||
|
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||||
|
+ "mChangeAnimations list");
|
||||||
|
}
|
||||||
|
|
||||||
|
//noinspection PointlessBooleanExpression,ConstantConditions
|
||||||
|
if (mMoveAnimations.remove(item) && DEBUG) {
|
||||||
|
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||||
|
+ "mMoveAnimations list");
|
||||||
|
}
|
||||||
|
dispatchFinishedWhenDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetAnimation(RecyclerView.ViewHolder holder) {
|
||||||
|
if (sDefaultInterpolator == null) {
|
||||||
|
sDefaultInterpolator =CubicBezierInterpolator.DEFAULT;
|
||||||
|
}
|
||||||
|
holder.itemView.animate().setInterpolator(sDefaultInterpolator);
|
||||||
|
endAnimation(holder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isRunning() {
|
||||||
|
return (!mPendingAdditions.isEmpty()
|
||||||
|
|| !mPendingChanges.isEmpty()
|
||||||
|
|| !mPendingMoves.isEmpty()
|
||||||
|
|| !mPendingRemovals.isEmpty()
|
||||||
|
|| !mMoveAnimations.isEmpty()
|
||||||
|
|| !mRemoveAnimations.isEmpty()
|
||||||
|
|| !mAddAnimations.isEmpty()
|
||||||
|
|| !mChangeAnimations.isEmpty()
|
||||||
|
|| !mMovesList.isEmpty()
|
||||||
|
|| !mAdditionsList.isEmpty()
|
||||||
|
|| !mChangesList.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the state of currently pending and running animations. If there are none
|
||||||
|
* pending/running, call {@link #dispatchAnimationsFinished()} to notify any
|
||||||
|
* listeners.
|
||||||
|
*/
|
||||||
|
void dispatchFinishedWhenDone() {
|
||||||
|
if (!isRunning()) {
|
||||||
|
dispatchAnimationsFinished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endAnimations() {
|
||||||
|
int count = mPendingMoves.size();
|
||||||
|
for (int i = count - 1; i >= 0; i--) {
|
||||||
|
MoveInfo item = mPendingMoves.get(i);
|
||||||
|
View view = item.holder.itemView;
|
||||||
|
view.setTranslationY(0);
|
||||||
|
view.setTranslationX(0);
|
||||||
|
dispatchMoveFinished(item.holder);
|
||||||
|
mPendingMoves.remove(i);
|
||||||
|
}
|
||||||
|
count = mPendingRemovals.size();
|
||||||
|
for (int i = count - 1; i >= 0; i--) {
|
||||||
|
RecyclerView.ViewHolder item = mPendingRemovals.get(i);
|
||||||
|
dispatchRemoveFinished(item);
|
||||||
|
mPendingRemovals.remove(i);
|
||||||
|
}
|
||||||
|
count = mPendingAdditions.size();
|
||||||
|
for (int i = count - 1; i >= 0; i--) {
|
||||||
|
RecyclerView.ViewHolder item = mPendingAdditions.get(i);
|
||||||
|
item.itemView.setAlpha(1);
|
||||||
|
dispatchAddFinished(item);
|
||||||
|
mPendingAdditions.remove(i);
|
||||||
|
}
|
||||||
|
count = mPendingChanges.size();
|
||||||
|
for (int i = count - 1; i >= 0; i--) {
|
||||||
|
endChangeAnimationIfNecessary(mPendingChanges.get(i));
|
||||||
|
}
|
||||||
|
mPendingChanges.clear();
|
||||||
|
if (!isRunning()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int listCount = mMovesList.size();
|
||||||
|
for (int i = listCount - 1; i >= 0; i--) {
|
||||||
|
ArrayList<MoveInfo> moves = mMovesList.get(i);
|
||||||
|
count = moves.size();
|
||||||
|
for (int j = count - 1; j >= 0; j--) {
|
||||||
|
MoveInfo moveInfo = moves.get(j);
|
||||||
|
RecyclerView.ViewHolder item = moveInfo.holder;
|
||||||
|
View view = item.itemView;
|
||||||
|
view.setTranslationY(0);
|
||||||
|
view.setTranslationX(0);
|
||||||
|
dispatchMoveFinished(moveInfo.holder);
|
||||||
|
moves.remove(j);
|
||||||
|
if (moves.isEmpty()) {
|
||||||
|
mMovesList.remove(moves);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listCount = mAdditionsList.size();
|
||||||
|
for (int i = listCount - 1; i >= 0; i--) {
|
||||||
|
ArrayList<RecyclerView.ViewHolder> additions = mAdditionsList.get(i);
|
||||||
|
count = additions.size();
|
||||||
|
for (int j = count - 1; j >= 0; j--) {
|
||||||
|
RecyclerView.ViewHolder item = additions.get(j);
|
||||||
|
View view = item.itemView;
|
||||||
|
view.setAlpha(1);
|
||||||
|
dispatchAddFinished(item);
|
||||||
|
additions.remove(j);
|
||||||
|
if (additions.isEmpty()) {
|
||||||
|
mAdditionsList.remove(additions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listCount = mChangesList.size();
|
||||||
|
for (int i = listCount - 1; i >= 0; i--) {
|
||||||
|
ArrayList<ChangeInfo> changes = mChangesList.get(i);
|
||||||
|
count = changes.size();
|
||||||
|
for (int j = count - 1; j >= 0; j--) {
|
||||||
|
endChangeAnimationIfNecessary(changes.get(j));
|
||||||
|
if (changes.isEmpty()) {
|
||||||
|
mChangesList.remove(changes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelAll(mRemoveAnimations);
|
||||||
|
cancelAll(mMoveAnimations);
|
||||||
|
cancelAll(mAddAnimations);
|
||||||
|
cancelAll(mChangeAnimations);
|
||||||
|
|
||||||
|
dispatchAnimationsFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancelAll(List<RecyclerView.ViewHolder> viewHolders) {
|
||||||
|
for (int i = viewHolders.size() - 1; i >= 0; i--) {
|
||||||
|
viewHolders.get(i).itemView.animate().cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
* <p>
|
||||||
|
* If the payload list is not empty, DefaultItemAnimator returns <code>true</code>.
|
||||||
|
* When this is the case:
|
||||||
|
* <ul>
|
||||||
|
* <li>If you override {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}, both
|
||||||
|
* ViewHolder arguments will be the same instance.
|
||||||
|
* </li>
|
||||||
|
* <li>
|
||||||
|
* If you are not overriding {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)},
|
||||||
|
* then DefaultItemAnimator will call {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int)} and
|
||||||
|
* run a move animation instead.
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder,
|
||||||
|
@NonNull List<Object> payloads) {
|
||||||
|
return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import android.graphics.drawable.Animatable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewOutlineProvider;
|
import android.view.ViewOutlineProvider;
|
||||||
|
@ -17,6 +18,8 @@ import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.Attachment;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
@ -33,21 +36,31 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||||
private Account user;
|
private Account user;
|
||||||
private Instant createdAt;
|
private Instant createdAt;
|
||||||
private ImageLoaderRequest avaRequest;
|
private ImageLoaderRequest avaRequest;
|
||||||
private Fragment parentFragment;
|
|
||||||
private String accountID;
|
private String accountID;
|
||||||
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||||
private SpannableStringBuilder parsedName;
|
private SpannableStringBuilder parsedName;
|
||||||
|
public final Status status;
|
||||||
|
private boolean hasVisibilityToggle;
|
||||||
|
|
||||||
public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID){
|
public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status){
|
||||||
super(parentID, parentFragment);
|
super(parentID, parentFragment);
|
||||||
this.user=user;
|
this.user=user;
|
||||||
this.createdAt=createdAt;
|
this.createdAt=createdAt;
|
||||||
avaRequest=new UrlImageLoaderRequest(user.avatar);
|
avaRequest=new UrlImageLoaderRequest(user.avatar);
|
||||||
this.parentFragment=parentFragment;
|
|
||||||
this.accountID=accountID;
|
this.accountID=accountID;
|
||||||
parsedName=new SpannableStringBuilder(user.displayName);
|
parsedName=new SpannableStringBuilder(user.displayName);
|
||||||
|
this.status=status;
|
||||||
HtmlParser.parseCustomEmoji(parsedName, user.emojis);
|
HtmlParser.parseCustomEmoji(parsedName, user.emojis);
|
||||||
emojiHelper.setText(parsedName);
|
emojiHelper.setText(parsedName);
|
||||||
|
hasVisibilityToggle=status.sensitive || !TextUtils.isEmpty(status.spoilerText);
|
||||||
|
if(!hasVisibilityToggle && !status.mediaAttachments.isEmpty()){
|
||||||
|
for(Attachment att:status.mediaAttachments){
|
||||||
|
if(att.type!=Attachment.Type.AUDIO){
|
||||||
|
hasVisibilityToggle=true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -70,7 +83,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||||
|
|
||||||
public static class Holder extends StatusDisplayItem.Holder<HeaderStatusDisplayItem> implements ImageLoaderViewHolder{
|
public static class Holder extends StatusDisplayItem.Holder<HeaderStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||||
private final TextView name, username, timestamp;
|
private final TextView name, username, timestamp;
|
||||||
private final ImageView avatar, more;
|
private final ImageView avatar, more, visibility;
|
||||||
|
|
||||||
private static final ViewOutlineProvider roundCornersOutline=new ViewOutlineProvider(){
|
private static final ViewOutlineProvider roundCornersOutline=new ViewOutlineProvider(){
|
||||||
@Override
|
@Override
|
||||||
|
@ -86,10 +99,12 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||||
timestamp=findViewById(R.id.timestamp);
|
timestamp=findViewById(R.id.timestamp);
|
||||||
avatar=findViewById(R.id.avatar);
|
avatar=findViewById(R.id.avatar);
|
||||||
more=findViewById(R.id.more);
|
more=findViewById(R.id.more);
|
||||||
|
visibility=findViewById(R.id.visibility);
|
||||||
avatar.setOnClickListener(this::onAvaClick);
|
avatar.setOnClickListener(this::onAvaClick);
|
||||||
avatar.setOutlineProvider(roundCornersOutline);
|
avatar.setOutlineProvider(roundCornersOutline);
|
||||||
avatar.setClipToOutline(true);
|
avatar.setClipToOutline(true);
|
||||||
more.setOnClickListener(this::onMoreClick);
|
more.setOnClickListener(this::onMoreClick);
|
||||||
|
visibility.setOnClickListener(v->item.parentFragment.onVisibilityIconClick(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -97,6 +112,10 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||||
name.setText(item.parsedName);
|
name.setText(item.parsedName);
|
||||||
username.setText('@'+item.user.acct);
|
username.setText('@'+item.user.acct);
|
||||||
timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt));
|
timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt));
|
||||||
|
visibility.setVisibility(item.hasVisibilityToggle ? View.VISIBLE : View.GONE);
|
||||||
|
if(item.hasVisibilityToggle){
|
||||||
|
visibility.setImageResource(item.status.spoilerRevealed ? R.drawable.ic_visibility_off : R.drawable.ic_visibility);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -11,6 +11,7 @@ import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
import org.joinmastodon.android.model.Attachment;
|
import org.joinmastodon.android.model.Attachment;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
|
||||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
||||||
|
|
||||||
import androidx.annotation.LayoutRes;
|
import androidx.annotation.LayoutRes;
|
||||||
|
@ -23,13 +24,11 @@ public abstract class ImageStatusDisplayItem extends StatusDisplayItem{
|
||||||
public final int totalPhotos;
|
public final int totalPhotos;
|
||||||
protected Attachment attachment;
|
protected Attachment attachment;
|
||||||
protected ImageLoaderRequest request;
|
protected ImageLoaderRequest request;
|
||||||
protected Fragment parentFragment;
|
public final Status status;
|
||||||
protected Status status;
|
|
||||||
|
|
||||||
public ImageStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Attachment photo, Status status, int index, int totalPhotos){
|
public ImageStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Attachment photo, Status status, int index, int totalPhotos){
|
||||||
super(parentID, parentFragment);
|
super(parentID, parentFragment);
|
||||||
this.attachment=photo;
|
this.attachment=photo;
|
||||||
this.parentFragment=parentFragment;
|
|
||||||
this.status=status;
|
this.status=status;
|
||||||
this.index=index;
|
this.index=index;
|
||||||
this.totalPhotos=totalPhotos;
|
this.totalPhotos=totalPhotos;
|
||||||
|
@ -47,6 +46,8 @@ public abstract class ImageStatusDisplayItem extends StatusDisplayItem{
|
||||||
|
|
||||||
public static abstract class Holder<T extends ImageStatusDisplayItem> extends StatusDisplayItem.Holder<T> implements ImageLoaderViewHolder{
|
public static abstract class Holder<T extends ImageStatusDisplayItem> extends StatusDisplayItem.Holder<T> implements ImageLoaderViewHolder{
|
||||||
public final ImageView photo;
|
public final ImageView photo;
|
||||||
|
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
|
||||||
|
private boolean didClear;
|
||||||
|
|
||||||
public Holder(Activity activity, @LayoutRes int layout, ViewGroup parent){
|
public Holder(Activity activity, @LayoutRes int layout, ViewGroup parent){
|
||||||
super(activity, layout, parent);
|
super(activity, layout, parent);
|
||||||
|
@ -56,24 +57,38 @@ public abstract class ImageStatusDisplayItem extends StatusDisplayItem{
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBind(ImageStatusDisplayItem item){
|
public void onBind(ImageStatusDisplayItem item){
|
||||||
|
crossfadeDrawable.setSize(item.attachment.getWidth(), item.attachment.getHeight());
|
||||||
|
crossfadeDrawable.setBlurhashDrawable(item.attachment.blurhashPlaceholder);
|
||||||
|
crossfadeDrawable.setCrossfadeAlpha(item.status.spoilerRevealed ? 0f : 1f);
|
||||||
|
photo.setImageDrawable(null);
|
||||||
|
photo.setImageDrawable(crossfadeDrawable);
|
||||||
|
didClear=false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setImage(int index, Drawable drawable){
|
public void setImage(int index, Drawable drawable){
|
||||||
photo.setImageDrawable(drawable);
|
crossfadeDrawable.setImageDrawable(drawable);
|
||||||
|
if(didClear && item.status.spoilerRevealed)
|
||||||
|
crossfadeDrawable.animateAlpha(0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clearImage(int index){
|
public void clearImage(int index){
|
||||||
photo.setImageDrawable(item.attachment.blurhashPlaceholder);
|
crossfadeDrawable.setCrossfadeAlpha(1f);
|
||||||
|
didClear=true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onViewClick(View v){
|
private void onViewClick(View v){
|
||||||
if(item.parentFragment instanceof PhotoViewerHost){
|
if(!item.status.spoilerRevealed){
|
||||||
|
item.parentFragment.onRevealSpoilerClick(this);
|
||||||
|
}else if(item.parentFragment instanceof PhotoViewerHost){
|
||||||
Status contentStatus=item.status.reblog!=null ? item.status.reblog : item.status;
|
Status contentStatus=item.status.reblog!=null ? item.status.reblog : item.status;
|
||||||
((PhotoViewerHost) item.parentFragment).openPhotoViewer(item.parentID, item.status, contentStatus.mediaAttachments.indexOf(item.attachment));
|
((PhotoViewerHost) item.parentFragment).openPhotoViewer(item.parentID, item.status, contentStatus.mediaAttachments.indexOf(item.attachment));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setRevealed(boolean revealed){
|
||||||
|
crossfadeDrawable.animateAlpha(revealed ? 0f : 1f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,9 +68,9 @@ public abstract class StatusDisplayItem{
|
||||||
Account account=Objects.requireNonNull(knownAccounts.get(status.inReplyToAccountId));
|
Account account=Objects.requireNonNull(knownAccounts.get(status.inReplyToAccountId));
|
||||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_fluent_arrow_reply_20_filled));
|
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_fluent_arrow_reply_20_filled));
|
||||||
}
|
}
|
||||||
items.add(new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID));
|
items.add(new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent));
|
||||||
if(!TextUtils.isEmpty(statusForContent.content))
|
if(!TextUtils.isEmpty(statusForContent.content))
|
||||||
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, accountID), fragment));
|
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, accountID), fragment, statusForContent));
|
||||||
int photoIndex=0;
|
int photoIndex=0;
|
||||||
int totalPhotos=0;
|
int totalPhotos=0;
|
||||||
for(Attachment attachment:statusForContent.mediaAttachments){
|
for(Attachment attachment:statusForContent.mediaAttachments){
|
||||||
|
@ -80,13 +80,13 @@ public abstract class StatusDisplayItem{
|
||||||
}
|
}
|
||||||
for(Attachment attachment:statusForContent.mediaAttachments){
|
for(Attachment attachment:statusForContent.mediaAttachments){
|
||||||
if(attachment.type==Attachment.Type.IMAGE){
|
if(attachment.type==Attachment.Type.IMAGE){
|
||||||
items.add(new PhotoStatusDisplayItem(parentID, status, attachment, fragment, photoIndex, totalPhotos));
|
items.add(new PhotoStatusDisplayItem(parentID, statusForContent, attachment, fragment, photoIndex, totalPhotos));
|
||||||
photoIndex++;
|
photoIndex++;
|
||||||
}else if(attachment.type==Attachment.Type.GIFV){
|
}else if(attachment.type==Attachment.Type.GIFV){
|
||||||
items.add(new GifVStatusDisplayItem(parentID, status, attachment, fragment, photoIndex, totalPhotos));
|
items.add(new GifVStatusDisplayItem(parentID, statusForContent, attachment, fragment, photoIndex, totalPhotos));
|
||||||
photoIndex++;
|
photoIndex++;
|
||||||
}else if(attachment.type==Attachment.Type.VIDEO){
|
}else if(attachment.type==Attachment.Type.VIDEO){
|
||||||
items.add(new VideoStatusDisplayItem(parentID, status, attachment, fragment, photoIndex, totalPhotos));
|
items.add(new VideoStatusDisplayItem(parentID, statusForContent, attachment, fragment, photoIndex, totalPhotos));
|
||||||
photoIndex++;
|
photoIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,14 @@ package org.joinmastodon.android.ui.displayitems;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.graphics.drawable.Animatable;
|
import android.graphics.drawable.Animatable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.ui.views.LinkedTextView;
|
import org.joinmastodon.android.ui.views.LinkedTextView;
|
||||||
|
|
||||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||||
|
@ -16,9 +20,12 @@ import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||||
public class TextStatusDisplayItem extends StatusDisplayItem{
|
public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||||
private CharSequence text;
|
private CharSequence text;
|
||||||
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||||
public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment){
|
public final Status status;
|
||||||
|
|
||||||
|
public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status){
|
||||||
super(parentID, parentFragment);
|
super(parentID, parentFragment);
|
||||||
this.text=text;
|
this.text=text;
|
||||||
|
this.status=status;
|
||||||
emojiHelper.setText(text);
|
emojiHelper.setText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,16 +46,37 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||||
|
|
||||||
public static class Holder extends StatusDisplayItem.Holder<TextStatusDisplayItem> implements ImageLoaderViewHolder{
|
public static class Holder extends StatusDisplayItem.Holder<TextStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||||
private final LinkedTextView text;
|
private final LinkedTextView text;
|
||||||
|
private final TextView spoilerTitle;
|
||||||
|
private final View spoilerOverlay;
|
||||||
|
|
||||||
public Holder(Activity activity, ViewGroup parent){
|
public Holder(Activity activity, ViewGroup parent){
|
||||||
super(activity, R.layout.display_item_text, parent);
|
super(activity, R.layout.display_item_text, parent);
|
||||||
text=findViewById(R.id.text);
|
text=findViewById(R.id.text);
|
||||||
|
spoilerTitle=findViewById(R.id.spoiler_title);
|
||||||
|
spoilerOverlay=findViewById(R.id.spoiler_overlay);
|
||||||
|
itemView.setOnClickListener(v->item.parentFragment.onRevealSpoilerClick(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBind(TextStatusDisplayItem item){
|
public void onBind(TextStatusDisplayItem item){
|
||||||
text.setText(item.text);
|
text.setText(item.text);
|
||||||
text.setInvalidateOnEveryFrame(false);
|
text.setInvalidateOnEveryFrame(false);
|
||||||
|
if(!TextUtils.isEmpty(item.status.spoilerText)){
|
||||||
|
spoilerTitle.setText(item.status.spoilerText);
|
||||||
|
if(item.status.spoilerRevealed){
|
||||||
|
spoilerOverlay.setVisibility(View.GONE);
|
||||||
|
text.setVisibility(View.VISIBLE);
|
||||||
|
itemView.setClickable(false);
|
||||||
|
}else{
|
||||||
|
spoilerOverlay.setVisibility(View.VISIBLE);
|
||||||
|
text.setVisibility(View.INVISIBLE);
|
||||||
|
itemView.setClickable(true);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
spoilerOverlay.setVisibility(View.GONE);
|
||||||
|
text.setVisibility(View.VISIBLE);
|
||||||
|
itemView.setClickable(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
package org.joinmastodon.android.ui.drawables;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.AnimatorListenerAdapter;
|
||||||
|
import android.animation.ObjectAnimator;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.ColorFilter;
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.FloatProperty;
|
||||||
|
import android.util.Property;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||||
|
|
||||||
|
public class BlurhashCrossfadeDrawable extends Drawable{
|
||||||
|
|
||||||
|
private int width, height;
|
||||||
|
private Drawable blurhashDrawable, imageDrawable;
|
||||||
|
private float blurhashAlpha=1f;
|
||||||
|
private ObjectAnimator currentAnim;
|
||||||
|
|
||||||
|
private static Property<BlurhashCrossfadeDrawable, Float> BLURHASH_ALPHA;
|
||||||
|
|
||||||
|
static{
|
||||||
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||||
|
BLURHASH_ALPHA=new FloatProperty<>(""){
|
||||||
|
@Override
|
||||||
|
public Float get(BlurhashCrossfadeDrawable object){
|
||||||
|
return object.blurhashAlpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setValue(BlurhashCrossfadeDrawable object, float value){
|
||||||
|
object.blurhashAlpha=value;
|
||||||
|
object.invalidateSelf();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}else{
|
||||||
|
BLURHASH_ALPHA=new Property<>(Float.class, ""){
|
||||||
|
@Override
|
||||||
|
public Float get(BlurhashCrossfadeDrawable object){
|
||||||
|
return object.blurhashAlpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void set(BlurhashCrossfadeDrawable object, Float value){
|
||||||
|
object.blurhashAlpha=value;
|
||||||
|
object.invalidateSelf();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSize(int w, int h){
|
||||||
|
width=w;
|
||||||
|
height=h;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBlurhashDrawable(Drawable blurhashDrawable){
|
||||||
|
this.blurhashDrawable=blurhashDrawable;
|
||||||
|
invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageDrawable(Drawable imageDrawable){
|
||||||
|
this.imageDrawable=imageDrawable;
|
||||||
|
invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(@NonNull Canvas canvas){
|
||||||
|
if(imageDrawable!=null && blurhashAlpha<1f){
|
||||||
|
imageDrawable.setBounds(getBounds());
|
||||||
|
imageDrawable.draw(canvas);
|
||||||
|
}
|
||||||
|
if(blurhashDrawable!=null && blurhashAlpha>0f){
|
||||||
|
blurhashDrawable.setBounds(getBounds());
|
||||||
|
blurhashDrawable.setAlpha(Math.round(255*blurhashAlpha));
|
||||||
|
blurhashDrawable.draw(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAlpha(int alpha){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setColorFilter(@Nullable ColorFilter colorFilter){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOpacity(){
|
||||||
|
return PixelFormat.OPAQUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getIntrinsicWidth(){
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getIntrinsicHeight(){
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void animateAlpha(float target){
|
||||||
|
if(currentAnim!=null)
|
||||||
|
currentAnim.cancel();
|
||||||
|
ObjectAnimator anim=ObjectAnimator.ofFloat(this, BLURHASH_ALPHA, target);
|
||||||
|
anim.setDuration(250);
|
||||||
|
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||||
|
anim.addListener(new AnimatorListenerAdapter(){
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation){
|
||||||
|
currentAnim=null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
anim.start();
|
||||||
|
currentAnim=anim;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCrossfadeAlpha(float alpha){
|
||||||
|
blurhashAlpha=alpha;
|
||||||
|
invalidateSelf();
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ import androidx.annotation.Nullable;
|
||||||
public class BlurHashDrawable extends Drawable{
|
public class BlurHashDrawable extends Drawable{
|
||||||
private final Bitmap bitmap;
|
private final Bitmap bitmap;
|
||||||
private final int width, height;
|
private final int width, height;
|
||||||
private static final Paint PAINT=new Paint(Paint.FILTER_BITMAP_FLAG);
|
private final Paint paint=new Paint(Paint.FILTER_BITMAP_FLAG);
|
||||||
|
|
||||||
public BlurHashDrawable(Bitmap bitmap, int width, int height){
|
public BlurHashDrawable(Bitmap bitmap, int width, int height){
|
||||||
this.bitmap=bitmap;
|
this.bitmap=bitmap;
|
||||||
|
@ -23,12 +23,12 @@ public class BlurHashDrawable extends Drawable{
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void draw(@NonNull Canvas canvas){
|
public void draw(@NonNull Canvas canvas){
|
||||||
canvas.drawBitmap(bitmap, null, getBounds(), PAINT);
|
canvas.drawBitmap(bitmap, null, getBounds(), paint);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setAlpha(int alpha){
|
public void setAlpha(int alpha){
|
||||||
|
paint.setAlpha(alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector android:height="20dp"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="20dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector android:height="20dp"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="20dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z"/>
|
||||||
|
</vector>
|
|
@ -17,6 +17,17 @@
|
||||||
android:scaleType="center"
|
android:scaleType="center"
|
||||||
android:src="@drawable/ic_post_more" />
|
android:src="@drawable/ic_post_more" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/visibility"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_below="@id/more"
|
||||||
|
android:background="?android:selectableItemBackgroundBorderless"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:tint="?android:textColorSecondary"
|
||||||
|
android:src="@drawable/ic_visibility" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/avatar"
|
android:id="@+id/avatar"
|
||||||
android:layout_width="46dp"
|
android:layout_width="46dp"
|
||||||
|
@ -42,6 +53,7 @@
|
||||||
android:layout_height="20dp"
|
android:layout_height="20dp"
|
||||||
android:layout_below="@id/name"
|
android:layout_below="@id/name"
|
||||||
android:layout_toEndOf="@id/avatar"
|
android:layout_toEndOf="@id/avatar"
|
||||||
|
android:layout_toStartOf="@id/visibility"
|
||||||
android:layoutDirection="locale"
|
android:layoutDirection="locale"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingLeft="16dp"
|
android:paddingLeft="16dp"
|
||||||
|
@ -13,4 +14,28 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textAppearance="@style/m3_body_large"/>
|
android:textAppearance="@style/m3_body_large"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/spoiler_overlay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/spoiler_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:textAppearance="@style/m3_title_large"
|
||||||
|
tools:text="CW title"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:text="@string/tap_to_reveal"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
|
@ -123,4 +123,5 @@
|
||||||
<string name="button_muted">Muted</string>
|
<string name="button_muted">Muted</string>
|
||||||
<string name="button_blocked">Blocked</string>
|
<string name="button_blocked">Blocked</string>
|
||||||
<string name="action_vote">Vote</string>
|
<string name="action_vote">Vote</string>
|
||||||
|
<string name="tap_to_reveal">Tap to reveal</string>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in New Issue