Compare commits

...

4 Commits

Author SHA1 Message Date
Ali Asadi 9454f3e4d1 Add TopCrop option to ScaleType (#372) 2021-12-10 10:53:46 +01:00
Ilia Voitcekhovskii 7094f9850a Fix unused inSampleSize and add scaling to decoding step (#359)
* Fix bitmap decoding to use inSampleSize

* Add scaling (by max side) while decoding bitmap
2021-12-10 10:51:01 +01:00
John 7039cf2059 Pinch to zoom (#388)
* Update gradle and libs

* Implemented pinch to zoom in example app
2021-12-10 10:50:19 +01:00
zakimiiiii 09a551635c Modified file to correct name (#378) 2019-07-02 14:15:09 +02:00
12 changed files with 198 additions and 47 deletions
+5 -9
View File
@@ -9,8 +9,7 @@ subprojects {
buildscript {
ext {
versions = [
gradle : '4.10.2',
kotlin : '1.3.0',
kotlin : '1.3.50',
code : 1,
name : '1.0.0',
sdk : [
@@ -19,13 +18,13 @@ buildscript {
],
android: [
buildTools: '28.0.3',
appcompat : '1.0.1',
annotation : '1.0.0',
appcompat : '1.1.0',
annotation : '1.1.0',
exifinterface : '1.0.0'
],
rx : [
rxJava1: '1.3.8',
rxJava2: '2.2.3'
rxJava2: '2.2.12'
],
test : [
junit : '4.12',
@@ -38,7 +37,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.0'
classpath 'com.android.tools.build:gradle:3.5.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
@@ -57,6 +56,3 @@ task clean(type: Delete) {
delete rootProject.buildDir
}
task wrapper(type: Wrapper) {
gradleVersion = versions.gradle
}
+1 -1
View File
@@ -32,7 +32,7 @@ dependencies {
implementation "androidx.annotation:annotation:${versions.android.annotation}"
implementation "androidx.exifinterface:exifinterface:${versions.android.exifinterface}"
implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
testImplementation "junit:junit:${versions.test.junit}"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:${versions.kotlin}"
testImplementation "org.mockito:mockito-core:${versions.test.mockito}"
@@ -1,13 +1,13 @@
package io.fotoapparat.coroutines
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
/**
* A [ConflatedBroadcastChannel] which exposes a [getValue] which will [await] for at least one value.
*/
@ExperimentalCoroutinesApi
internal class AwaitBroadcastChannel<T>(
private val channel: ConflatedBroadcastChannel<T> = ConflatedBroadcastChannel(),
private val deferred: CompletableDeferred<Boolean> = CompletableDeferred()
@@ -31,7 +31,13 @@ internal class AwaitBroadcastChannel<T>(
channel.send(element)
}
override fun cancel(cause: CancellationException?) {
channel.cancel(cause)
deferred.cancel(cause)
}
override fun cancel(cause: Throwable?): Boolean {
return channel.cancel(cause) && deferred.cancel(cause)
deferred.cancel(cause?.message ?:"", cause)
return channel.close(cause)
}
}
@@ -15,6 +15,13 @@ enum class ScaleType {
* The preview will be scaled so as its one dimensions will be equal and the other one equal or
* smaller than the corresponding dimension of the view
*/
CenterInside
CenterInside,
/**
* The preview will be scaled so as its one dimensions will be equal and the other one equal or
* larger than the corresponding dimension of the view with focus on the top part
*/
TopCrop,
}
@@ -23,7 +23,8 @@ internal class BitmapPhotoTransformer(
desiredResolution = desiredResolution
)
val decodedBitmap = input.decodeBitmap(scaleFactor) ?: throw UnableToDecodeBitmapException()
val decodedBitmap = input.decodeBitmap(scaleFactor, originalResolution, desiredResolution)
?: throw UnableToDecodeBitmapException()
val bitmap = if (decodedBitmap.width == desiredResolution.width && decodedBitmap.height == desiredResolution.height) {
decodedBitmap
@@ -44,14 +45,28 @@ internal class BitmapPhotoTransformer(
}
private fun Photo.decodeBitmap(scaleFactor: Float): Bitmap? {
private fun Photo.decodeBitmap(
scaleFactor: Float,
originalResolution: Resolution,
desiredResolution: Resolution
): Bitmap? {
val options = BitmapFactory.Options()
options.inSampleSize = scaleFactor.toInt()
options.inScaled = true
if (desiredResolution.width > desiredResolution.height) {
options.inDensity = originalResolution.width
options.inTargetDensity = desiredResolution.width * options.inSampleSize
} else {
options.inDensity = originalResolution.height
options.inTargetDensity = desiredResolution.height * options.inSampleSize
}
return BitmapFactory.decodeByteArray(
encodedImage,
0,
encodedImage.size
encodedImage.size,
options
)
}
@@ -82,6 +82,7 @@ private fun ViewGroup.layoutTextureView(
) = when (scaleType) {
ScaleType.CenterInside -> previewResolution?.centerInside(this)
ScaleType.CenterCrop -> previewResolution?.centerCrop(this)
ScaleType.TopCrop -> previewResolution?.topCrop(this)
else -> null
}
@@ -129,6 +130,28 @@ private fun Resolution.centerCrop(view: ViewGroup) {
view.layoutChildrenAt(rect)
}
private fun Resolution.topCrop(view: ViewGroup) {
val scale = Math.max(
view.measuredWidth / width.toFloat(),
view.measuredHeight / height.toFloat()
)
val width = (width * scale).toInt()
val height = (height * scale).toInt()
val extraX = Math.max(0, width - view.measuredWidth)
val rect = Rect(
-extraX / 2,
0,
width - extraX / 2,
height
)
view.layoutChildrenAt(rect)
}
private fun ViewGroup.layoutChildrenAt(rect: Rect) {
(0 until childCount).forEach {
getChildAt(it).layout(
@@ -5,6 +5,7 @@ import android.content.Context
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.widget.FrameLayout
import io.fotoapparat.hardware.metering.FocalRequest
import io.fotoapparat.hardware.metering.PointF
@@ -15,7 +16,7 @@ import io.fotoapparat.parameter.Resolution
*
* If the camera doesn't support focus metering on specific area this will only display a visual feedback.
*/
class FocusView
open class FocusView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@@ -25,6 +26,15 @@ class FocusView
private val visualFeedbackCircle = FeedbackCircleView(context, attrs, defStyleAttr)
private var focusMeteringListener: ((FocalRequest) -> Unit)? = null
var scaleListener: ((Float) -> Unit)? = null
var ptrListener: ((Int) -> Unit)? = null
private var mPtrCount: Int = 0
set(value) {
field = value
ptrListener?.invoke(value)
}
init {
clipToPadding = false
clipChildren = false
@@ -38,6 +48,14 @@ class FocusView
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
tapDetector.onTouchEvent(event)
scaleDetector.onTouchEvent(event)
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_POINTER_DOWN -> mPtrCount++
MotionEvent.ACTION_POINTER_UP -> mPtrCount--
MotionEvent.ACTION_DOWN -> mPtrCount++
MotionEvent.ACTION_UP -> mPtrCount--
}
return true
}
@@ -64,4 +82,15 @@ class FocusView
}
private val tapDetector = GestureDetector(context, gestureDetectorListener)
private val scaleGestureDetector = object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
return scaleListener
?.let {
it(detector.scaleFactor)
true
}?: super.onScale(detector)
}
}
private val scaleDetector = ScaleGestureDetector(context, scaleGestureDetector)
}
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
@@ -5,23 +5,26 @@ import android.util.Log;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.Locale;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SwitchCompat;
import io.fotoapparat.Fotoapparat;
import io.fotoapparat.capability.Capabilities;
import io.fotoapparat.configuration.CameraConfiguration;
import io.fotoapparat.configuration.UpdateConfiguration;
import io.fotoapparat.error.CameraErrorListener;
import io.fotoapparat.exception.camera.CameraException;
import io.fotoapparat.parameter.ScaleType;
import io.fotoapparat.parameter.Zoom;
import io.fotoapparat.preview.Frame;
import io.fotoapparat.preview.FrameProcessor;
import io.fotoapparat.result.BitmapPhoto;
@@ -29,6 +32,8 @@ import io.fotoapparat.result.PhotoResult;
import io.fotoapparat.result.WhenDoneListener;
import io.fotoapparat.view.CameraView;
import io.fotoapparat.view.FocusView;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
import static io.fotoapparat.log.LoggersKt.fileLogger;
import static io.fotoapparat.log.LoggersKt.logcat;
@@ -57,9 +62,13 @@ public class ActivityJava extends AppCompatActivity {
private boolean hasCameraPermission;
private CameraView cameraView;
private FocusView focusView;
private TextView zoomLvl;
private ImageView switchCamera;
private View capture;
private Fotoapparat fotoapparat;
private Zoom.VariableZoom cameraZoom;
private float curZoom = 0f;
boolean activeCameraBack = true;
@@ -92,6 +101,8 @@ public class ActivityJava extends AppCompatActivity {
cameraView = findViewById(R.id.cameraView);
focusView = findViewById(R.id.focusView);
capture = findViewById(R.id.capture);
zoomLvl = findViewById(R.id.zoomLvl);
switchCamera = findViewById(R.id.switchCamera);
hasCameraPermission = permissionsDelegate.hasCameraPermission();
if (hasCameraPermission) {
@@ -105,7 +116,6 @@ public class ActivityJava extends AppCompatActivity {
takePictureOnClick();
switchCameraOnClick();
toggleTorchOnSwitch();
zoomSeekBar();
}
private Fotoapparat createFotoapparat() {
@@ -129,15 +139,66 @@ public class ActivityJava extends AppCompatActivity {
.build();
}
private void zoomSeekBar() {
SeekBar seekBar = findViewById(R.id.zoomSeekBar);
seekBar.setOnSeekBarChangeListener(new OnProgressChanged() {
private void adjustViewsVisibility() {
fotoapparat.getCapabilities().whenAvailable(new Function1<Capabilities, Unit>() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
fotoapparat.setZoom(progress / (float) seekBar.getMax());
public Unit invoke(Capabilities capabilities) {
Zoom zoom = capabilities != null ? capabilities.getZoom() : null;
if(zoom instanceof Zoom.VariableZoom){
cameraZoom = (Zoom.VariableZoom) zoom;
focusView.setScaleListener(new Function1<Float, Unit>() {
@Override
public Unit invoke(Float aFloat) {
scaleZoom(aFloat);
return null;
}
});
focusView.setPtrListener(new Function1<Integer, Unit>() {
@Override
public Unit invoke(Integer integer) {
pointerChanged(integer);
return null;
}
});
} else {
zoomLvl.setVisibility(View.GONE);
focusView.setScaleListener(null);
focusView.setPtrListener(null);
}
return null;
}
});
if (fotoapparat.isAvailable(front())){
switchCamera.setVisibility(View.VISIBLE);
} else {
switchCamera.setVisibility(View.GONE);
}
}
private void scaleZoom(float scaleFactor){
float plusZoom = 0;
if (scaleFactor < 1) plusZoom = -1 * (1 - scaleFactor);
else plusZoom = scaleFactor - 1;
float newZoom = curZoom + plusZoom;
if (newZoom < 0 || newZoom > 1) return;
curZoom = newZoom;
fotoapparat.setZoom(curZoom);
int progress = Math.round (cameraZoom.getMaxZoom() * curZoom);
int value = cameraZoom.getZoomRatios().get(progress);
float roundedValue = (float)(Math.round(((float)value) / 10f)) / 10f;
zoomLvl.setVisibility(View.VISIBLE);
zoomLvl.setText(String.format(Locale.getDefault(), "%.1f×", roundedValue));
}
private void pointerChanged(int fingerCount){
if(fingerCount == 0) {
zoomLvl.setVisibility(View.GONE);
}
}
private void switchCameraOnClick() {
@@ -180,6 +241,7 @@ public class ActivityJava extends AppCompatActivity {
activeCameraBack ? back() : front(),
cameraConfiguration
);
adjustViewsVisibility();
}
});
}
@@ -223,6 +285,7 @@ public class ActivityJava extends AppCompatActivity {
super.onStart();
if (hasCameraPermission) {
fotoapparat.start();
adjustViewsVisibility();
}
}
@@ -242,6 +305,7 @@ public class ActivityJava extends AppCompatActivity {
if (permissionsDelegate.resultGranted(requestCode, permissions, grantResults)) {
hasCameraPermission = true;
fotoapparat.start();
adjustViewsVisibility();
cameraView.setVisibility(View.VISIBLE);
}
}
@@ -28,6 +28,8 @@ class MainActivity : AppCompatActivity() {
private lateinit var fotoapparat: Fotoapparat
private lateinit var cameraZoom: Zoom.VariableZoom
private var curZoom: Float = 0f
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
@@ -148,8 +150,16 @@ class MainActivity : AppCompatActivity() {
capabilities
?.let {
(it.zoom as? Zoom.VariableZoom)
?.let { zoom -> setupZoom(zoom) }
?: run { zoomSeekBar.visibility = View.GONE }
?.let {
cameraZoom = it
focusView.scaleListener = this::scaleZoom
focusView.ptrListener = this::pointerChanged
}
?: run {
zoomLvl?.visibility = View.GONE
focusView.scaleListener = null
focusView.ptrListener = null
}
torchSwitch.visibility = if (it.flashModes.contains(Flash.Torch)) View.VISIBLE else View.GONE
}
@@ -159,19 +169,27 @@ class MainActivity : AppCompatActivity() {
switchCamera.visibility = if (fotoapparat.isAvailable(front())) View.VISIBLE else View.GONE
}
private fun setupZoom(zoom: Zoom.VariableZoom) {
zoomSeekBar.max = zoom.maxZoom
cameraZoom = zoom
zoomSeekBar.visibility = View.VISIBLE
zoomSeekBar onProgressChanged { updateZoom(zoomSeekBar.progress) }
updateZoom(0)
}
//When zooming slowly, the values are approximately 0.9 ~ 1.1
private fun scaleZoom(scaleFactor: Float) {
//convert to -0.1 ~ 0.1
val plusZoom = if (scaleFactor < 1) -1 * (1 - scaleFactor) else scaleFactor - 1
val newZoom = curZoom + plusZoom
if (newZoom < 0 || newZoom > 1) return
private fun updateZoom(progress: Int) {
fotoapparat.setZoom(progress.toFloat() / zoomSeekBar.max)
curZoom = newZoom
fotoapparat.setZoom(curZoom)
val progress = (cameraZoom.maxZoom * curZoom).roundToInt()
val value = cameraZoom.zoomRatios[progress]
val roundedValue = ((value.toFloat()) / 10).roundToInt().toFloat() / 10
zoomLvl.text = String.format("%.1f ×", roundedValue)
zoomLvl.visibility = View.VISIBLE
zoomLvl.text = String.format("%.1f×", roundedValue)
}
private fun pointerChanged(fingerCount: Int){
if(fingerCount == 0) {
zoomLvl?.visibility = View.GONE
}
}
}
+1 -8
View File
@@ -53,12 +53,6 @@
android:padding="20dp"
tools:ignore="RtlHardcoded" />
<SeekBar
android:id="@+id/zoomSeekBar"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<ImageView
android:id="@+id/switchCamera"
android:layout_width="wrap_content"
@@ -74,8 +68,7 @@
android:id="@+id/zoomLvl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|top"
android:layout_marginTop="50dp"
android:layout_gravity="center"
android:textColor="#FFF"
android:textSize="20sp"
tools:text="2.4" />