download control details

Closes #4063
// FREEBIE
This commit is contained in:
Jake McGinty 2015-09-09 18:05:21 -10:00 committed by Moxie Marlinspike
parent 0794380ca8
commit 92b2da0286
17 changed files with 177 additions and 84 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -4,19 +4,22 @@
<com.pnikosis.materialishprogress.ProgressWheel <com.pnikosis.materialishprogress.ProgressWheel
android:id="@+id/progress_wheel" android:id="@+id/progress_wheel"
android:layout_width="match_parent" android:layout_width="@dimen/transfer_controls_contracted_width"
android:layout_height="match_parent" android:layout_height="@dimen/transfer_controls_contracted_width"
android:background="@drawable/progress_background"
android:visibility="gone" android:visibility="gone"
android:layout_gravity="center"
app:matProg_barColor="@color/white" app:matProg_barColor="@color/white"
app:matProg_linearProgress="true" app:matProg_linearProgress="true"
app:matProg_spinSpeed="0.333" /> app:matProg_spinSpeed="0.333" />
<ImageButton android:id="@+id/download_button" <TextView android:id="@+id/download_details"
android:layout_width="match_parent" android:layout_width="@dimen/transfer_controls_expanded_width"
android:layout_height="match_parent" android:layout_height="@dimen/transfer_controls_contracted_width"
android:layout_gravity="center" android:padding="15dp"
android:background="@drawable/progress_background" android:gravity="center"
android:src="@drawable/ic_file_download_white_36dp" android:textColor="?conversation_item_received_text_primary_color"
android:visibility="gone" /> android:drawableLeft="@drawable/ic_file_download_white_36dp"
android:textSize="16dp"
android:visibility="gone"
android:textStyle="bold" />
</merge> </merge>

View File

@ -34,4 +34,7 @@
<dimen name="quick_media_drawer_default_height">250dp</dimen> <dimen name="quick_media_drawer_default_height">250dp</dimen>
<dimen name="quick_camera_shutter_ring_size">52dp</dimen> <dimen name="quick_camera_shutter_ring_size">52dp</dimen>
<dimen name="transfer_controls_expanded_width">150dp</dimen>
<dimen name="transfer_controls_contracted_width">70dp</dimen>
</resources> </resources>

View File

@ -384,6 +384,11 @@
<string name="RegistrationService_registration_error">Registration error</string> <string name="RegistrationService_registration_error">Registration error</string>
<string name="RegistrationService_textsecure_registration_has_encountered_a_problem">TextSecure registration has encountered a problem.</string> <string name="RegistrationService_textsecure_registration_has_encountered_a_problem">TextSecure registration has encountered a problem.</string>
<!-- Slide -->
<string name="Slide_image">Image</string>
<string name="Slide_audio">Audio</string>
<string name="Slide_video">Video</string>
<!-- SmsMessageRecord --> <!-- SmsMessageRecord -->
<string name="SmsMessageRecord_received_corrupted_key_exchange_message">Received corrupted key <string name="SmsMessageRecord_received_corrupted_key_exchange_message">Received corrupted key
exchange message! exchange message!

View File

@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.components;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -12,21 +11,27 @@ import android.view.animation.AnimationUtils;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
public class AnimatingToggle extends FrameLayout { public class AnimatingToggle extends FrameLayout {
private View current; private View current;
private final Animation inAnimation;
private final Animation outAnimation;
public AnimatingToggle(Context context) { public AnimatingToggle(Context context) {
super(context); this(context, null);
} }
public AnimatingToggle(Context context, AttributeSet attrs) { public AnimatingToggle(Context context, AttributeSet attrs) {
super(context, attrs); this(context, attrs, 0);
} }
public AnimatingToggle(Context context, AttributeSet attrs, int defStyleAttr) { public AnimatingToggle(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
this.outAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.animation_toggle_out);
this.inAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.animation_toggle_in);
} }
@Override @Override
@ -43,45 +48,10 @@ public class AnimatingToggle extends FrameLayout {
} }
public void display(@Nullable View view) { public void display(@Nullable View view) {
display(view, true);
}
protected void display(@Nullable View view, boolean animated) {
if (view == current) return; if (view == current) return;
if (current != null) ViewUtil.animateOut(current, outAnimation);
if (animated) { if (view != null) ViewUtil.animateIn(view, inAnimation);
if (current != null) animateOut(current, AnimationUtils.loadAnimation(getContext(), R.anim.animation_toggle_out));
if (view != null) animateIn(view, AnimationUtils.loadAnimation(getContext(), R.anim.animation_toggle_in));
} else {
if (current != null) current.setVisibility(GONE);
if (view != null) view.setVisibility(VISIBLE);
}
current = view; current = view;
} }
private void animateOut(final View view, Animation animation) {
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
view.setVisibility(View.GONE);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
view.startAnimation(animation);
}
private void animateIn(View view, Animation animation) {
animation.setInterpolator(new FastOutSlowInInterpolator());
view.setVisibility(View.VISIBLE);
view.startAnimation(animation);
}
} }

View File

@ -91,7 +91,6 @@ public class ThumbnailView extends FrameLayout {
private TransferControlView getTransferControls() { private TransferControlView getTransferControls() {
if (transferControls == null) transferControls = ViewUtil.inflateStub(this, R.id.transfer_controls_stub); if (transferControls == null) transferControls = ViewUtil.inflateStub(this, R.id.transfer_controls_stub);
return transferControls; return transferControls;
} }
@ -100,7 +99,8 @@ public class ThumbnailView extends FrameLayout {
} }
public void setImageResource(@Nullable MasterSecret masterSecret, public void setImageResource(@Nullable MasterSecret masterSecret,
long id, long timestamp, long id,
long timestamp,
@NonNull ListenableFutureTask<SlideDeck> slideDeckFuture) @NonNull ListenableFutureTask<SlideDeck> slideDeckFuture)
{ {
if (this.slideDeckFuture != null && this.slideDeckListener != null) { if (this.slideDeckFuture != null && this.slideDeckListener != null) {
@ -126,15 +126,17 @@ public class ThumbnailView extends FrameLayout {
Log.w(TAG, "Not re-loading slide " + slide.getPart().getPartId()); Log.w(TAG, "Not re-loading slide " + slide.getPart().getPartId());
return; return;
} }
if (!isContextValid()) { if (!isContextValid()) {
Log.w(TAG, "Not loading slide, context is invalid"); Log.w(TAG, "Not loading slide, context is invalid");
return; return;
} }
Log.w(TAG, "loading part with id " + slide.getPart().getPartId() + ", progress " + slide.getTransferProgress()); Log.w(TAG, "loading part with id " + slide.getPart().getPartId()
+ ", progress " + slide.getTransferProgress());
this.slide = slide; this.slide = slide;
buildGlideRequest(slide, masterSecret).into(image); loadInto(slide, masterSecret, image);
if (this.slide.getTransferProgress() == PartDatabase.TRANSFER_PROGRESS_DONE) { if (this.slide.getTransferProgress() == PartDatabase.TRANSFER_PROGRESS_DONE) {
setOnClickListener(new ThumbnailClickDispatcher()); setOnClickListener(new ThumbnailClickDispatcher());
@ -145,7 +147,6 @@ public class ThumbnailView extends FrameLayout {
if (!hideControls) { if (!hideControls) {
getTransferControls().setSlide(slide); getTransferControls().setSlide(slide);
getTransferControls().setDownloadClickListener(new DownloadClickDispatcher()); getTransferControls().setDownloadClickListener(new DownloadClickDispatcher());
getTransferControls().setVisibility(View.VISIBLE);
} }
} }
@ -189,29 +190,27 @@ public class ThumbnailView extends FrameLayout {
!((Activity)getContext()).isDestroyed(); !((Activity)getContext()).isDestroyed();
} }
private GenericRequestBuilder buildGlideRequest(@NonNull Slide slide, private void loadInto(@NonNull Slide slide,
@Nullable MasterSecret masterSecret) @Nullable MasterSecret masterSecret,
@NonNull ImageView view)
{ {
final GenericRequestBuilder builder;
if (slide.getThumbnailUri() != null) { if (slide.getThumbnailUri() != null) {
builder = buildThumbnailGlideRequest(slide, masterSecret); buildThumbnailGlideRequest(slide, masterSecret).into(view);
} else if (!slide.isInProgress()) {
buildPlaceholderGlideRequest(slide).into(view);
} else { } else {
builder = buildPlaceholderGlideRequest(slide); Glide.clear(view);
}
if (slide.getTransferProgress() != PartDatabase.TRANSFER_PROGRESS_DONE && !hideControls) {
return builder;
} else {
return builder.error(R.drawable.ic_missing_thumbnail_picture);
} }
} }
private GenericRequestBuilder buildThumbnailGlideRequest(Slide slide, MasterSecret masterSecret) { private GenericRequestBuilder buildThumbnailGlideRequest(Slide slide, MasterSecret masterSecret) {
final GenericRequestBuilder builder; final GenericRequestBuilder builder;
if (slide.isDraft()) builder = buildDraftGlideRequest(slide, masterSecret); if (slide.isDraft()) builder = buildDraftGlideRequest(slide, masterSecret);
else builder = buildPartGlideRequest(slide, masterSecret); else builder = buildPartGlideRequest(slide, masterSecret);
return builder;
if (slide.isInProgress()) return builder;
else return builder.error(R.drawable.ic_missing_thumbnail_picture);
} }
private GenericRequestBuilder buildDraftGlideRequest(Slide slide, MasterSecret masterSecret) { private GenericRequestBuilder buildDraftGlideRequest(Slide slide, MasterSecret masterSecret) {

View File

@ -3,9 +3,18 @@ package org.thoughtcrime.securesms.components;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.widget.ImageButton; import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.ValueAnimator;
import com.nineoldandroids.animation.ValueAnimator.AnimatorUpdateListener;
import com.pnikosis.materialishprogress.ProgressWheel; import com.pnikosis.materialishprogress.ProgressWheel;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
@ -17,10 +26,18 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import de.greenrobot.event.EventBus; import de.greenrobot.event.EventBus;
public class TransferControlView extends AnimatingToggle { public class TransferControlView extends FrameLayout {
private Slide slide; private static final int TRANSITION_MS = 300;
private ProgressWheel progressWheel;
private ImageButton downloadButton; @Nullable private Slide slide;
@Nullable private View current;
private final ProgressWheel progressWheel;
private final TextView downloadDetails;
private final Animation inAnimation;
private final Animation outAnimation;
private final int contractedWidth;
private final int expandedWidth;
public TransferControlView(Context context) { public TransferControlView(Context context) {
this(context, null); this(context, null);
@ -33,8 +50,18 @@ public class TransferControlView extends AnimatingToggle {
public TransferControlView(Context context, AttributeSet attrs, int defStyleAttr) { public TransferControlView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
inflate(context, R.layout.transfer_controls_view, this); inflate(context, R.layout.transfer_controls_view, this);
setBackgroundResource(R.drawable.transfer_controls_background);
setVisibility(GONE);
this.progressWheel = ViewUtil.findById(this, R.id.progress_wheel); this.progressWheel = ViewUtil.findById(this, R.id.progress_wheel);
this.downloadButton = ViewUtil.findById(this, R.id.download_button); this.downloadDetails = ViewUtil.findById(this, R.id.download_details);
this.contractedWidth = getResources().getDimensionPixelSize(R.dimen.transfer_controls_contracted_width);
this.expandedWidth = getResources().getDimensionPixelSize(R.dimen.transfer_controls_expanded_width);
this.outAnimation = new AlphaAnimation(1f, 0f);
this.inAnimation = new AlphaAnimation(0f, 1f);
this.outAnimation.setInterpolator(new FastOutSlowInInterpolator());
this.inAnimation.setInterpolator(new FastOutSlowInInterpolator());
this.outAnimation.setDuration(TRANSITION_MS);
this.inAnimation.setDuration(TRANSITION_MS);
} }
@Override protected void onAttachedToWindow() { @Override protected void onAttachedToWindow() {
@ -49,13 +76,13 @@ public class TransferControlView extends AnimatingToggle {
public void setSlide(final @NonNull Slide slide) { public void setSlide(final @NonNull Slide slide) {
this.slide = slide; this.slide = slide;
if (slide.getTransferProgress() == PartDatabase.TRANSFER_PROGRESS_STARTED) { if (slide.getTransferProgress() == PartDatabase.TRANSFER_PROGRESS_STARTED) {
showProgressSpinner(); showProgressSpinner();
} else if (slide.isPendingDownload()) { } else if (slide.isPendingDownload()) {
display(downloadButton); downloadDetails.setText(slide.getContentDescription());
display(downloadDetails);
} else { } else {
display(null, false); display(null);
} }
} }
@ -65,21 +92,65 @@ public class TransferControlView extends AnimatingToggle {
} }
public void setDownloadClickListener(final @Nullable OnClickListener listener) { public void setDownloadClickListener(final @Nullable OnClickListener listener) {
downloadButton.setOnClickListener(listener); downloadDetails.setOnClickListener(listener);
} }
public void clear() { public void clear() {
display(null, false); clearAnimation();
setVisibility(GONE);
if (current != null) {
current.clearAnimation();
current.setVisibility(GONE);
}
current = null;
slide = null; slide = null;
} }
private void display(@Nullable final View view) {
final int sourceWidth = current == downloadDetails ? expandedWidth : contractedWidth;
final int targetWidth = view == downloadDetails ? expandedWidth : contractedWidth;
if (current == view || current == null) {
ViewGroup.LayoutParams layoutParams = getLayoutParams();
layoutParams.width = targetWidth;
setLayoutParams(layoutParams);
} else {
ViewUtil.animateOut(current, outAnimation);
Animator anim = getWidthAnimator(sourceWidth, targetWidth);
anim.start();
}
if (view == null) {
ViewUtil.animateOut(this, outAnimation);
} else {
ViewUtil.animateIn(this, inAnimation);
ViewUtil.animateIn(view, inAnimation);
}
current = view;
}
private Animator getWidthAnimator(final int from, final int to) {
final ValueAnimator anim = ValueAnimator.ofInt(from, to);
anim.addUpdateListener(new AnimatorUpdateListener() {
@Override public void onAnimationUpdate(ValueAnimator animation) {
final int val = (Integer)animation.getAnimatedValue();
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
layoutParams.width = val;
setLayoutParams(layoutParams);
}
});
anim.setInterpolator(new FastOutSlowInInterpolator());
anim.setDuration(TRANSITION_MS);
return anim;
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
public void onEventAsync(final PartProgressEvent event) { public void onEventAsync(final PartProgressEvent event) {
if (this.slide != null && event.partId.equals(this.slide.getPart().getPartId())) { if (this.slide != null && event.partId.equals(this.slide.getPart().getPartId())) {
Util.runOnMain(new Runnable() { Util.runOnMain(new Runnable() {
@Override public void run() { @Override public void run() {
progressWheel.setInstantProgress(((float)event.progress) / event.total); progressWheel.setInstantProgress(((float)event.progress) / event.total);
if (event.progress >= event.total) display(null);
} }
}); });
} }

View File

@ -33,6 +33,8 @@ import android.view.animation.AlphaAnimation;
import android.view.animation.Animation; import android.view.animation.Animation;
import android.widget.Toast; import android.widget.Toast;
import junit.framework.Assert;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;

View File

@ -20,6 +20,7 @@ import android.content.Context;
import android.content.res.Resources.Theme; import android.content.res.Resources.Theme;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.DrawableRes; import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ResUtil; import org.thoughtcrime.securesms.util.ResUtil;
@ -49,6 +50,10 @@ public class AudioSlide extends Slide {
return true; return true;
} }
@NonNull @Override public String getContentDescription() {
return context.getString(R.string.Slide_audio);
}
@Override @Override
public @DrawableRes int getPlaceholderRes(Theme theme) { public @DrawableRes int getPlaceholderRes(Theme theme) {
return ResUtil.getDrawableRes(theme, R.attr.conversation_icon_attach_audio); return ResUtil.getDrawableRes(theme, R.attr.conversation_icon_attach_audio);

View File

@ -20,6 +20,7 @@ import android.content.Context;
import android.content.res.Resources.Theme; import android.content.res.Resources.Theme;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.DrawableRes; import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
@ -59,4 +60,8 @@ public class ImageSlide extends Slide {
public boolean hasImage() { public boolean hasImage() {
return true; return true;
} }
@NonNull @Override public String getContentDescription() {
return context.getString(R.string.Slide_image);
}
} }

View File

@ -63,6 +63,8 @@ public abstract class Slide {
return false; return false;
} }
public @NonNull String getContentDescription() { return ""; }
public PduPart getPart() { public PduPart getPart() {
return part; return part;
} }

View File

@ -20,6 +20,7 @@ import android.content.Context;
import android.content.res.Resources.Theme; import android.content.res.Resources.Theme;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.DrawableRes; import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ResUtil; import org.thoughtcrime.securesms.util.ResUtil;
@ -53,4 +54,8 @@ public class VideoSlide extends Slide {
public boolean hasVideo() { public boolean hasVideo() {
return true; return true;
} }
@NonNull @Override public String getContentDescription() {
return context.getString(R.string.Slide_video);
}
} }

View File

@ -26,6 +26,7 @@ import android.text.TextUtils.TruncateAt;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewStub; import android.view.ViewStub;
import android.view.animation.Animation;
import android.widget.TextView; import android.widget.TextView;
public class ViewUtil { public class ViewUtil {
@ -73,4 +74,26 @@ public class ViewUtil {
public static <T extends View> T findById(@NonNull View parent, @IdRes int resId) { public static <T extends View> T findById(@NonNull View parent, @IdRes int resId) {
return (T) parent.findViewById(resId); return (T) parent.findViewById(resId);
} }
public static void animateOut(final @NonNull View view, final @NonNull Animation animation) {
if (view.getVisibility() == View.GONE) return;
view.clearAnimation();
animation.setAnimationListener(new Animation.AnimationListener() {
@Override public void onAnimationStart(Animation animation) {}
@Override public void onAnimationRepeat(Animation animation) {}
@Override public void onAnimationEnd(Animation animation) {
view.setVisibility(View.GONE);
}
});
view.startAnimation(animation);
}
public static void animateIn(final @NonNull View view, final @NonNull Animation animation) {
if (view.getVisibility() == View.VISIBLE) return;
view.clearAnimation();
view.setVisibility(View.VISIBLE);
view.startAnimation(animation);
}
} }