Voice Note Locking.

Limit of 60 minutes, after which it's cancelled.
This commit is contained in:
Alan Evans 2019-03-28 15:04:38 -03:00 committed by GitHub
parent cab3657ab0
commit e842f78457
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 378 additions and 169 deletions

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M22.1,15l-9.5,-9.5c-0.3,-0.3 -0.8,-0.3 -1.1,0L1.9,15L3,16.1l9,-9l9,9L22.1,15z"/>
</vector>

9
res/drawable/ic_lock.xml Normal file
View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M18,9h-1V6c0,-2.8 -2.2,-5 -5,-5S7,3.2 7,6v3H6c-1.1,0 -2,0.9 -2,2v9c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-9C20,9.9 19.1,9 18,9zM9,6c0,-1.7 1.3,-3 3,-3s3,1.3 3,3v3H9V6zM13,16.2v2.3h-2v-2.3c-0.6,-0.4 -1,-1 -1,-1.7c0,-1.1 0.9,-2 2,-2s2,0.9 2,2C14,15.2 13.6,15.9 13,16.2z"/>
</vector>

View File

@ -124,31 +124,12 @@
<org.thoughtcrime.securesms.components.MicrophoneRecorderView
android:id="@+id/recorder_view"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_width="36dp"
android:layout_gravity="center_vertical"
android:clipChildren="false"
android:clipToPadding="false">
<ImageButton
android:id="@+id/quick_audio_toggle"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginEnd="2dp"
android:padding="6dp"
android:src="?quick_mic_icon"
android:background="@null"
android:contentDescription="@string/conversation_activity__quick_attachment_drawer_record_and_send_audio_description" />
<ImageView
android:id="@+id/quick_audio_fab"
android:layout_width="74dp"
android:layout_height="74dp"
android:src="@drawable/ic_mic_white_48dp"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/core_red"
android:visibility="gone"
android:scaleType="center"/>
<include layout="@layout/microphone_recorder_view" />
</org.thoughtcrime.securesms.components.MicrophoneRecorderView>
@ -177,51 +158,7 @@
</LinearLayout>
<LinearLayout
android:id="@+id/recording_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:visibility="gone">
<TextView
android:id="@+id/record_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="none"
android:layout_marginStart="20dp"
style="@style/Signal.Text.Body"
android:text="00:00"
android:textColor="@color/core_grey_60"
android:singleLine="true"
android:visibility="gone"
tools:visibility="visible"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="true">
<TextView
android:id="@+id/slide_to_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="left|start|center_vertical"
android:drawableStart="@drawable/ic_keyboard_arrow_left_grey600_24dp"
style="@style/Signal.Text.Caption"
android:text="@string/conversation_input_panel__slide_to_cancel"
android:textAllCaps="true"
android:textColor="@color/core_grey_60"
android:ellipsize="none"
android:visibility="gone"
android:paddingEnd="50dp"
tools:visibility="visible"/>
</FrameLayout>
</LinearLayout>
<include layout="@layout/recording_layout" />
</FrameLayout>

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="@layout/conversation_input_panel">
<ImageButton
android:id="@+id/quick_audio_toggle"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginEnd="2dp"
android:background="@null"
android:contentDescription="@string/conversation_activity__quick_attachment_drawer_record_and_send_audio_description"
android:padding="6dp"
android:src="?quick_mic_icon" />
<LinearLayout
android:id="@+id/lock_drop_target"
android:layout_width="40dp"
android:layout_height="64dp"
android:layout_gravity="center"
android:background="?attr/conversation_input_background"
android:clipChildren="false"
android:orientation="vertical"
android:visibility="gone"
tools:showIn="@layout/conversation_input_panel"
tools:translationY="@dimen/recording_voice_lock_target"
tools:visibility="visible">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:contentDescription="@string/conversation_activity__quick_attachment_drawer_lock_record_description"
android:tint="?attr/conversation_input_inline_attach_icon_tint"
app:srcCompat="@drawable/ic_lock" />
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="6dp"
android:contentDescription="@string/conversation_activity__quick_attachment_drawer_lock_record_description"
android:tint="?attr/conversation_input_inline_attach_icon_tint"
app:srcCompat="@drawable/ic_chevron_up" />
</LinearLayout>
<ImageView
android:id="@+id/quick_audio_fab"
android:layout_width="74dp"
android:layout_height="74dp"
android:layout_marginStart="-18dp"
android:layout_marginTop="-18dp"
android:background="@drawable/circle_tintable"
android:clickable="false"
android:scaleType="center"
android:src="@drawable/ic_mic_white_48dp"
android:visibility="gone"
tools:ignore="ContentDescription"
tools:visibility="visible" />
</merge>

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/recording_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:visibility="visible"
tools:showIn="@layout/conversation_input_panel">
<TextView
android:id="@+id/record_time"
style="@style/Signal.Text.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:ellipsize="none"
android:singleLine="true"
android:textColor="@color/core_grey_60"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="00:00"
tools:visibility="visible" />
<TextView
android:id="@+id/record_cancel"
style="@style/Signal.Text.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="none"
android:gravity="center_vertical"
android:text="@string/conversation_input_panel__cancel"
android:textAllCaps="true"
android:textColor="@color/red_A700"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/frameLayout"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<FrameLayout
android:id="@+id/frameLayout"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:clipChildren="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/record_time"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/slide_to_cancel"
style="@style/Signal.Text.Caption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableStart="@drawable/ic_keyboard_arrow_left_grey600_24dp"
android:ellipsize="none"
android:gravity="center_vertical"
android:text="@string/conversation_input_panel__slide_to_cancel"
android:textAllCaps="true"
android:textColor="@color/core_grey_60"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
</android.support.constraint.ConstraintLayout>

View File

@ -99,4 +99,6 @@
<dimen name="alertview_small_icon_size">14dp</dimen>
<dimen name="recording_voice_lock_target">-150dp</dimen>
</resources>

View File

@ -869,10 +869,12 @@
<string name="conversation_activity__attachment_thumbnail">Attachment Thumbnail</string>
<string name="conversation_activity__quick_attachment_drawer_toggle_camera_description">Toggle quick camera attachment drawer</string>
<string name="conversation_activity__quick_attachment_drawer_record_and_send_audio_description">Record and send audio attachment</string>
<string name="conversation_activity__quick_attachment_drawer_lock_record_description">Lock recording of audio attachment</string>
<string name="conversation_activity__enable_signal_for_sms">Enable Signal for SMS</string>
<!-- conversation_input_panel -->
<string name="conversation_input_panel__slide_to_cancel">SLIDE TO CANCEL</string>
<string name="conversation_input_panel__slide_to_cancel">Slide to cancel</string>
<string name="conversation_input_panel__cancel">Cancel</string>
<!-- conversation_item -->
<string name="conversation_item__mms_image_description">Media message</string>

View File

@ -5,6 +5,7 @@ import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.DimenRes;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat;
@ -38,7 +39,6 @@ import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
public class InputPanel extends LinearLayout
implements MicrophoneRecorderView.Listener,
@ -58,6 +58,7 @@ public class InputPanel extends LinearLayout
private View quickAudioToggle;
private View buttonToggle;
private View recordingContainer;
private View recordLockCancel;
private MicrophoneRecorderView microphoneRecorderView;
private SlideToCancel slideToCancel;
@ -93,10 +94,15 @@ public class InputPanel extends LinearLayout
this.quickAudioToggle = findViewById(R.id.quick_audio_toggle);
this.buttonToggle = findViewById(R.id.button_toggle);
this.recordingContainer = findViewById(R.id.recording_container);
this.recordTime = new RecordTime(findViewById(R.id.record_time));
this.recordLockCancel = findViewById(R.id.record_cancel);
this.slideToCancel = new SlideToCancel(findViewById(R.id.slide_to_cancel));
this.microphoneRecorderView = findViewById(R.id.recorder_view);
this.microphoneRecorderView.setListener(this);
this.recordTime = new RecordTime(findViewById(R.id.record_time),
TimeUnit.HOURS.toSeconds(1),
() -> microphoneRecorderView.cancelAction());
this.recordLockCancel.setOnClickListener(v -> microphoneRecorderView.cancelAction());
if (TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
emojiToggle.setVisibility(View.GONE);
@ -181,21 +187,21 @@ public class InputPanel extends LinearLayout
}
@Override
public void onRecordPressed(float startPositionX) {
public void onRecordPressed() {
if (listener != null) listener.onRecorderStarted();
recordTime.display();
slideToCancel.display(startPositionX);
slideToCancel.display();
if (emojiVisible) ViewUtil.fadeOut(emojiToggle, FADE_TIME, View.INVISIBLE);
ViewUtil.fadeOut(composeText, FADE_TIME, View.INVISIBLE);
ViewUtil.fadeOut(quickCameraToggle, FADE_TIME, View.INVISIBLE);
ViewUtil.fadeOut(quickAudioToggle, FADE_TIME, View.INVISIBLE);
ViewUtil.fadeOut(buttonToggle, FADE_TIME, View.INVISIBLE);
buttonToggle.animate().alpha(0).setDuration(FADE_TIME).start();
}
@Override
public void onRecordReleased(float x) {
long elapsedTime = onRecordHideEvent(x);
public void onRecordReleased() {
long elapsedTime = onRecordHideEvent();
if (listener != null) {
Log.d(TAG, "Elapsed time: " + elapsedTime);
@ -209,8 +215,8 @@ public class InputPanel extends LinearLayout
}
@Override
public void onRecordMoved(float x, float absoluteX) {
slideToCancel.moveTo(x);
public void onRecordMoved(float offsetX, float absoluteX) {
slideToCancel.moveTo(offsetX);
int direction = ViewCompat.getLayoutDirection(this);
float position = absoluteX / recordingContainer.getWidth();
@ -223,11 +229,19 @@ public class InputPanel extends LinearLayout
}
@Override
public void onRecordCanceled(float x) {
onRecordHideEvent(x);
public void onRecordCanceled() {
onRecordHideEvent();
if (listener != null) listener.onRecorderCanceled();
}
@Override
public void onRecordLocked() {
slideToCancel.hide();
recordLockCancel.setVisibility(View.VISIBLE);
buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start();
if (listener != null) listener.onRecorderLocked();
}
public void onPause() {
this.microphoneRecorderView.cancelAction();
}
@ -239,8 +253,10 @@ public class InputPanel extends LinearLayout
quickCameraToggle.setEnabled(enabled);
}
private long onRecordHideEvent(float x) {
ListenableFuture<Void> future = slideToCancel.hide(x);
private long onRecordHideEvent() {
recordLockCancel.setVisibility(View.GONE);
ListenableFuture<Void> future = slideToCancel.hide();
long elapsedTime = recordTime.hide();
future.addListener(new AssertedSuccessListener<Void>() {
@ -250,7 +266,7 @@ public class InputPanel extends LinearLayout
ViewUtil.fadeIn(composeText, FADE_TIME);
ViewUtil.fadeIn(quickCameraToggle, FADE_TIME);
ViewUtil.fadeIn(quickAudioToggle, FADE_TIME);
ViewUtil.fadeIn(buttonToggle, FADE_TIME);
buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start();
}
});
@ -276,9 +292,17 @@ public class InputPanel extends LinearLayout
return getResources().getDimensionPixelSize(dimenRes);
}
public boolean isRecordingInLockedMode() {
return microphoneRecorderView.isRecordingLocked();
}
public void releaseRecordingLock() {
microphoneRecorderView.unlockAction();
}
public interface Listener {
void onRecorderStarted();
void onRecorderLocked();
void onRecorderFinished();
void onRecorderCanceled();
void onRecorderPermissionRequired();
@ -290,23 +314,19 @@ public class InputPanel extends LinearLayout
private final View slideToCancelView;
private float startPositionX;
public SlideToCancel(View slideToCancelView) {
SlideToCancel(View slideToCancelView) {
this.slideToCancelView = slideToCancelView;
}
public void display(float startPositionX) {
this.startPositionX = startPositionX;
public void display() {
ViewUtil.fadeIn(this.slideToCancelView, FADE_TIME);
}
public ListenableFuture<Void> hide(float x) {
public ListenableFuture<Void> hide() {
final SettableFuture<Void> future = new SettableFuture<>();
float offset = getOffset(x);
AnimationSet animation = new AnimationSet(true);
animation.addAnimation(new TranslateAnimation(Animation.ABSOLUTE, offset,
animation.addAnimation(new TranslateAnimation(Animation.ABSOLUTE, slideToCancelView.getTranslationX(),
Animation.ABSOLUTE, 0,
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, 0));
@ -323,8 +343,7 @@ public class InputPanel extends LinearLayout
return future;
}
public void moveTo(float x) {
float offset = getOffset(x);
void moveTo(float offset) {
Animation animation = new TranslateAnimation(Animation.ABSOLUTE, offset,
Animation.ABSOLUTE, offset,
Animation.RELATIVE_TO_SELF, 0,
@ -336,49 +355,55 @@ public class InputPanel extends LinearLayout
slideToCancelView.startAnimation(animation);
}
private float getOffset(float x) {
return ViewCompat.getLayoutDirection(slideToCancelView) == ViewCompat.LAYOUT_DIRECTION_LTR ?
-Math.max(0, this.startPositionX - x) : Math.max(0, x - this.startPositionX);
}
}
private static class RecordTime implements Runnable {
private final TextView recordTimeView;
private final AtomicLong startTime = new AtomicLong(0);
private final long limitSeconds;
private final Runnable onLimitHit;
private long startTime;
private RecordTime(TextView recordTimeView) {
private RecordTime(TextView recordTimeView, long limitSeconds, Runnable onLimitHit) {
this.recordTimeView = recordTimeView;
this.limitSeconds = limitSeconds;
this.onLimitHit = onLimitHit;
}
@MainThread
public void display() {
this.startTime.set(System.currentTimeMillis());
this.startTime = System.currentTimeMillis();
this.recordTimeView.setText(DateUtils.formatElapsedTime(0));
ViewUtil.fadeIn(this.recordTimeView, FADE_TIME);
Util.runOnMainDelayed(this, TimeUnit.SECONDS.toMillis(1));
}
@MainThread
public long hide() {
long elapsedtime = System.currentTimeMillis() - startTime.get();
this.startTime.set(0);
long elapsedTime = System.currentTimeMillis() - startTime;
this.startTime = 0;
ViewUtil.fadeOut(this.recordTimeView, FADE_TIME, View.INVISIBLE);
return elapsedtime;
return elapsedTime;
}
@Override
@MainThread
public void run() {
long localStartTime = startTime.get();
long localStartTime = startTime;
if (localStartTime > 0) {
long elapsedTime = System.currentTimeMillis() - localStartTime;
recordTimeView.setText(DateUtils.formatElapsedTime(TimeUnit.MILLISECONDS.toSeconds(elapsedTime)));
Util.runOnMainDelayed(this, TimeUnit.SECONDS.toMillis(1));
long elapsedSeconds = TimeUnit.MILLISECONDS.toSeconds(elapsedTime);
if (elapsedSeconds >= limitSeconds) {
onLimitHit.run();
} else {
recordTimeView.setText(DateUtils.formatElapsedTime(elapsedSeconds));
Util.runOnMainDelayed(this, TimeUnit.SECONDS.toMillis(1));
}
}
}
}
public interface MediaListener {
public void onMediaSelected(@NonNull Uri uri, String contentType);
void onMediaSelected(@NonNull Uri uri, String contentType);
}
}

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components;
import android.Manifest;
import android.content.Context;
import android.graphics.PorterDuff;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
@ -12,6 +13,7 @@ import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.AnticipateOvershootInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;
import android.view.animation.OvershootInterpolator;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
@ -22,13 +24,20 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.ViewUtil;
public class MicrophoneRecorderView extends FrameLayout implements View.OnTouchListener {
public final class MicrophoneRecorderView extends FrameLayout implements View.OnTouchListener {
enum State {
NOT_RUNNING,
RUNNING_HELD,
RUNNING_LOCKED
}
public static final int ANIMATION_DURATION = 200;
private FloatingRecordButton floatingRecordButton;
private LockDropTarget lockDropTarget;
private @Nullable Listener listener;
private boolean actionInProgress;
private @NonNull State state = State.NOT_RUNNING;
public MicrophoneRecorderView(Context context) {
super(context);
@ -42,22 +51,49 @@ public class MicrophoneRecorderView extends FrameLayout implements View.OnTouchL
public void onFinishInflate() {
super.onFinishInflate();
ImageView recordButtonFab = ViewUtil.findById(this, R.id.quick_audio_fab);
this.floatingRecordButton = new FloatingRecordButton(getContext(), recordButtonFab);
floatingRecordButton = new FloatingRecordButton(getContext(), findViewById(R.id.quick_audio_fab));
lockDropTarget = new LockDropTarget (getContext(), findViewById(R.id.lock_drop_target));
View recordButton = ViewUtil.findById(this, R.id.quick_audio_toggle);
recordButton.setOnTouchListener(this);
}
public void cancelAction() {
if (this.actionInProgress) {
this.actionInProgress = false;
this.floatingRecordButton.hide(this.floatingRecordButton.lastPositionX);
if (state != State.NOT_RUNNING) {
state = State.NOT_RUNNING;
hideUi();
if (listener != null) listener.onRecordCanceled(this.floatingRecordButton.lastPositionX);
if (listener != null) listener.onRecordCanceled();
}
}
public boolean isRecordingLocked() {
return state == State.RUNNING_LOCKED;
}
private void lockAction() {
if (state == State.RUNNING_HELD) {
state = State.RUNNING_LOCKED;
hideUi();
if (listener != null) listener.onRecordLocked();
}
}
public void unlockAction() {
if (state == State.RUNNING_LOCKED) {
state = State.NOT_RUNNING;
hideUi();
if (listener != null) listener.onRecordReleased();
}
}
private void hideUi() {
floatingRecordButton.hide();
lockDropTarget.hide();
}
@Override
public boolean onTouch(View v, final MotionEvent event) {
switch (event.getAction()) {
@ -65,23 +101,29 @@ public class MicrophoneRecorderView extends FrameLayout implements View.OnTouchL
if (!Permissions.hasAll(getContext(), Manifest.permission.RECORD_AUDIO)) {
if (listener != null) listener.onRecordPermissionRequired();
} else {
this.actionInProgress = true;
this.floatingRecordButton.display(event.getX());
if (listener != null) listener.onRecordPressed(event.getX());
state = State.RUNNING_HELD;
floatingRecordButton.display(event.getX(), event.getY());
lockDropTarget.display();
if (listener != null) listener.onRecordPressed();
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (this.actionInProgress) {
this.actionInProgress = false;
this.floatingRecordButton.hide(event.getX());
if (listener != null) listener.onRecordReleased(event.getX());
if (this.state == State.RUNNING_HELD) {
state = State.NOT_RUNNING;
hideUi();
if (listener != null) listener.onRecordReleased();
}
break;
case MotionEvent.ACTION_MOVE:
if (this.actionInProgress) {
this.floatingRecordButton.moveTo(event.getX());
if (listener != null) listener.onRecordMoved(event.getX(), event.getRawX());
if (this.state == State.RUNNING_HELD) {
this.floatingRecordButton.moveTo(event.getX(), event.getY());
if (listener != null) listener.onRecordMoved(floatingRecordButton.lastOffsetX, event.getRawX());
int dimensionPixelSize = getResources().getDimensionPixelSize(R.dimen.recording_voice_lock_target);
if (floatingRecordButton.lastOffsetY <= dimensionPixelSize) {
lockAction();
}
}
break;
}
@ -94,10 +136,11 @@ public class MicrophoneRecorderView extends FrameLayout implements View.OnTouchL
}
public interface Listener {
void onRecordPressed(float x);
void onRecordReleased(float x);
void onRecordCanceled(float x);
void onRecordMoved(float x, float absoluteX);
void onRecordPressed();
void onRecordReleased();
void onRecordCanceled();
void onRecordLocked();
void onRecordMoved(float offsetX, float absoluteX);
void onRecordPermissionRequired();
}
@ -106,81 +149,73 @@ public class MicrophoneRecorderView extends FrameLayout implements View.OnTouchL
private final ImageView recordButtonFab;
private float startPositionX;
private float lastPositionX;
private float startPositionY;
private float lastOffsetX;
private float lastOffsetY;
public FloatingRecordButton(Context context, ImageView recordButtonFab) {
FloatingRecordButton(Context context, ImageView recordButtonFab) {
this.recordButtonFab = recordButtonFab;
this.recordButtonFab.getBackground().setColorFilter(context.getResources()
.getColor(R.color.red_500),
PorterDuff.Mode.SRC_IN);
}
public void display(float x) {
void display(float x, float y) {
this.startPositionX = x;
this.lastPositionX = x;
this.startPositionY = y;
recordButtonFab.setVisibility(View.VISIBLE);
float translation = ViewCompat.getLayoutDirection(recordButtonFab) ==
ViewCompat.LAYOUT_DIRECTION_LTR ? -.25f : .25f;
AnimationSet animation = new AnimationSet(true);
animation.addAnimation(new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, -.25f,
Animation.RELATIVE_TO_SELF, -.25f));
animation.addAnimation(new TranslateAnimation(Animation.ABSOLUTE, 0,
Animation.ABSOLUTE, 0,
Animation.ABSOLUTE, 0,
Animation.ABSOLUTE, 0));
animation.addAnimation(new ScaleAnimation(.5f, 1f, .5f, 1f,
Animation.RELATIVE_TO_SELF, .5f,
Animation.RELATIVE_TO_SELF, .5f));
animation.setFillBefore(true);
animation.setFillAfter(true);
animation.setDuration(ANIMATION_DURATION);
animation.setInterpolator(new OvershootInterpolator());
recordButtonFab.startAnimation(animation);
}
public void moveTo(float x) {
this.lastPositionX = x;
void moveTo(float x, float y) {
lastOffsetX = getXOffset(x);
lastOffsetY = getYOffset(y);
float offset = getOffset(x);
if (Math.abs(lastOffsetX) > Math.abs(lastOffsetY)) {
lastOffsetY = 0;
} else {
lastOffsetX = 0;
}
Animation translateAnimation = new TranslateAnimation(Animation.ABSOLUTE, offset,
Animation.ABSOLUTE, offset,
Animation.RELATIVE_TO_SELF, -.25f,
Animation.RELATIVE_TO_SELF, -.25f);
translateAnimation.setDuration(0);
translateAnimation.setFillAfter(true);
translateAnimation.setFillBefore(true);
recordButtonFab.startAnimation(translateAnimation);
recordButtonFab.setTranslationX(lastOffsetX);
recordButtonFab.setTranslationY(lastOffsetY);
}
public void hide(float x) {
this.lastPositionX = x;
float offset = getOffset(x);
void hide() {
recordButtonFab.setTranslationX(0);
recordButtonFab.setTranslationY(0);
if (recordButtonFab.getVisibility() != VISIBLE) return;
AnimationSet animation = new AnimationSet(false);
Animation scaleAnimation = new ScaleAnimation(1, 0.5f, 1, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
Animation translateAnimation = new TranslateAnimation(Animation.ABSOLUTE, offset,
Animation translateAnimation = new TranslateAnimation(Animation.ABSOLUTE, lastOffsetX,
Animation.ABSOLUTE, 0,
Animation.RELATIVE_TO_SELF, -.25f,
Animation.RELATIVE_TO_SELF, -.25f);
Animation.ABSOLUTE, lastOffsetY,
Animation.ABSOLUTE, 0);
scaleAnimation.setInterpolator(new AnticipateOvershootInterpolator(1.5f));
translateAnimation.setInterpolator(new DecelerateInterpolator());
animation.addAnimation(scaleAnimation);
animation.addAnimation(translateAnimation);
animation.setDuration(ANIMATION_DURATION);
animation.setFillBefore(true);
animation.setFillAfter(false);
animation.setInterpolator(new AnticipateOvershootInterpolator(1.5f));
recordButtonFab.setVisibility(View.GONE);
@ -188,16 +223,48 @@ public class MicrophoneRecorderView extends FrameLayout implements View.OnTouchL
recordButtonFab.startAnimation(animation);
}
private float getOffset(float x) {
private float getXOffset(float x) {
return ViewCompat.getLayoutDirection(recordButtonFab) == ViewCompat.LAYOUT_DIRECTION_LTR ?
-Math.max(0, this.startPositionX - x) : Math.max(0, x - this.startPositionX);
}
private int getWidthAdjustment() {
int width = recordButtonFab.getWidth() / 4;
return ViewCompat.getLayoutDirection(recordButtonFab) == ViewCompat.LAYOUT_DIRECTION_LTR ? -width : width;
private float getYOffset(float y) {
return Math.min(0, y - this.startPositionY);
}
}
private static class LockDropTarget {
private final View lockDropTarget;
private final int dropTargetPosition;
LockDropTarget(Context context, View lockDropTarget) {
this.lockDropTarget = lockDropTarget;
this.dropTargetPosition = context.getResources().getDimensionPixelSize(R.dimen.recording_voice_lock_target);
}
void display() {
lockDropTarget.setScaleX(1);
lockDropTarget.setScaleY(1);
lockDropTarget.setAlpha(0);
lockDropTarget.setTranslationY(0);
lockDropTarget.setVisibility(VISIBLE);
lockDropTarget.animate()
.setStartDelay(ANIMATION_DURATION * 2)
.setDuration(ANIMATION_DURATION)
.setInterpolator(new DecelerateInterpolator())
.translationY(dropTargetPosition)
.alpha(1)
.start();
}
void hide() {
lockDropTarget.animate()
.setStartDelay(0)
.setDuration(ANIMATION_DURATION)
.setInterpolator(new LinearInterpolator())
.scaleX(0).scaleY(0)
.start();
}
}
}

View File

@ -45,6 +45,8 @@ public class SendButton extends ImageButton
}
private TransportOptions initializeTransportOptions(boolean media) {
if (isInEditMode()) return null;
TransportOptions transportOptions = new TransportOptions(getContext(), media);
transportOptions.addOnTransportChangedListener(this);

View File

@ -1995,6 +1995,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void sendMessage() {
if (inputPanel.isRecordingInLockedMode()) {
inputPanel.releaseRecordingLock();
return;
}
try {
Recipient recipient = getRecipient();
@ -2176,6 +2181,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void updateToggleButtonState() {
if (inputPanel.isRecordingInLockedMode()) {
buttonToggle.display(sendButton);
quickAttachmentToggle.show();
inlineAttachmentToggle.hide();
return;
}
if (composeText.getText().length() == 0 && !attachmentManager.isAttachmentPresent()) {
buttonToggle.display(attachButton);
quickAttachmentToggle.show();
@ -2232,8 +2244,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
audioRecorder.startRecording();
}
@Override
public void onRecorderLocked() {
updateToggleButtonState();
}
@Override
public void onRecorderFinished() {
updateToggleButtonState();
Vibrator vibrator = ServiceUtil.getVibrator(this);
vibrator.vibrate(20);
@ -2274,6 +2292,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
public void onRecorderCanceled() {
updateToggleButtonState();
Vibrator vibrator = ServiceUtil.getVibrator(this);
vibrator.vibrate(50);