session-android/src/org/thoughtcrime/securesms/components/camera/QuickAttachmentDrawer.java

577 lines
19 KiB
Java

package org.thoughtcrime.securesms.components.camera;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.hardware.Camera;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.support.annotation.NonNull;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import com.nineoldandroids.animation.ObjectAnimator;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.InputAwareLayout.InputView;
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout;
import org.thoughtcrime.securesms.components.camera.CameraView.CameraViewListener;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
public class QuickAttachmentDrawer extends ViewGroup implements InputView, CameraViewListener {
private static final String TAG = QuickAttachmentDrawer.class.getSimpleName();
private final ViewDragHelper dragHelper;
private CameraView cameraView;
private int coverViewPosition;
private KeyboardAwareLinearLayout container;
private View coverView;
private View controls;
private ImageButton fullScreenButton;
private ImageButton swapCameraButton;
private ImageButton shutterButton;
private int slideOffset;
private float initialMotionX;
private float initialMotionY;
private int rotation;
private AttachmentDrawerListener listener;
private int halfExpandedHeight;
private ObjectAnimator animator;
private DrawerState drawerState = DrawerState.COLLAPSED;
private Rect drawChildrenRect = new Rect();
private boolean paused = false;
public QuickAttachmentDrawer(Context context) {
this(context, null);
}
public QuickAttachmentDrawer(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public QuickAttachmentDrawer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
dragHelper = ViewDragHelper.create(this, 1.f, new ViewDragHelperCallback());
initializeView();
updateHalfExpandedAnchorPoint();
onConfigurationChanged();
}
private void initializeView() {
inflate(getContext(), R.layout.quick_attachment_drawer, this);
cameraView = ViewUtil.findById(this, R.id.quick_camera);
updateControlsView();
coverViewPosition = getChildCount();
controls.setVisibility(GONE);
cameraView.setVisibility(GONE);
cameraView.addListener(this);
}
public static boolean isDeviceSupported(Context context) {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA) &&
Camera.getNumberOfCameras() > 0;
}
@Override
public boolean isShowing() {
return drawerState.isVisible();
}
@Override
public void hide(boolean immediate) {
setDrawerStateAndUpdate(DrawerState.COLLAPSED, immediate);
}
@Override
public void show(int height, boolean immediate) {
setDrawerStateAndUpdate(DrawerState.HALF_EXPANDED, immediate);
}
public void onConfigurationChanged() {
int rotation = ServiceUtil.getWindowManager(getContext()).getDefaultDisplay().getRotation();
final boolean rotationChanged = this.rotation != rotation;
this.rotation = rotation;
if (rotationChanged) {
if (isShowing()) {
cameraView.onPause();
}
updateControlsView();
setDrawerStateAndUpdate(drawerState, true);
}
}
private void updateControlsView() {
Log.i(TAG, "updateControlsView()");
View controls = LayoutInflater.from(getContext()).inflate(isLandscape() ? R.layout.quick_camera_controls_land
: R.layout.quick_camera_controls,
this, false);
shutterButton = (ImageButton) controls.findViewById(R.id.shutter_button);
swapCameraButton = (ImageButton) controls.findViewById(R.id.swap_camera_button);
fullScreenButton = (ImageButton) controls.findViewById(R.id.fullscreen_button);
if (cameraView.isMultiCamera()) {
swapCameraButton.setVisibility(View.VISIBLE);
swapCameraButton.setImageResource(cameraView.isRearCamera() ? R.drawable.quick_camera_front
: R.drawable.quick_camera_rear);
swapCameraButton.setOnClickListener(new CameraFlipClickListener());
}
shutterButton.setOnClickListener(new ShutterClickListener());
fullScreenButton.setOnClickListener(new FullscreenClickListener());
ViewUtil.swapChildInPlace(this, this.controls, controls, indexOfChild(cameraView) + 1);
this.controls = controls;
}
private boolean isLandscape() {
return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
}
private View getCoverView() {
if (coverView == null) coverView = getChildAt(coverViewPosition);
return coverView;
}
private KeyboardAwareLinearLayout getContainer() {
if (container == null) container = (KeyboardAwareLinearLayout)getParent();
return container;
}
private void updateHalfExpandedAnchorPoint() {
if (getContainer() != null) {
halfExpandedHeight = getContainer().getKeyboardHeight();
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed && drawerState == DrawerState.FULL_EXPANDED) {
setSlideOffset(getMeasuredHeight());
}
final int paddingLeft = getPaddingLeft();
final int paddingTop = getPaddingTop();
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
final int childHeight = child.getMeasuredHeight();
int childTop = paddingTop;
int childLeft = paddingLeft;
int childBottom;
if (child == cameraView) {
childTop = computeCameraTopPosition(slideOffset);
childBottom = childTop + childHeight;
if (cameraView.getMeasuredWidth() < getMeasuredWidth())
childLeft = (getMeasuredWidth() - cameraView.getMeasuredWidth()) / 2 + paddingLeft;
} else if (child == controls) {
childBottom = getMeasuredHeight();
} else {
childTop = computeCoverTopPosition(slideOffset);
childBottom = childTop + childHeight;
}
final int childRight = childLeft + child.getMeasuredWidth();
child.layout(childLeft, childTop, childRight, childBottom);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("Width must have an exact value or MATCH_PARENT");
} else if (heightMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("Height must have an exact value or MATCH_PARENT");
}
int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
final LayoutParams lp = child.getLayoutParams();
if (child.getVisibility() == GONE && i == 0) {
continue;
}
int childWidthSpec;
switch (lp.width) {
case LayoutParams.WRAP_CONTENT:
childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
break;
case LayoutParams.MATCH_PARENT:
childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
break;
default:
childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
break;
}
int childHeightSpec;
switch (lp.height) {
case LayoutParams.WRAP_CONTENT:
childHeightSpec = MeasureSpec.makeMeasureSpec(layoutHeight, MeasureSpec.AT_MOST);
break;
case LayoutParams.MATCH_PARENT:
childHeightSpec = MeasureSpec.makeMeasureSpec(layoutHeight, MeasureSpec.EXACTLY);
break;
default:
childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
break;
}
child.measure(childWidthSpec, childHeightSpec);
}
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (h != oldh) updateHalfExpandedAnchorPoint();
}
@Override
protected boolean drawChild(@NonNull Canvas canvas, @NonNull View child, long drawingTime) {
boolean result;
final int save = canvas.save(Canvas.CLIP_SAVE_FLAG);
canvas.getClipBounds(drawChildrenRect);
if (child == coverView) {
drawChildrenRect.bottom = Math.min(drawChildrenRect.bottom, child.getBottom());
} else if (coverView != null) {
drawChildrenRect.top = Math.max(drawChildrenRect.top, coverView.getBottom());
}
canvas.clipRect(drawChildrenRect);
result = super.drawChild(canvas, child, drawingTime);
canvas.restoreToCount(save);
return result;
}
@Override
public void computeScroll() {
if (dragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
if (slideOffset == 0 && cameraView.isStarted()) {
cameraView.onPause();
controls.setVisibility(GONE);
cameraView.setVisibility(GONE);
} else if (slideOffset != 0 && !cameraView.isStarted() & !paused) {
controls.setVisibility(VISIBLE);
cameraView.setVisibility(VISIBLE);
cameraView.onResume();
}
}
private void setDrawerState(DrawerState drawerState) {
switch (drawerState) {
case COLLAPSED:
fullScreenButton.setImageResource(R.drawable.quick_camera_fullscreen);
break;
case HALF_EXPANDED:
if (isLandscape()) {
setDrawerState(DrawerState.FULL_EXPANDED);
return;
}
fullScreenButton.setImageResource(R.drawable.quick_camera_fullscreen);
break;
case FULL_EXPANDED:
fullScreenButton.setImageResource(isLandscape() ? R.drawable.quick_camera_hide
: R.drawable.quick_camera_exit_fullscreen);
break;
}
if (listener != null && drawerState != this.drawerState) {
this.drawerState = drawerState;
listener.onAttachmentDrawerStateChanged(drawerState);
}
}
@SuppressWarnings("unused")
public void setSlideOffset(int slideOffset) {
this.slideOffset = slideOffset;
requestLayout();
}
public int getTargetSlideOffset() {
switch (drawerState) {
case FULL_EXPANDED: return getMeasuredHeight();
case HALF_EXPANDED: return halfExpandedHeight;
default: return 0;
}
}
public void setDrawerStateAndUpdate(final DrawerState requestedDrawerState) {
setDrawerStateAndUpdate(requestedDrawerState, false);
}
public void setDrawerStateAndUpdate(final DrawerState requestedDrawerState, boolean instant) {
DrawerState oldDrawerState = this.drawerState;
setDrawerState(requestedDrawerState);
if (oldDrawerState != drawerState) {
updateHalfExpandedAnchorPoint();
slideTo(getTargetSlideOffset(), instant);
}
}
public void setListener(AttachmentDrawerListener listener) {
this.listener = listener;
cameraView.addListener(listener);
}
@Override
public void onImageCapture(@NonNull byte[] imageBytes) {}
@Override
public void onCameraFail() {
swapCameraButton.setEnabled(false);
shutterButton.setEnabled(false);
}
@Override
public void onCameraStart() {
swapCameraButton.setEnabled(true);
shutterButton.setEnabled(true);
}
@Override
public void onCameraStop() {
swapCameraButton.setEnabled(false);
shutterButton.setEnabled(false);
}
public interface AttachmentDrawerListener extends CameraViewListener {
void onAttachmentDrawerStateChanged(DrawerState drawerState);
}
private class ViewDragHelperCallback extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == controls;
}
@Override
public void onViewDragStateChanged(int state) {
if (dragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) {
setDrawerState(drawerState);
slideOffset = getTargetSlideOffset();
requestLayout();
}
}
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
slideOffset = Util.clamp(slideOffset - dy, 0, getMeasuredHeight());
requestLayout();
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if (releasedChild == controls) {
float direction = -yvel;
DrawerState drawerState = DrawerState.COLLAPSED;
if (direction > 1) {
drawerState = DrawerState.FULL_EXPANDED;
} else if (direction < -1) {
boolean halfExpand = (slideOffset > halfExpandedHeight && !isLandscape());
drawerState = halfExpand ? DrawerState.HALF_EXPANDED : DrawerState.COLLAPSED;
} else if (!isLandscape()) {
if (slideOffset >= (halfExpandedHeight + getMeasuredHeight()) / 2) {
drawerState = DrawerState.FULL_EXPANDED;
} else if (slideOffset >= halfExpandedHeight / 2) {
drawerState = DrawerState.HALF_EXPANDED;
}
}
setDrawerState(drawerState);
int slideOffset = getTargetSlideOffset();
dragHelper.captureChildView(coverView, 0);
dragHelper.settleCapturedViewAt(coverView.getLeft(), computeCoverTopPosition(slideOffset));
dragHelper.captureChildView(cameraView, 0);
dragHelper.settleCapturedViewAt(cameraView.getLeft(), computeCameraTopPosition(slideOffset));
ViewCompat.postInvalidateOnAnimation(QuickAttachmentDrawer.this);
}
}
@Override
public int getViewVerticalDragRange(View child) {
return getMeasuredHeight();
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
final int action = MotionEventCompat.getActionMasked(event);
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
dragHelper.cancel();
return false;
}
final float x = event.getX();
final float y = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
initialMotionX = x;
initialMotionY = y;
break;
case MotionEvent.ACTION_MOVE:
final float adx = Math.abs(x - initialMotionX);
final float ady = Math.abs(y - initialMotionY);
final int dragSlop = dragHelper.getTouchSlop();
if (adx > dragSlop && ady < dragSlop) {
return super.onInterceptTouchEvent(event);
}
if ((ady > dragSlop && adx > ady) || !isDragViewUnder((int) initialMotionX, (int) initialMotionY)) {
dragHelper.cancel();
return false;
}
break;
}
return dragHelper.shouldInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
dragHelper.processTouchEvent(event);
return true;
}
// NOTE: Android Studio bug misreports error, squashing the warning.
// https://code.google.com/p/android/issues/detail?id=175977
@SuppressWarnings("ResourceType")
private boolean isDragViewUnder(int x, int y) {
int[] viewLocation = new int[2];
cameraView.getLocationOnScreen(viewLocation);
int[] parentLocation = new int[2];
this.getLocationOnScreen(parentLocation);
int screenX = parentLocation[0] + x;
int screenY = parentLocation[1] + y;
return screenX >= viewLocation[0] && screenX < viewLocation[0] + cameraView.getWidth() &&
screenY >= viewLocation[1] && screenY < viewLocation[1] + cameraView.getHeight();
}
private int computeCameraTopPosition(int slideOffset) {
if (VERSION.SDK_INT < VERSION_CODES.ICE_CREAM_SANDWICH) {
return getPaddingTop();
}
final int baseCameraTop = (cameraView.getMeasuredHeight() - halfExpandedHeight) / 2;
final int baseOffset = getMeasuredHeight() - slideOffset - baseCameraTop;
final float slop = Util.clamp((float)(slideOffset - halfExpandedHeight) / (getMeasuredHeight() - halfExpandedHeight),
0f,
1f);
return baseOffset + (int)(slop * baseCameraTop);
}
private int computeCoverTopPosition(int slideOffset) {
return getMeasuredHeight() - getPaddingBottom() - slideOffset - getCoverView().getMeasuredHeight();
}
private void slideTo(int slideOffset, boolean forceInstant) {
if (animator != null) {
animator.cancel();
animator = null;
}
if (!forceInstant) {
animator = ObjectAnimator.ofInt(this, "slideOffset", this.slideOffset, slideOffset);
animator.setInterpolator(new FastOutSlowInInterpolator());
animator.setDuration(400);
animator.start();
ViewCompat.postInvalidateOnAnimation(this);
} else {
this.slideOffset = slideOffset;
requestLayout();
invalidate();
}
}
public void onPause() {
paused = true;
cameraView.onPause();
}
public void onResume() {
paused = false;
if (drawerState.isVisible()) cameraView.onResume();
}
public enum DrawerState {
COLLAPSED, HALF_EXPANDED, FULL_EXPANDED;
public boolean isVisible() {
return this != COLLAPSED;
}
}
private class ShutterClickListener implements OnClickListener {
@Override
public void onClick(View v) {
boolean crop = drawerState != DrawerState.FULL_EXPANDED;
int imageHeight = crop ? getContainer().getKeyboardHeight() : cameraView.getMeasuredHeight();
Rect previewRect = new Rect(0, 0, cameraView.getMeasuredWidth(), imageHeight);
cameraView.takePicture(previewRect);
}
}
private class CameraFlipClickListener implements OnClickListener {
@Override
public void onClick(View v) {
cameraView.flipCamera();
swapCameraButton.setImageResource(cameraView.isRearCamera() ? R.drawable.quick_camera_front
: R.drawable.quick_camera_rear);
}
}
private class FullscreenClickListener implements OnClickListener {
@Override
public void onClick(View v) {
if (drawerState != DrawerState.FULL_EXPANDED) {
setDrawerStateAndUpdate(DrawerState.FULL_EXPANDED);
} else if (isLandscape()) {
setDrawerStateAndUpdate(DrawerState.COLLAPSED);
} else {
setDrawerStateAndUpdate(DrawerState.HALF_EXPANDED);
}
}
}
}