/* * This is the source code of Telegram for Android v. 5.x.x * It is licensed under GNU GPL v. 2 or later. * You should have received a copy of the license in this archive (see LICENSE). * * Copyright Nikolai Kudashov, 2013-2018. */ package org.telegram.ui.Components; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; import android.os.Build; import android.view.MotionEvent; import android.widget.FrameLayout; import org.telegram.messenger.AndroidUtilities; import org.telegram.ui.BubbleActivity; public class PhotoFilterBlurControl extends FrameLayout { public interface PhotoFilterLinearBlurControlDelegate { void valueChanged(Point centerPoint, float falloff, float size, float angle); } private final static float BlurInsetProximity = AndroidUtilities.dp(20); private final static float BlurMinimumFalloff = 0.1f; private final static float BlurMinimumDifference = 0.02f; private final static float BlurViewCenterInset = AndroidUtilities.dp(30.0f); private final static float BlurViewRadiusInset = AndroidUtilities.dp(30.0f); private enum BlurViewActiveControl { BlurViewActiveControlNone, BlurViewActiveControlCenter, BlurViewActiveControlInnerRadius, BlurViewActiveControlOuterRadius, BlurViewActiveControlWholeArea, BlurViewActiveControlRotation } private final int GestureStateBegan = 1; private final int GestureStateChanged = 2; private final int GestureStateEnded = 3; private final int GestureStateCancelled = 4; private final int GestureStateFailed = 5; private BlurViewActiveControl activeControl; private Point startCenterPoint = new Point(); private float startDistance; private float startRadius; private Size actualAreaSize = new Size(); private Point centerPoint = new Point(0.5f, 0.5f); private float falloff = 0.15f; private float size = 0.35f; private float angle; private RectF arcRect = new RectF(); private float pointerStartX; private float pointerStartY; private float startPointerDistance; private float pointerScale = 1; private boolean isMoving; private boolean isZooming; private boolean checkForMoving = true; private boolean checkForZooming; private int type; private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private Paint arcPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private boolean inBubbleMode; private PhotoFilterLinearBlurControlDelegate delegate; public PhotoFilterBlurControl(Context context) { super(context); setWillNotDraw(false); paint.setColor(0xffffffff); arcPaint.setColor(0xffffffff); arcPaint.setStrokeWidth(AndroidUtilities.dp(2)); arcPaint.setStyle(Paint.Style.STROKE); inBubbleMode = context instanceof BubbleActivity; } public void setType(int blurType) { type = blurType; invalidate(); } public void setDelegate(PhotoFilterLinearBlurControlDelegate delegate) { this.delegate = delegate; } private float getDistance(MotionEvent event) { if (event.getPointerCount() != 2) { return 0; } float x1 = event.getX(0); float y1 = event.getY(0); float x2 = event.getX(1); float y2 = event.getY(1); return (float) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); } private float degreesToRadians(float degrees) { return degrees * (float) Math.PI / 180.0f; } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_DOWN: { if (event.getPointerCount() == 1) { if (checkForMoving && !isMoving) { float locationX = event.getX(); float locationY = event.getY(); Point centerPoint = getActualCenterPoint(); Point delta = new Point(locationX - centerPoint.x, locationY - centerPoint.y); float radialDistance = (float) Math.sqrt(delta.x * delta.x + delta.y * delta.y); float innerRadius = getActualInnerRadius(); float outerRadius = getActualOuterRadius(); boolean close = Math.abs(outerRadius - innerRadius) < BlurInsetProximity; float innerRadiusOuterInset = close ? 0 : BlurViewRadiusInset; float outerRadiusInnerInset = close ? 0 : BlurViewRadiusInset; if (type == 0) { float distance = (float) Math.abs(delta.x * Math.cos(degreesToRadians(angle) + Math.PI / 2) + delta.y * Math.sin(degreesToRadians(angle) + Math.PI / 2)); if (radialDistance < BlurViewCenterInset) { isMoving = true; } else if (distance > innerRadius - BlurViewRadiusInset && distance < innerRadius + innerRadiusOuterInset) { isMoving = true; } else if (distance > outerRadius - outerRadiusInnerInset && distance < outerRadius + BlurViewRadiusInset) { isMoving = true; } else if ((distance <= innerRadius - BlurViewRadiusInset) || distance >= outerRadius + BlurViewRadiusInset) { isMoving = true; } } else if (type == 1) { if (radialDistance < BlurViewCenterInset) { isMoving = true; } else if (radialDistance > innerRadius - BlurViewRadiusInset && radialDistance < innerRadius + innerRadiusOuterInset) { isMoving = true; } else if (radialDistance > outerRadius - outerRadiusInnerInset && radialDistance < outerRadius + BlurViewRadiusInset) { isMoving = true; } } checkForMoving = false; if (isMoving) { handlePan(GestureStateBegan, event); } } } else { if (isMoving) { handlePan(GestureStateEnded, event); checkForMoving = true; isMoving = false; } if (event.getPointerCount() == 2) { if (checkForZooming && !isZooming) { handlePinch(GestureStateBegan, event); isZooming = true; } } else { handlePinch(GestureStateEnded, event); checkForZooming = true; isZooming = false; } } break; } case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: { if (isMoving) { handlePan(GestureStateEnded, event); isMoving = false; } else if (isZooming) { handlePinch(GestureStateEnded, event); isZooming = false; } checkForMoving = true; checkForZooming = true; break; } case MotionEvent.ACTION_MOVE: { if (isMoving) { handlePan(GestureStateChanged, event); } else if (isZooming) { handlePinch(GestureStateChanged, event); } } } return true; } private void handlePan(int state, MotionEvent event) { float locationX = event.getX(); float locationY = event.getY(); Point actualCenterPoint = getActualCenterPoint(); float dx = locationX - actualCenterPoint.x; float dy = locationY - actualCenterPoint.y; float radialDistance = (float) Math.sqrt(dx * dx + dy * dy); float shorterSide = Math.min(actualAreaSize.width, actualAreaSize.height); float innerRadius = shorterSide * falloff; float outerRadius = shorterSide * size; float distance = (float) Math.abs(dx * Math.cos(degreesToRadians(angle) + Math.PI / 2.0f) + dy * Math.sin(degreesToRadians(angle) + Math.PI / 2.0f)); switch (state) { case GestureStateBegan: { pointerStartX = event.getX(); pointerStartY = event.getY(); boolean close = Math.abs(outerRadius - innerRadius) < BlurInsetProximity; float innerRadiusOuterInset = close ? 0 : BlurViewRadiusInset; float outerRadiusInnerInset = close ? 0 : BlurViewRadiusInset; if (type == 0) { if (radialDistance < BlurViewCenterInset) { activeControl = BlurViewActiveControl.BlurViewActiveControlCenter; startCenterPoint = actualCenterPoint; } else if (distance > innerRadius - BlurViewRadiusInset && distance < innerRadius + innerRadiusOuterInset) { activeControl = BlurViewActiveControl.BlurViewActiveControlInnerRadius; startDistance = distance; startRadius = innerRadius; } else if (distance > outerRadius - outerRadiusInnerInset && distance < outerRadius + BlurViewRadiusInset) { activeControl = BlurViewActiveControl.BlurViewActiveControlOuterRadius; startDistance = distance; startRadius = outerRadius; } else if (distance <= innerRadius - BlurViewRadiusInset || distance >= outerRadius + BlurViewRadiusInset) { activeControl = BlurViewActiveControl.BlurViewActiveControlRotation; } } else if (type == 1) { if (radialDistance < BlurViewCenterInset) { activeControl = BlurViewActiveControl.BlurViewActiveControlCenter; startCenterPoint = actualCenterPoint; } else if (radialDistance > innerRadius - BlurViewRadiusInset && radialDistance < innerRadius + innerRadiusOuterInset) { activeControl = BlurViewActiveControl.BlurViewActiveControlInnerRadius; startDistance = radialDistance; startRadius = innerRadius; } else if (radialDistance > outerRadius - outerRadiusInnerInset && radialDistance < outerRadius + BlurViewRadiusInset) { activeControl = BlurViewActiveControl.BlurViewActiveControlOuterRadius; startDistance = radialDistance; startRadius = outerRadius; } } setSelected(true, true); } break; case GestureStateChanged: { if (type == 0) { switch (activeControl) { case BlurViewActiveControlCenter: { float translationX = locationX - pointerStartX; float translationY = locationY - pointerStartY; Rect actualArea = new Rect((getWidth() - actualAreaSize.width) / 2, (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0) + (getHeight() - actualAreaSize.height) / 2, actualAreaSize.width, actualAreaSize.height); Point newPoint = new Point(Math.max(actualArea.x, Math.min(actualArea.x + actualArea.width, startCenterPoint.x + translationX)), Math.max(actualArea.y, Math.min(actualArea.y + actualArea.height, startCenterPoint.y + translationY))); centerPoint = new Point((newPoint.x - actualArea.x) / actualAreaSize.width, ((newPoint.y - actualArea.y) + (actualAreaSize.width - actualAreaSize.height) / 2) / actualAreaSize.width); } break; case BlurViewActiveControlInnerRadius: { float d = distance - startDistance; falloff = Math.min(Math.max(BlurMinimumFalloff, (startRadius + d) / shorterSide), size - BlurMinimumDifference); } break; case BlurViewActiveControlOuterRadius: { float d = distance - startDistance; size = Math.max(falloff + BlurMinimumDifference, (startRadius + d) / shorterSide); } break; case BlurViewActiveControlRotation: { float translationX = locationX - pointerStartX; float translationY = locationY - pointerStartY; boolean clockwise = false; boolean right = locationX > actualCenterPoint.x; boolean bottom = locationY > actualCenterPoint.y; final boolean b = Math.abs(translationY) > Math.abs(translationX); if (!right && !bottom) { if (b) { if (translationY < 0) { clockwise = true; } } else { if (translationX > 0) { clockwise = true; } } } else if (right && !bottom) { if (b) { if (translationY > 0) { clockwise = true; } } else { if (translationX > 0) { clockwise = true; } } } else if (right && bottom) { if (b) { if (translationY > 0) { clockwise = true; } } else { if (translationX < 0) { clockwise = true; } } } else { if (b) { if (translationY < 0) { clockwise = true; } } else { if (translationX < 0) { clockwise = true; } } } float d = (float) Math.sqrt(translationX * translationX + translationY * translationY); angle += d * ((clockwise ? 1 : 0) * 2 - 1) / (float) Math.PI / 1.15f; pointerStartX = locationX; pointerStartY = locationY; } break; default: break; } } else if (type == 1) { switch (activeControl) { case BlurViewActiveControlCenter: { float translationX = locationX - pointerStartX; float translationY = locationY - pointerStartY; Rect actualArea = new Rect((getWidth() - actualAreaSize.width) / 2, (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0) + (getHeight() - actualAreaSize.height) / 2, actualAreaSize.width, actualAreaSize.height); Point newPoint = new Point(Math.max(actualArea.x, Math.min(actualArea.x + actualArea.width, startCenterPoint.x + translationX)), Math.max(actualArea.y, Math.min(actualArea.y + actualArea.height, startCenterPoint.y + translationY))); centerPoint = new Point((newPoint.x - actualArea.x) / actualAreaSize.width, ((newPoint.y - actualArea.y) + (actualAreaSize.width - actualAreaSize.height) / 2) / actualAreaSize.width); } break; case BlurViewActiveControlInnerRadius: { float d = radialDistance - startDistance; falloff = Math.min(Math.max(BlurMinimumFalloff, (startRadius + d) / shorterSide), size - BlurMinimumDifference); } break; case BlurViewActiveControlOuterRadius: { float d = radialDistance - startDistance; size = Math.max(falloff + BlurMinimumDifference, (startRadius + d) / shorterSide); } break; default: break; } } invalidate(); if (delegate != null) { delegate.valueChanged(centerPoint, falloff, size, degreesToRadians(angle) + (float) Math.PI / 2.0f); } } break; case GestureStateEnded: case GestureStateCancelled: case GestureStateFailed: { activeControl = BlurViewActiveControl.BlurViewActiveControlNone; setSelected(false, true); } break; default: break; } } private void handlePinch(int state, MotionEvent event) { switch (state) { case GestureStateBegan: { startPointerDistance = getDistance(event); pointerScale = 1; activeControl = BlurViewActiveControl.BlurViewActiveControlWholeArea; setSelected(true, true); } case GestureStateChanged: { float newDistance = getDistance(event); pointerScale += (newDistance - startPointerDistance) / AndroidUtilities.density * 0.01f; falloff = Math.max(BlurMinimumFalloff, falloff * pointerScale); size = Math.max(falloff + BlurMinimumDifference, size * pointerScale); pointerScale = 1; startPointerDistance = newDistance; invalidate(); if (delegate != null) { delegate.valueChanged(centerPoint, falloff, size, degreesToRadians(angle) + (float) Math.PI / 2.0f); } } break; case GestureStateEnded: case GestureStateCancelled: case GestureStateFailed: { activeControl = BlurViewActiveControl.BlurViewActiveControlNone; setSelected(false, true); } break; default: break; } } private void setSelected(boolean selected, boolean animated) { /*if (animated) { [UIView animateWithDuration:0.16f delay:0.0f options:UIViewAnimationOptionBeginFromCurrentState animations:^ { self.alpha = selected ? 0.6f : 1.0f; } completion:nil]; } else { self.alpha = selected ? 0.6f : 1.0f; }*/ } public void setActualAreaSize(float width, float height) { actualAreaSize.width = width; actualAreaSize.height = height; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Point centerPoint = getActualCenterPoint(); float innerRadius = getActualInnerRadius(); float outerRadius = getActualOuterRadius(); canvas.translate(centerPoint.x, centerPoint.y); if (type == 0) { canvas.rotate(angle); float space = AndroidUtilities.dp(6.0f); float length = AndroidUtilities.dp(12.0f); float thickness = AndroidUtilities.dp(1.5f); for (int i = 0; i < 30; i++) { canvas.drawRect(i * (length + space), -innerRadius, i * (length + space) + length, thickness - innerRadius, paint); float left = -i * (length + space) - space - length; float right = -i * (length + space) - space; canvas.drawRect(left, -innerRadius, right, thickness - innerRadius, paint); canvas.drawRect(i * (length + space), innerRadius, length + i * (length + space), thickness + innerRadius, paint); canvas.drawRect(left, innerRadius, right, thickness + innerRadius, paint); } length = AndroidUtilities.dp(6.0f); for (int i = 0; i < 64; i++) { canvas.drawRect(i * (length + space), -outerRadius, length + i * (length + space), thickness - outerRadius, paint); float left = -i * (length + space) - space - length; float right = -i * (length + space) - space; canvas.drawRect(left, -outerRadius, right, thickness - outerRadius, paint); canvas.drawRect(i * (length + space), outerRadius, length + i * (length + space), thickness + outerRadius, paint); canvas.drawRect(left, outerRadius, right, thickness + outerRadius, paint); } } else if (type == 1) { float radSpace = 6.15f; float radLen = 10.2f; arcRect.set(-innerRadius, -innerRadius, innerRadius, innerRadius); for (int i = 0; i < 22; i++) { canvas.drawArc(arcRect, i * (radSpace + radLen), radLen, false, arcPaint); } radSpace = 2.02f; radLen = 3.6f; arcRect.set(-outerRadius, -outerRadius, outerRadius, outerRadius); for (int i = 0; i < 64; i++) { canvas.drawArc(arcRect, i * (radSpace + radLen), radLen, false, arcPaint); } } canvas.drawCircle(0, 0, AndroidUtilities.dp(8), paint); } private Point getActualCenterPoint() { return new Point((getWidth() - actualAreaSize.width) / 2 + centerPoint.x * actualAreaSize.width, (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0) + (getHeight() - actualAreaSize.height) / 2 - (actualAreaSize.width - actualAreaSize.height) / 2 + centerPoint.y * actualAreaSize.width); } private float getActualInnerRadius() { return (Math.min(actualAreaSize.width, actualAreaSize.height)) * falloff; } private float getActualOuterRadius() { return (Math.min(actualAreaSize.width, actualAreaSize.height)) * size; } }