diff --git a/build.gradle.kts b/build.gradle.kts index 810382f4..5dbf725d 100755 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,39 +1,41 @@ @Suppress("DSL_SCOPE_VIOLATION") plugins { - alias(libs.plugins.android.application) apply false - alias(libs.plugins.android.library) apply false - alias(libs.plugins.kotlin.android) apply false - alias(libs.plugins.baseline.profile) apply false - alias(libs.plugins.nexus.plugin) - alias(libs.plugins.spotless) - alias(libs.plugins.dokka) - alias(libs.plugins.kotlin.binary.compatibility) + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.baseline.profile) apply false + alias(libs.plugins.nexus.plugin) + alias(libs.plugins.spotless) + alias(libs.plugins.dokka) + alias(libs.plugins.kotlin.binary.compatibility) } subprojects { - apply(plugin = rootProject.libs.plugins.spotless.get().pluginId) + apply(plugin = rootProject.libs.plugins.spotless.get().pluginId) + if (!this.name.contains("dialog")) { configure { - kotlin { - target("**/*.kt") - targetExclude("$buildDir/**/*.kt") - ktlint().editorConfigOverride( - mapOf( - "indent_size" to "2", - "continuation_indent_size" to "2" - ) - ) - licenseHeaderFile(rootProject.file("spotless/copyright.kt")) - trimTrailingWhitespace() - endWithNewline() - } - format("xml") { - target("**/*.xml") - targetExclude("**/build/**/*.xml") - // Look for the first XML tag that isn't a comment ( + diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/PhotoViewDialog.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/PhotoViewDialog.kt new file mode 100644 index 00000000..ebf07e4b --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/PhotoViewDialog.kt @@ -0,0 +1,328 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getstream.photoview.dialog + +import android.content.Context +import android.util.Log +import android.view.View +import android.widget.ImageView +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DimenRes +import androidx.annotation.Px +import androidx.core.content.ContextCompat +import io.getstream.photoview.dialog.listeners.OnDismissListener +import io.getstream.photoview.dialog.listeners.OnImageChangeListener +import io.getstream.photoview.dialog.loader.ImageLoader +import io.getstream.photoview.dialog.viewer.builder.BuilderData +import io.getstream.photoview.dialog.viewer.dialog.ImageViewerDialog +import kotlin.math.roundToInt + +//N.B.! This class is written in Java for convenient use of lambdas due to languages compatibility issues. +@Suppress("unused") +public class PhotoViewDialog( + context: Context, + private val builderData: BuilderData +) { + private val dialog: ImageViewerDialog + + init { + dialog = ImageViewerDialog(context, builderData) + } + /** + * Displays the built viewer if passed list of images is not empty + * + * @param animate whether the passed transition view should be animated on open. Useful for screen rotation handling. + */ + /** + * Displays the built viewer if passed list of images is not empty + */ + @JvmOverloads + public fun show(animate: Boolean = true) { + if (builderData.images.isNotEmpty()) { + dialog.show(animate) + } else { + Log.w("PhotoView", "Images list cannot be empty! Viewer ignored.") + } + } + + /** + * Closes the viewer with suitable close animation + */ + public fun close() { + dialog.close() + } + + /** + * Dismisses the dialog with no animation + */ + public fun dismiss() { + dialog.dismiss() + } + + /** + * Updates an existing images list if a new list is not empty, otherwise closes the viewer + */ + public fun updateImages(images: Array) { + updateImages(ArrayList(listOf(*images))) + } + + /** + * Updates an existing images list if a new list is not empty, otherwise closes the viewer + */ + public fun updateImages(images: List) { + if (images.isNotEmpty()) { + dialog.updateImages(images) + } else { + dialog.close() + } + } + + public fun currentPosition(): Int { + return dialog.getCurrentPosition() + } + + public fun setCurrentPosition(position: Int): Int { + return dialog.setCurrentPosition(position) + } + + /** + * Updates transition image view. + * Useful for a case when image position has changed and you want to update the transition animation target. + */ + public fun updateTransitionImage(imageView: ImageView?) { + dialog.updateTransitionImage(imageView) + } + + /** + * Builder class for [PhotoViewDialog] + */ + public class Builder( + private val context: Context, + images: List, + imageLoader: ImageLoader + ) { + private val data: BuilderData + + public constructor( + context: Context, + images: Array, + imageLoader: ImageLoader + ) : this( + context, ArrayList( + listOf(*images) + ), imageLoader + ) + + init { + data = BuilderData(images, imageLoader) + } + + /** + * Sets a position to start viewer from. + * + * @return This Builder object to allow calls chaining + */ + public fun withStartPosition(position: Int): Builder { + data.startPosition = position + return this + } + + /** + * Sets a background color value for the viewer + * + * @return This Builder object to allow calls chaining + */ + public fun withBackgroundColor(@ColorInt color: Int): Builder { + data.backgroundColor = color + return this + } + + /** + * Sets a background color resource for the viewer + * + * @return This Builder object to allow calls chaining + */ + public fun withBackgroundColorResource(@ColorRes color: Int): Builder { + return withBackgroundColor(ContextCompat.getColor(context, color)) + } + + /** + * Sets custom overlay view to be shown over the viewer. + * Commonly used for image description or counter displaying. + * + * @return This Builder object to allow calls chaining + */ + public fun withOverlayView(view: View): Builder { + data.overlayView = view + return this + } + + /** + * Sets space between the images using dimension. + * + * @return This Builder object to allow calls chaining + */ + public fun withImagesMargin(@DimenRes dimen: Int): Builder { + data.imageMarginPixels = context.resources.getDimension(dimen).roundToInt() + return this + } + + /** + * Sets space between the images in pixels. + * + * @return This Builder object to allow calls chaining + */ + public fun withImageMarginPixels(marginPixels: Int): Builder { + data.imageMarginPixels = marginPixels + return this + } + + /** + * Sets overall padding for zooming and scrolling area using dimension. + * + * @return This Builder object to allow calls chaining + */ + public fun withContainerPadding(@DimenRes padding: Int): Builder { + val paddingPx = context.resources.getDimension(padding).roundToInt() + return withContainerPaddingPixels(paddingPx, paddingPx, paddingPx, paddingPx) + } + + /** + * Sets `start`, `top`, `end` and `bottom` padding for zooming and scrolling area using dimension. + * + * @return This Builder object to allow calls chaining + */ + public fun withContainerPadding( + @DimenRes start: Int, @DimenRes top: Int, + @DimenRes end: Int, @DimenRes bottom: Int + ): Builder { + withContainerPaddingPixels( + context.resources.getDimension(start).roundToInt(), + context.resources.getDimension(top).roundToInt(), + context.resources.getDimension(end).roundToInt(), + context.resources.getDimension(bottom).roundToInt() + ) + return this + } + + /** + * Sets overall padding for zooming and scrolling area in pixels. + * + * @return This Builder object to allow calls chaining + */ + public fun withContainerPaddingPixels(@Px padding: Int): Builder { + data.containerPaddingPixels = intArrayOf(padding, padding, padding, padding) + return this + } + + /** + * Sets `start`, `top`, `end` and `bottom` padding for zooming and scrolling area in pixels. + * + * @return This Builder object to allow calls chaining + */ + public fun withContainerPaddingPixels(start: Int, top: Int, end: Int, bottom: Int): Builder { + data.containerPaddingPixels = intArrayOf(start, top, end, bottom) + return this + } + + /** + * Sets status bar visibility. True by default. + * + * @return This Builder object to allow calls chaining + */ + public fun withHiddenStatusBar(value: Boolean): Builder { + data.shouldStatusBarHide = value + return this + } + + /** + * Enables or disables zooming. True by default. + * + * @return This Builder object to allow calls chaining + */ + public fun allowZooming(value: Boolean): Builder { + data.isZoomingAllowed = value + return this + } + + /** + * Enables or disables the "Swipe to Dismiss" gesture. True by default. + * + * @return This Builder object to allow calls chaining + */ + public fun allowSwipeToDismiss(value: Boolean): Builder { + data.isSwipeToDismissAllowed = value + return this + } + + /** + * Sets a target [ImageView] to be part of transition when opening or closing the viewer/ + * + * @return This Builder object to allow calls chaining + */ + public fun withTransitionFrom(imageView: ImageView?): Builder { + data.transitionView = imageView + return this + } + + /** + * Sets [OnImageChangeListener] for the viewer. + * + * @return This Builder object to allow calls chaining + */ + public fun withImageChangeListener(imageChangeListener: OnImageChangeListener?): Builder { + data.imageChangeListener = imageChangeListener + return this + } + + /** + * Sets [OnDismissListener] for viewer. + * + * @return This Builder object to allow calls chaining + */ + public fun withDismissListener(onDismissListener: OnDismissListener?): Builder { + data.onDismissListener = onDismissListener + return this + } + + /** + * Creates a [PhotoViewDialog] with the arguments supplied to this builder. It does not + * show the dialog. This allows the user to do any extra processing + * before displaying the dialog. Use [.show] if you don't have any other processing + * to do and want this to be created and displayed. + */ + public fun build(): PhotoViewDialog { + return PhotoViewDialog(context, data) + } + /** + * Creates the [PhotoViewDialog] with the arguments supplied to this builder and + * shows the dialog. + * + * @param animate whether the passed transition view should be animated on open. Useful for screen rotation handling. + */ + /** + * Creates the [PhotoViewDialog] with the arguments supplied to this builder and + * shows the dialog. + */ + @JvmOverloads + public fun show(animate: Boolean = true): PhotoViewDialog { + val viewer = build() + viewer.show(animate) + return viewer + } + } +} diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/ImageView.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/ImageView.kt new file mode 100644 index 00000000..22a5aaa3 --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/ImageView.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.photoview.dialog.common.extensions + +import android.graphics.drawable.BitmapDrawable +import android.widget.ImageView + +@JvmSynthetic +internal fun ImageView.copyBitmapFrom(target: ImageView?) { + target?.drawable?.let { + if (it is BitmapDrawable) { + setImageBitmap(it.bitmap) + } + } +} \ No newline at end of file diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/PhotoView.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/PhotoView.kt new file mode 100644 index 00000000..0986fb48 --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/PhotoView.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.photoview.dialog.common.extensions + +import io.getstream.photoview.PhotoView + +@JvmSynthetic +internal fun PhotoView.resetScale(animate: Boolean) { + setScale(minimumScale, animate) +} \ No newline at end of file diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/SparseArray.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/SparseArray.kt new file mode 100644 index 00000000..cd4b2366 --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/SparseArray.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.photoview.dialog.common.extensions + +import android.util.SparseArray +import java.util.ConcurrentModificationException + +@JvmSynthetic +internal inline fun SparseArray.forEach(block: (Int, T) -> Unit) { + val size = this.size() + for (index in 0 until size) { + if (size != this.size()) throw ConcurrentModificationException() + block(this.keyAt(index), this.valueAt(index)) + } +} \ No newline at end of file diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/Transition.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/Transition.kt new file mode 100644 index 00000000..6ab0fca7 --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/Transition.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.photoview.dialog.common.extensions + +import android.transition.Transition + +@JvmSynthetic +internal fun Transition.addListener( + onTransitionEnd: ((Transition) -> Unit)? = null, + onTransitionResume: ((Transition) -> Unit)? = null, + onTransitionPause: ((Transition) -> Unit)? = null, + onTransitionCancel: ((Transition) -> Unit)? = null, + onTransitionStart: ((Transition) -> Unit)? = null +) = addListener( + object : Transition.TransitionListener { + override fun onTransitionEnd(transition: Transition) { + onTransitionEnd?.invoke(transition) + } + + override fun onTransitionResume(transition: Transition) { + onTransitionResume?.invoke(transition) + } + + override fun onTransitionPause(transition: Transition) { + onTransitionPause?.invoke(transition) + } + + override fun onTransitionCancel(transition: Transition) { + onTransitionCancel?.invoke(transition) + } + + override fun onTransitionStart(transition: Transition) { + onTransitionStart?.invoke(transition) + } + }) \ No newline at end of file diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/View.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/View.kt new file mode 100644 index 00000000..d10cba46 --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/View.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.photoview.dialog.common.extensions + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ObjectAnimator +import android.graphics.Rect +import android.view.View +import android.view.ViewConfiguration +import android.view.ViewGroup + +internal val View?.localVisibleRect: Rect + get() = Rect().also { this?.getLocalVisibleRect(it) } + +internal val View?.globalVisibleRect: Rect + get() = Rect().also { this?.getGlobalVisibleRect(it) } + +internal val View?.hitRect: Rect + get() = Rect().also { this?.getHitRect(it) } + +internal val View?.isRectVisible: Boolean + get() = this != null && globalVisibleRect != localVisibleRect + +internal val View?.isVisible: Boolean + get() = this != null && visibility == View.VISIBLE + +internal fun View.makeVisible() { + visibility = View.VISIBLE +} + +internal fun View.makeInvisible() { + visibility = View.INVISIBLE +} + +internal fun View.makeGone() { + visibility = View.GONE +} + +internal inline fun T.postApply(crossinline block: T.() -> Unit) { + post { apply(block) } +} + +internal inline fun T.postDelayed(delayMillis: Long, crossinline block: T.() -> Unit) { + postDelayed({ block() }, delayMillis) +} + +internal fun View.applyMargin( + start: Int? = null, + top: Int? = null, + end: Int? = null, + bottom: Int? = null +) { + if (layoutParams is ViewGroup.MarginLayoutParams) { + layoutParams = (layoutParams as ViewGroup.MarginLayoutParams).apply { + marginStart = start ?: marginStart + topMargin = top ?: topMargin + marginEnd = end ?: marginEnd + bottomMargin = bottom ?: bottomMargin + } + } +} + +internal fun View.requestNewSize( + width: Int, height: Int +) { + layoutParams.width = width + layoutParams.height = height + layoutParams = layoutParams +} + +internal fun View.makeViewMatchParent() { + applyMargin(0, 0, 0, 0) + requestNewSize( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) +} + +internal fun View.animateAlpha(from: Float?, to: Float?, duration: Long) { + alpha = from ?: 0f + clearAnimation() + animate() + .alpha(to ?: 0f) + .setDuration(duration) + .start() +} + +internal fun View.switchVisibilityWithAnimation() { + val isVisible = visibility == View.VISIBLE + val from = if (isVisible) 1.0f else 0.0f + val to = if (isVisible) 0.0f else 1.0f + + ObjectAnimator.ofFloat(this, "alpha", from, to).apply { + duration = ViewConfiguration.getDoubleTapTimeout().toLong() + + if (isVisible) { + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + makeGone() + } + }) + } else { + makeVisible() + } + + start() + } +} + diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/ViewPager.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/ViewPager.kt new file mode 100644 index 00000000..dd788244 --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/ViewPager.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.photoview.dialog.common.extensions + +import androidx.viewpager.widget.ViewPager + +internal fun ViewPager.addOnPageChangeListener( + onPageScrolled: ((position: Int, offset: Float, offsetPixels: Int) -> Unit)? = null, + onPageSelected: ((position: Int) -> Unit)? = null, + onPageScrollStateChanged: ((state: Int) -> Unit)? = null +) = object : ViewPager.OnPageChangeListener { + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { + onPageScrolled?.invoke(position, positionOffset, positionOffsetPixels) + } + + override fun onPageSelected(position: Int) { + onPageSelected?.invoke(position) + } + + override fun onPageScrollStateChanged(state: Int) { + onPageScrollStateChanged?.invoke(state) + } +}.also { addOnPageChangeListener(it) } \ No newline at end of file diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/ViewPropertyAnimator.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/ViewPropertyAnimator.kt new file mode 100644 index 00000000..25f8947f --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/extensions/ViewPropertyAnimator.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.photoview.dialog.common.extensions + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.view.ViewPropertyAnimator + +internal fun ViewPropertyAnimator.setAnimatorListener( + onAnimationEnd: ((Animator?) -> Unit)? = null, + onAnimationStart: ((Animator?) -> Unit)? = null +) = this.setListener( + object : AnimatorListenerAdapter() { + + override fun onAnimationEnd(animation: Animator) { + onAnimationEnd?.invoke(animation) + } + + override fun onAnimationStart(animation: Animator) { + onAnimationStart?.invoke(animation) + } + } +) \ No newline at end of file diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/gestures/detector/SimpleOnGestureListener.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/gestures/detector/SimpleOnGestureListener.kt new file mode 100644 index 00000000..da6b9595 --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/gestures/detector/SimpleOnGestureListener.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.photoview.dialog.common.gestures.detector + +import android.view.GestureDetector +import android.view.MotionEvent + +internal class SimpleOnGestureListener( + private val onSingleTap: ((MotionEvent) -> Boolean)? = null, + private val onDoubleTap: ((MotionEvent) -> Boolean)? = null +) : GestureDetector.SimpleOnGestureListener() { + + override fun onSingleTapConfirmed(event: MotionEvent): Boolean = + onSingleTap?.invoke(event) ?: false + + override fun onDoubleTap(event: MotionEvent): Boolean = + onDoubleTap?.invoke(event) ?: false +} \ No newline at end of file diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/gestures/direction/SwipeDirection.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/gestures/direction/SwipeDirection.kt new file mode 100644 index 00000000..6ba2faea --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/gestures/direction/SwipeDirection.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.photoview.dialog.common.gestures.direction + +internal enum class SwipeDirection { + NOT_DETECTED, + UP, + DOWN, + LEFT, + RIGHT; + + companion object { + fun fromAngle(angle: Double): SwipeDirection { + return when (angle) { + in 0.0..45.0 -> RIGHT + in 45.0..135.0 -> UP + in 135.0..225.0 -> LEFT + in 225.0..315.0 -> DOWN + in 315.0..360.0 -> RIGHT + else -> NOT_DETECTED + } + } + } +} \ No newline at end of file diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/gestures/direction/SwipeDirectionDetector.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/gestures/direction/SwipeDirectionDetector.kt new file mode 100644 index 00000000..c354cbb9 --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/gestures/direction/SwipeDirectionDetector.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.photoview.dialog.common.gestures.direction + +import android.content.Context +import android.view.MotionEvent +import android.view.ViewConfiguration +import kotlin.math.atan2 +import kotlin.math.sqrt + +internal class SwipeDirectionDetector( + context: Context, + private val onDirectionDetected: (SwipeDirection) -> Unit +) { + + private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop + private var startX: Float = 0f + private var startY: Float = 0f + private var isDetected: Boolean = false + + fun handleTouchEvent(event: MotionEvent) { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + startX = event.x + startY = event.y + } + + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { + if (!isDetected) { + onDirectionDetected(SwipeDirection.NOT_DETECTED) + } + startY = 0.0f + startX = startY + isDetected = false + } + + MotionEvent.ACTION_MOVE -> if (!isDetected && getEventDistance(event) > touchSlop) { + isDetected = true + onDirectionDetected(getDirection(startX, startY, event.x, event.y)) + } + } + } + + /** + * Given two points in the plane p1=(x1, x2) and p2=(y1, y1), this method + * returns the direction that an arrow pointing from p1 to p2 would have. + * + * @param x1 the x position of the first point + * @param y1 the y position of the first point + * @param x2 the x position of the second point + * @param y2 the y position of the second point + * @return the direction + */ + private fun getDirection(x1: Float, y1: Float, x2: Float, y2: Float): SwipeDirection { + val angle = getAngle(x1, y1, x2, y2) + return SwipeDirection.fromAngle(angle) + } + + /** + * Finds the angle between two points in the plane (x1,y1) and (x2, y2) + * The angle is measured with 0/360 being the X-axis to the right, angles + * increase counter clockwise. + * + * @param x1 the x position of the first point + * @param y1 the y position of the first point + * @param x2 the x position of the second point + * @param y2 the y position of the second point + * @return the angle between two points + */ + private fun getAngle(x1: Float, y1: Float, x2: Float, y2: Float): Double { + val rad = atan2((y1 - y2).toDouble(), (x2 - x1).toDouble()) + Math.PI + return (rad * 180 / Math.PI + 180) % 360 + } + + private fun getEventDistance(ev: MotionEvent): Float { + val dx = ev.getX(0) - startX + val dy = ev.getY(0) - startY + return sqrt((dx * dx + dy * dy).toDouble()).toFloat() + } +} \ No newline at end of file diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/gestures/dismiss/SwipeToDismissHandler.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/gestures/dismiss/SwipeToDismissHandler.kt new file mode 100644 index 00000000..4e8975dd --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/gestures/dismiss/SwipeToDismissHandler.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.photoview.dialog.common.gestures.dismiss + +import android.annotation.SuppressLint +import android.view.MotionEvent +import android.view.View +import android.view.animation.AccelerateInterpolator +import io.getstream.photoview.dialog.common.extensions.hitRect +import io.getstream.photoview.dialog.common.extensions.setAnimatorListener + +internal class SwipeToDismissHandler( + private val swipeView: View, + private val onDismiss: () -> Unit, + private val onSwipeViewMove: (translationY: Float, translationLimit: Int) -> Unit, + private val shouldAnimateDismiss: () -> Boolean +) : View.OnTouchListener { + + companion object { + private const val ANIMATION_DURATION = 200L + } + + private var translationLimit: Int = swipeView.height / 4 + private var isTracking = false + private var startY: Float = 0f + + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(v: View, event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + if (swipeView.hitRect.contains(event.x.toInt(), event.y.toInt())) { + isTracking = true + } + startY = event.y + return true + } + + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + if (isTracking) { + isTracking = false + onTrackingEnd(v.height) + } + return true + } + + MotionEvent.ACTION_MOVE -> { + if (isTracking) { + val translationY = event.y - startY + swipeView.translationY = translationY + onSwipeViewMove(translationY, translationLimit) + } + return true + } + + else -> { + return false + } + } + } + + internal fun initiateDismissToBottom() { + animateTranslation(swipeView.height.toFloat()) + } + + private fun onTrackingEnd(parentHeight: Int) { + val animateTo = when { + swipeView.translationY < -translationLimit -> -parentHeight.toFloat() + swipeView.translationY > translationLimit -> parentHeight.toFloat() + else -> 0f + } + + if (animateTo != 0f && !shouldAnimateDismiss()) { + onDismiss() + } else { + animateTranslation(animateTo) + } + } + + private fun animateTranslation(translationTo: Float) { + swipeView.animate() + .translationY(translationTo) + .setDuration(ANIMATION_DURATION) + .setInterpolator(AccelerateInterpolator()) + .setUpdateListener { onSwipeViewMove(swipeView.translationY, translationLimit) } + .setAnimatorListener(onAnimationEnd = { + if (translationTo != 0f) { + onDismiss() + } + + //remove the update listener, otherwise it will be saved on the next animation execution: + swipeView.animate().setUpdateListener(null) + }) + .start() + } +} \ No newline at end of file diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/pager/MultiTouchViewPager.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/pager/MultiTouchViewPager.kt new file mode 100644 index 00000000..8b0af745 --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/pager/MultiTouchViewPager.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.photoview.dialog.common.pager + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import androidx.viewpager.widget.ViewPager +import io.getstream.photoview.dialog.common.extensions.addOnPageChangeListener + +internal class MultiTouchViewPager @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : ViewPager(context, attrs) { + + internal var isIdle = true + private set + + private var isInterceptionDisallowed: Boolean = false + private var pageChangeListener: OnPageChangeListener? = null + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + pageChangeListener = addOnPageChangeListener( + onPageScrollStateChanged = ::onPageScrollStateChanged + ) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + pageChangeListener?.let { removeOnPageChangeListener(it) } + } + + override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { + isInterceptionDisallowed = disallowIntercept + super.requestDisallowInterceptTouchEvent(disallowIntercept) + } + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + return if (ev.pointerCount > 1 && isInterceptionDisallowed) { + requestDisallowInterceptTouchEvent(false) + val handled = super.dispatchTouchEvent(ev) + requestDisallowInterceptTouchEvent(true) + handled + } else { + super.dispatchTouchEvent(ev) + } + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + return if (ev.pointerCount > 1) { + false + } else { + try { + super.onInterceptTouchEvent(ev) + } catch (ex: IllegalArgumentException) { + false + } + } + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(ev: MotionEvent): Boolean { + return try { + super.onTouchEvent(ev) + } catch (ex: IllegalArgumentException) { + false + } + } + + private fun onPageScrollStateChanged(state: Int) { + isIdle = state == SCROLL_STATE_IDLE + } +} \ No newline at end of file diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/pager/RecyclingPagerAdapter.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/pager/RecyclingPagerAdapter.kt new file mode 100644 index 00000000..3929783c --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/common/pager/RecyclingPagerAdapter.kt @@ -0,0 +1,147 @@ +package io.getstream.photoview.dialog.common.pager + +import android.os.Bundle +import android.os.Parcelable +import android.util.SparseArray +import android.view.View +import android.view.ViewGroup +import androidx.viewpager.widget.PagerAdapter +import io.getstream.photoview.dialog.common.extensions.forEach + +internal abstract class RecyclingPagerAdapter + : PagerAdapter() { + + companion object { + private val STATE = RecyclingPagerAdapter::class.java.simpleName + private const val VIEW_TYPE_IMAGE = 0 + } + + internal abstract fun getItemCount(): Int + internal abstract fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH + internal abstract fun onBindViewHolder(holder: VH, position: Int) + + private val typeCaches = SparseArray() + private var savedStates = SparseArray() + + override fun destroyItem(parent: ViewGroup, position: Int, item: Any) { + if (item is ViewHolder) { + item.detach(parent) + } + } + + override fun getCount() = getItemCount() + + override fun getItemPosition(item: Any) = POSITION_NONE + + @Suppress("UNCHECKED_CAST") + override fun instantiateItem(parent: ViewGroup, position: Int): Any { + var cache = typeCaches.get(VIEW_TYPE_IMAGE) + + if (cache == null) { + cache = RecycleCache(this) + typeCaches.put(VIEW_TYPE_IMAGE, cache) + } + + return cache.getFreeViewHolder(parent, VIEW_TYPE_IMAGE) + .apply { + attach(parent, position) + onBindViewHolder(this as VH, position) + onRestoreInstanceState(savedStates.get(getItemId(position))) + } + } + + override fun isViewFromObject(view: View, obj: Any): Boolean = + obj is ViewHolder && obj.itemView === view + + override fun saveState(): Parcelable? { + for (viewHolder in getAttachedViewHolders()) { + savedStates.put(getItemId(viewHolder.position), viewHolder.onSaveInstanceState()) + } + return Bundle().apply { putSparseParcelableArray(STATE, savedStates) } + } + + override fun restoreState(state: Parcelable?, loader: ClassLoader?) { + if (state != null && state is Bundle) { + state.classLoader = loader + val sparseArray: SparseArray? = state.getSparseParcelableArray(STATE) + savedStates = sparseArray ?: SparseArray() + } + super.restoreState(state, loader) + } + + private fun getItemId(position: Int) = position + + private fun getAttachedViewHolders(): List { + val attachedViewHolders = ArrayList() + + typeCaches.forEach { _, value -> + value.caches.forEach { holder -> + if (holder.isAttached) { + attachedViewHolders.add(holder) + } + } + } + + return attachedViewHolders + } + + private class RecycleCache internal constructor( + private val adapter: RecyclingPagerAdapter<*> + ) { + + internal val caches = mutableListOf() + + internal fun getFreeViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + var iterationsCount = 0 + var viewHolder: ViewHolder + + while (iterationsCount < caches.size) { + viewHolder = caches[iterationsCount] + if (!viewHolder.isAttached) { + return viewHolder + } + iterationsCount++ + } + + return adapter.onCreateViewHolder(parent, viewType).also { caches.add(it) } + } + } + + internal abstract class ViewHolder(internal val itemView: View) { + + companion object { + private val STATE = ViewHolder::class.java.simpleName + } + + internal var position: Int = 0 + internal var isAttached: Boolean = false + + internal fun attach(parent: ViewGroup, position: Int) { + this.isAttached = true + this.position = position + parent.addView(itemView) + } + + internal fun detach(parent: ViewGroup) { + parent.removeView(itemView) + isAttached = false + } + + internal fun onRestoreInstanceState(state: Parcelable?) { + getStateFromParcelable(state)?.let { itemView.restoreHierarchyState(it) } + } + + internal fun onSaveInstanceState(): Parcelable { + val state = SparseArray() + itemView.saveHierarchyState(state) + return Bundle().apply { putSparseParcelableArray(STATE, state) } + } + + private fun getStateFromParcelable(state: Parcelable?): SparseArray? { + if (state != null && state is Bundle && state.containsKey(STATE)) { + return state.getSparseParcelableArray(STATE) + } + return null + } + } +} \ No newline at end of file diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/listeners/OnDismissListener.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/listeners/OnDismissListener.kt new file mode 100644 index 00000000..c3fd5b11 --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/listeners/OnDismissListener.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getstream.photoview.dialog.listeners + +/** + * Interface definition for a callback to be invoked when + * [io.getstream.photoview.dialog.PhotoViewDialog] was dismissed. + */ +//N.B.! This class is written in Java for convenient use of lambdas due to languages compatibility issues. +public fun interface OnDismissListener { + + public fun onDismiss() +} diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/listeners/OnImageChangeListener.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/listeners/OnImageChangeListener.kt new file mode 100644 index 00000000..a8ec10f4 --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/listeners/OnImageChangeListener.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getstream.photoview.dialog.listeners + +/** + * Interface definition for a callback to be invoked when current image position was changed. + */ +//N.B.! This class is written in Java for convenient use of lambdas due to languages compatibility issues. +public fun interface OnImageChangeListener { + + public fun onImageChange(position: Int) +} \ No newline at end of file diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/loader/ImageLoader.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/loader/ImageLoader.kt new file mode 100644 index 00000000..926573a6 --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/loader/ImageLoader.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getstream.photoview.dialog.loader + +import android.widget.ImageView + +/** + * Interface definition for a callback to be invoked when image should be loaded + */ +//N.B.! This class is written in Java for convenient use of lambdas due to languages compatibility issues. +public fun interface ImageLoader { + /** + * Fires every time when image object should be displayed in a provided [ImageView] + * + * @param imageView an [ImageView] object where the image should be loaded + * @param image image data from which image should be loaded + */ + public fun loadImage(imageView: ImageView, image: T) +} diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/viewer/adapter/ImagesPagerAdapter.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/viewer/adapter/ImagesPagerAdapter.kt new file mode 100644 index 00000000..c0a9df23 --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/viewer/adapter/ImagesPagerAdapter.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.photoview.dialog.viewer.adapter + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import io.getstream.photoview.PhotoView +import io.getstream.photoview.dialog.common.extensions.resetScale +import io.getstream.photoview.dialog.common.pager.RecyclingPagerAdapter +import io.getstream.photoview.dialog.loader.ImageLoader + +internal class ImagesPagerAdapter( + private val context: Context, + _images: List, + private val imageLoader: ImageLoader, + private val isZoomingAllowed: Boolean +) : RecyclingPagerAdapter.ViewHolder>() { + + private var images = _images + private val holders = mutableListOf() + + fun isScaled(position: Int): Boolean = + holders.firstOrNull { it.position == position }?.isScaled ?: false + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val photoView = PhotoView(context).apply { + isEnabled = isZoomingAllowed + setOnViewDragListener { _, _ -> setAllowParentInterceptOnEdge(scale == 1.0f) } + } + + return ViewHolder(photoView).also { holders.add(it) } + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(position) + + override fun getItemCount() = images.size + + internal fun updateImages(images: List) { + this.images = images + notifyDataSetChanged() + } + + internal fun resetScale(position: Int) = + holders.firstOrNull { it.position == position }?.resetScale() + + internal inner class ViewHolder(itemView: View) : RecyclingPagerAdapter.ViewHolder(itemView) { + + internal val isScaled: Boolean + get() = photoView.scale > 1f + + private val photoView: PhotoView = itemView as PhotoView + + fun bind(position: Int) { + this.position = position + imageLoader.loadImage(photoView, images[position]) + } + + fun resetScale() = photoView.resetScale(animate = true) + } +} \ No newline at end of file diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/viewer/builder/BuilderData.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/viewer/builder/BuilderData.kt new file mode 100644 index 00000000..2e3dae60 --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/viewer/builder/BuilderData.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.photoview.dialog.viewer.builder + +import android.graphics.Color +import android.view.View +import android.widget.ImageView +import io.getstream.photoview.dialog.listeners.OnDismissListener +import io.getstream.photoview.dialog.listeners.OnImageChangeListener +import io.getstream.photoview.dialog.loader.ImageLoader + +public class BuilderData( + public val images: List, + public val imageLoader: ImageLoader +) { + public var backgroundColor: Int = Color.BLACK + public var startPosition: Int = 0 + public var imageChangeListener: OnImageChangeListener? = null + public var onDismissListener: OnDismissListener? = null + public var overlayView: View? = null + public var imageMarginPixels: Int = 0 + public var containerPaddingPixels: IntArray = IntArray(4) + public var shouldStatusBarHide: Boolean = true + public var isZoomingAllowed: Boolean = true + public var isSwipeToDismissAllowed: Boolean = true + public var transitionView: ImageView? = null +} \ No newline at end of file diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/viewer/dialog/ImageViewerDialog.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/viewer/dialog/ImageViewerDialog.kt new file mode 100644 index 00000000..342ef615 --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/viewer/dialog/ImageViewerDialog.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.photoview.dialog.viewer.dialog + +import android.content.Context +import android.view.KeyEvent +import android.widget.ImageView +import androidx.appcompat.app.AlertDialog +import io.getstream.photoview.dialog.R +import io.getstream.photoview.dialog.viewer.builder.BuilderData +import io.getstream.photoview.dialog.viewer.view.ImageViewerView + +internal class ImageViewerDialog( + context: Context, + private val builderData: BuilderData +) { + + private val dialog: AlertDialog + private val viewerView: ImageViewerView = ImageViewerView(context) + private var animateOpen = true + + private val dialogStyle: Int + get() = if (builderData.shouldStatusBarHide) + R.style.ImageViewerDialog_NoStatusBar + else + R.style.ImageViewerDialog_Default + + init { + setupViewerView() + dialog = AlertDialog + .Builder(context, dialogStyle) + .setView(viewerView) + .setOnKeyListener { _, keyCode, event -> onDialogKeyEvent(keyCode, event) } + .create() + .apply { + setOnShowListener { viewerView.open(builderData.transitionView, animateOpen) } + setOnDismissListener { builderData.onDismissListener?.onDismiss() } + } + } + + fun show(animate: Boolean) { + animateOpen = animate + dialog.show() + } + + fun close() { + viewerView.close() + } + + fun dismiss() { + dialog.dismiss() + } + + fun updateImages(images: List) { + viewerView.updateImages(images) + } + + fun getCurrentPosition(): Int = + viewerView.currentPosition + + fun setCurrentPosition(position: Int): Int { + viewerView.currentPosition = position + return viewerView.currentPosition + } + + fun updateTransitionImage(imageView: ImageView?) { + viewerView.updateTransitionImage(imageView) + } + + private fun onDialogKeyEvent(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK && + event.action == KeyEvent.ACTION_UP && + !event.isCanceled + ) { + if (viewerView.isScaled) { + viewerView.resetScale() + } else { + viewerView.close() + } + return true + } + return false + } + + private fun setupViewerView() { + viewerView.apply { + isZoomingAllowed = builderData.isZoomingAllowed + isSwipeToDismissAllowed = builderData.isSwipeToDismissAllowed + + containerPadding = builderData.containerPaddingPixels + imagesMargin = builderData.imageMarginPixels + overlayView = builderData.overlayView + + setBackgroundColor(builderData.backgroundColor) + setImages(builderData.images, builderData.startPosition, builderData.imageLoader) + + onPageChange = { position -> builderData.imageChangeListener?.onImageChange(position) } + onDismiss = { dialog.dismiss() } + } + } +} \ No newline at end of file diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/viewer/view/ImageViewerView.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/viewer/view/ImageViewerView.kt new file mode 100644 index 00000000..b5055ecb --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/viewer/view/ImageViewerView.kt @@ -0,0 +1,362 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.photoview.dialog.viewer.view + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.ScaleGestureDetector +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.RelativeLayout +import androidx.core.view.GestureDetectorCompat +import io.getstream.photoview.dialog.R +import io.getstream.photoview.dialog.common.extensions.addOnPageChangeListener +import io.getstream.photoview.dialog.common.extensions.animateAlpha +import io.getstream.photoview.dialog.common.extensions.applyMargin +import io.getstream.photoview.dialog.common.extensions.copyBitmapFrom +import io.getstream.photoview.dialog.common.extensions.isRectVisible +import io.getstream.photoview.dialog.common.extensions.isVisible +import io.getstream.photoview.dialog.common.extensions.makeGone +import io.getstream.photoview.dialog.common.extensions.makeInvisible +import io.getstream.photoview.dialog.common.extensions.makeVisible +import io.getstream.photoview.dialog.common.extensions.switchVisibilityWithAnimation +import io.getstream.photoview.dialog.common.gestures.detector.SimpleOnGestureListener +import io.getstream.photoview.dialog.common.gestures.direction.SwipeDirection +import io.getstream.photoview.dialog.common.gestures.direction.SwipeDirection.DOWN +import io.getstream.photoview.dialog.common.gestures.direction.SwipeDirection.LEFT +import io.getstream.photoview.dialog.common.gestures.direction.SwipeDirection.RIGHT +import io.getstream.photoview.dialog.common.gestures.direction.SwipeDirection.UP +import io.getstream.photoview.dialog.common.gestures.direction.SwipeDirectionDetector +import io.getstream.photoview.dialog.common.gestures.dismiss.SwipeToDismissHandler +import io.getstream.photoview.dialog.common.pager.MultiTouchViewPager +import io.getstream.photoview.dialog.loader.ImageLoader +import io.getstream.photoview.dialog.viewer.adapter.ImagesPagerAdapter + +internal class ImageViewerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : RelativeLayout(context, attrs, defStyleAttr) { + + internal var isZoomingAllowed = true + internal var isSwipeToDismissAllowed = true + + internal var currentPosition: Int + get() = imagesPager.currentItem + set(value) { + imagesPager.currentItem = value + } + + internal var onDismiss: (() -> Unit)? = null + internal var onPageChange: ((position: Int) -> Unit)? = null + + internal val isScaled + get() = imagesAdapter?.isScaled(currentPosition) ?: false + + internal var containerPadding = intArrayOf(0, 0, 0, 0) + + internal var imagesMargin + get() = imagesPager.pageMargin + set(value) { + imagesPager.pageMargin = value + } + + internal var overlayView: View? = null + set(value) { + field = value + value?.let { rootContainer.addView(it) } + } + + private var rootContainer: ViewGroup + private var backgroundView: View + private var dismissContainer: ViewGroup + + private val transitionImageContainer: FrameLayout + private val transitionImageView: ImageView + private var externalTransitionImageView: ImageView? = null + + private var imagesPager: MultiTouchViewPager + private var imagesAdapter: ImagesPagerAdapter? = null + + private var directionDetector: SwipeDirectionDetector + private var gestureDetector: GestureDetectorCompat + private var scaleDetector: ScaleGestureDetector + private lateinit var swipeDismissHandler: SwipeToDismissHandler + + private var wasScaled: Boolean = false + private var wasDoubleTapped = false + private var isOverlayWasClicked: Boolean = false + private var swipeDirection: SwipeDirection? = null + + private var images: List = listOf() + private var imageLoader: ImageLoader? = null + private lateinit var transitionImageAnimator: TransitionImageAnimator + + private var startPosition: Int = 0 + set(value) { + field = value + currentPosition = value + } + + private val shouldDismissToBottom: Boolean + get() = externalTransitionImageView == null + || !externalTransitionImageView.isRectVisible + || !isAtStartPosition + + private val isAtStartPosition: Boolean + get() = currentPosition == startPosition + + init { + View.inflate(context, R.layout.view_image_viewer, this) + + rootContainer = findViewById(R.id.rootContainer) + backgroundView = findViewById(R.id.backgroundView) + dismissContainer = findViewById(R.id.dismissContainer) + + transitionImageContainer = findViewById(R.id.transitionImageContainer) + transitionImageView = findViewById(R.id.transitionImageView) + + imagesPager = findViewById(R.id.imagesPager) + imagesPager.addOnPageChangeListener( + onPageSelected = { + externalTransitionImageView?.apply { + if (isAtStartPosition) makeInvisible() else makeVisible() + } + onPageChange?.invoke(it) + }) + + directionDetector = createSwipeDirectionDetector() + gestureDetector = createGestureDetector() + scaleDetector = createScaleGestureDetector() + } + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + if (overlayView.isVisible && overlayView?.dispatchTouchEvent(event) == true) { + return true + } + + if (!this::transitionImageAnimator.isInitialized || transitionImageAnimator.isAnimating) { + return true + } + + //one more tiny kludge to prevent single tap a one-finger zoom which is broken by the SDK + if (wasDoubleTapped && + event.action == MotionEvent.ACTION_MOVE && + event.pointerCount == 1) { + return true + } + + handleUpDownEvent(event) + + if (swipeDirection == null && (scaleDetector.isInProgress || event.pointerCount > 1 || wasScaled)) { + wasScaled = true + return imagesPager.dispatchTouchEvent(event) + } + + return if (isScaled) super.dispatchTouchEvent(event) else handleTouchIfNotScaled(event) + } + + override fun setBackgroundColor(color: Int) { + findViewById(R.id.backgroundView).setBackgroundColor(color) + } + + internal fun setImages(images: List, startPosition: Int, imageLoader: ImageLoader) { + this.images = images + this.imageLoader = imageLoader + this.imagesAdapter = ImagesPagerAdapter(context, images, imageLoader, isZoomingAllowed) + this.imagesPager.adapter = imagesAdapter + this.startPosition = startPosition + } + + internal fun open(transitionImageView: ImageView?, animate: Boolean) { + prepareViewsForTransition() + + externalTransitionImageView = transitionImageView + + imageLoader?.loadImage(this.transitionImageView, images[startPosition]) + this.transitionImageView.copyBitmapFrom(transitionImageView) + + transitionImageAnimator = createTransitionImageAnimator(transitionImageView) + swipeDismissHandler = createSwipeToDismissHandler() + rootContainer.setOnTouchListener(swipeDismissHandler) + + if (animate) animateOpen() else prepareViewsForViewer() + } + + internal fun close() { + if (shouldDismissToBottom) { + swipeDismissHandler.initiateDismissToBottom() + } else { + animateClose() + } + } + + internal fun updateImages(images: List) { + this.images = images + imagesAdapter?.updateImages(images) + } + + internal fun updateTransitionImage(imageView: ImageView?) { + externalTransitionImageView?.makeVisible() + imageView?.makeInvisible() + + externalTransitionImageView = imageView + startPosition = currentPosition + transitionImageAnimator = createTransitionImageAnimator(imageView) + imageLoader?.loadImage(transitionImageView, images[startPosition]) + } + + internal fun resetScale() { + imagesAdapter?.resetScale(currentPosition) + } + + private fun animateOpen() { + transitionImageAnimator.animateOpen( + containerPadding = containerPadding, + onTransitionStart = { duration -> + backgroundView.animateAlpha(0f, 1f, duration) + overlayView?.animateAlpha(0f, 1f, duration) + }, + onTransitionEnd = { prepareViewsForViewer() }) + } + + private fun animateClose() { + prepareViewsForTransition() + dismissContainer.applyMargin(0, 0, 0, 0) + + transitionImageAnimator.animateClose( + shouldDismissToBottom = shouldDismissToBottom, + onTransitionStart = { duration -> + backgroundView.animateAlpha(backgroundView.alpha, 0f, duration) + overlayView?.animateAlpha(overlayView?.alpha, 0f, duration) + }, + onTransitionEnd = { onDismiss?.invoke() }) + } + + private fun prepareViewsForTransition() { + transitionImageContainer.makeVisible() + imagesPager.makeGone() + } + + private fun prepareViewsForViewer() { + backgroundView.alpha = 1f + transitionImageContainer.makeGone() + imagesPager.makeVisible() + } + + private fun handleTouchIfNotScaled(event: MotionEvent): Boolean { + directionDetector.handleTouchEvent(event) + + return when (swipeDirection) { + UP, DOWN -> { + if (isSwipeToDismissAllowed && !wasScaled && imagesPager.isIdle) { + swipeDismissHandler.onTouch(rootContainer, event) + } else true + } + LEFT, RIGHT -> { + imagesPager.dispatchTouchEvent(event) + } + else -> true + } + } + + private fun handleUpDownEvent(event: MotionEvent) { + if (event.action == MotionEvent.ACTION_UP) { + handleEventActionUp(event) + } + + if (event.action == MotionEvent.ACTION_DOWN) { + handleEventActionDown(event) + } + + scaleDetector.onTouchEvent(event) + gestureDetector.onTouchEvent(event) + } + + private fun handleEventActionDown(event: MotionEvent) { + swipeDirection = null + wasScaled = false + imagesPager.dispatchTouchEvent(event) + + swipeDismissHandler.onTouch(rootContainer, event) + isOverlayWasClicked = dispatchOverlayTouch(event) + } + + private fun handleEventActionUp(event: MotionEvent) { + wasDoubleTapped = false + swipeDismissHandler.onTouch(rootContainer, event) + imagesPager.dispatchTouchEvent(event) + isOverlayWasClicked = dispatchOverlayTouch(event) + } + + private fun handleSingleTap(event: MotionEvent, isOverlayWasClicked: Boolean) { + if (overlayView != null && !isOverlayWasClicked) { + overlayView?.switchVisibilityWithAnimation() + super.dispatchTouchEvent(event) + } + } + + private fun handleSwipeViewMove(translationY: Float, translationLimit: Int) { + val alpha = calculateTranslationAlpha(translationY, translationLimit) + backgroundView.alpha = alpha + overlayView?.alpha = alpha + } + + private fun dispatchOverlayTouch(event: MotionEvent): Boolean = + overlayView + ?.let { it.isVisible && it.dispatchTouchEvent(event) } + ?: false + + private fun calculateTranslationAlpha(translationY: Float, translationLimit: Int): Float = + 1.0f - 1.0f / translationLimit.toFloat() / 4f * Math.abs(translationY) + + private fun createSwipeDirectionDetector() = + SwipeDirectionDetector(context) { swipeDirection = it } + + private fun createGestureDetector() = + GestureDetectorCompat(context, SimpleOnGestureListener( + onSingleTap = { + if (imagesPager.isIdle) { + handleSingleTap(it, isOverlayWasClicked) + } + false + }, + onDoubleTap = { + wasDoubleTapped = !isScaled + false + } + )) + + private fun createScaleGestureDetector() = + ScaleGestureDetector(context, ScaleGestureDetector.SimpleOnScaleGestureListener()) + + private fun createSwipeToDismissHandler() + : SwipeToDismissHandler = SwipeToDismissHandler( + swipeView = dismissContainer, + shouldAnimateDismiss = { shouldDismissToBottom }, + onDismiss = { animateClose() }, + onSwipeViewMove = ::handleSwipeViewMove) + + private fun createTransitionImageAnimator(transitionImageView: ImageView?) = + TransitionImageAnimator( + externalImage = transitionImageView, + internalImage = this.transitionImageView, + internalImageContainer = this.transitionImageContainer) +} \ No newline at end of file diff --git a/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/viewer/view/TransitionImageAnimator.kt b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/viewer/view/TransitionImageAnimator.kt new file mode 100644 index 00000000..928ca50a --- /dev/null +++ b/photoview-dialog/src/main/kotlin/io/getstream/photoview/dialog/viewer/view/TransitionImageAnimator.kt @@ -0,0 +1,161 @@ +/* + * Copyright 2018 stfalcon.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.photoview.dialog.viewer.view + +import android.transition.AutoTransition +import android.transition.Transition +import android.transition.TransitionManager +import android.view.View +import android.view.ViewGroup +import android.view.animation.DecelerateInterpolator +import android.widget.FrameLayout +import android.widget.ImageView +import io.getstream.photoview.dialog.common.extensions.addListener +import io.getstream.photoview.dialog.common.extensions.applyMargin +import io.getstream.photoview.dialog.common.extensions.globalVisibleRect +import io.getstream.photoview.dialog.common.extensions.isRectVisible +import io.getstream.photoview.dialog.common.extensions.localVisibleRect +import io.getstream.photoview.dialog.common.extensions.makeViewMatchParent +import io.getstream.photoview.dialog.common.extensions.postApply +import io.getstream.photoview.dialog.common.extensions.postDelayed +import io.getstream.photoview.dialog.common.extensions.requestNewSize + +internal class TransitionImageAnimator( + private val externalImage: ImageView?, + private val internalImage: ImageView, + private val internalImageContainer: FrameLayout +) { + + companion object { + private const val TRANSITION_DURATION_OPEN = 200L + private const val TRANSITION_DURATION_CLOSE = 250L + } + + internal var isAnimating = false + + private var isClosing = false + + private val transitionDuration: Long + get() = if (isClosing) TRANSITION_DURATION_CLOSE else TRANSITION_DURATION_OPEN + + private val internalRoot: ViewGroup + get() = internalImageContainer.parent as ViewGroup + + internal fun animateOpen( + containerPadding: IntArray, + onTransitionStart: (Long) -> Unit, + onTransitionEnd: () -> Unit + ) { + if (externalImage.isRectVisible) { + onTransitionStart(TRANSITION_DURATION_OPEN) + doOpenTransition(containerPadding, onTransitionEnd) + } else { + onTransitionEnd() + } + } + + internal fun animateClose( + shouldDismissToBottom: Boolean, + onTransitionStart: (Long) -> Unit, + onTransitionEnd: () -> Unit + ) { + if (externalImage.isRectVisible && !shouldDismissToBottom) { + onTransitionStart(TRANSITION_DURATION_CLOSE) + doCloseTransition(onTransitionEnd) + } else { + externalImage?.visibility = View.VISIBLE + onTransitionEnd() + } + } + + private fun doOpenTransition(containerPadding: IntArray, onTransitionEnd: () -> Unit) { + isAnimating = true + prepareTransitionLayout() + + internalRoot.postApply { + //ain't nothing but a kludge to prevent blinking when transition is starting + externalImage?.postDelayed(50) { visibility = View.INVISIBLE } + + TransitionManager.beginDelayedTransition(internalRoot, createTransition { + if (!isClosing) { + isAnimating = false + onTransitionEnd() + } + }) + + internalImageContainer.makeViewMatchParent() + internalImage.makeViewMatchParent() + + internalRoot.applyMargin( + containerPadding[0], + containerPadding[1], + containerPadding[2], + containerPadding[3] + ) + + internalImageContainer.requestLayout() + } + } + + private fun doCloseTransition(onTransitionEnd: () -> Unit) { + isAnimating = true + isClosing = true + + TransitionManager.beginDelayedTransition( + internalRoot, createTransition { handleCloseTransitionEnd(onTransitionEnd) }) + + prepareTransitionLayout() + internalImageContainer.requestLayout() + } + + private fun prepareTransitionLayout() { + externalImage?.let { + if (externalImage.isRectVisible) { + with(externalImage.localVisibleRect) { + internalImage.requestNewSize(it.width, it.height) + internalImage.applyMargin(top = -top, start = -left) + } + with(externalImage.globalVisibleRect) { + internalImageContainer.requestNewSize(width(), height()) + internalImageContainer.applyMargin(left, top, right, bottom) + } + } + + resetRootTranslation() + } + } + + private fun handleCloseTransitionEnd(onTransitionEnd: () -> Unit) { + externalImage?.visibility = View.VISIBLE + internalImage.post { onTransitionEnd() } + isAnimating = false + } + + private fun resetRootTranslation() { + internalRoot + .animate() + .translationY(0f) + .setDuration(transitionDuration) + .start() + } + + private fun createTransition(onTransitionEnd: (() -> Unit)? = null): Transition = + AutoTransition() + .setDuration(transitionDuration) + .setInterpolator(DecelerateInterpolator()) + .addListener(onTransitionEnd = { onTransitionEnd?.invoke() }) +} \ No newline at end of file diff --git a/photoview-dialog/src/main/res/layout/view_image_viewer.xml b/photoview-dialog/src/main/res/layout/view_image_viewer.xml new file mode 100644 index 00000000..9bbd116e --- /dev/null +++ b/photoview-dialog/src/main/res/layout/view_image_viewer.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/photoview-dialog/src/main/res/values/styles.xml b/photoview-dialog/src/main/res/values/styles.xml new file mode 100644 index 00000000..5c467c7d --- /dev/null +++ b/photoview-dialog/src/main/res/values/styles.xml @@ -0,0 +1,20 @@ + + + + + + + + + + \ No newline at end of file diff --git a/sample/api/sample.api b/sample/api/sample.api index 759c39ba..cafb2bac 100644 --- a/sample/api/sample.api +++ b/sample/api/sample.api @@ -70,6 +70,10 @@ public final class io/getstream/photoview/sample/LauncherActivity$Companion { public final fun getOptions ()[Ljava/lang/String; } +public final class io/getstream/photoview/sample/PhotoViewDialogActivity : androidx/appcompat/app/AppCompatActivity { + public fun ()V +} + public final class io/getstream/photoview/sample/RotationSampleActivity : androidx/appcompat/app/AppCompatActivity { public fun ()V public fun onCreate (Landroid/os/Bundle;)V @@ -112,6 +116,16 @@ public final class io/getstream/photoview/sample/databinding/ActivityLauncherBin public static fun inflate (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Z)Lio/getstream/photoview/sample/databinding/ActivityLauncherBinding; } +public final class io/getstream/photoview/sample/databinding/ActivityPhotoviewDialogBinding : androidx/viewbinding/ViewBinding { + public final field button Landroid/widget/Button; + public final field ivPhoto Landroidx/constraintlayout/widget/ConstraintLayout; + public static fun bind (Landroid/view/View;)Lio/getstream/photoview/sample/databinding/ActivityPhotoviewDialogBinding; + public synthetic fun getRoot ()Landroid/view/View; + public fun getRoot ()Landroidx/constraintlayout/widget/ConstraintLayout; + public static fun inflate (Landroid/view/LayoutInflater;)Lio/getstream/photoview/sample/databinding/ActivityPhotoviewDialogBinding; + public static fun inflate (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Z)Lio/getstream/photoview/sample/databinding/ActivityPhotoviewDialogBinding; +} + public final class io/getstream/photoview/sample/databinding/ActivityRotationSampleBinding : androidx/viewbinding/ViewBinding { public final field appbar Lcom/google/android/material/appbar/AppBarLayout; public final field ivPhoto Lio/getstream/photoview/PhotoView; diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index f50f6399..8f2393a8 100755 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { implementation(libs.glide) implementation(project(":photoview")) + implementation(project(":photoview-dialog")) baselineProfile(project(":benchmark")) } diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 8dc6716d..c7f671b8 100755 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -16,39 +16,42 @@ limitations under the License. --> - - - - - - - - - - - - - - - - - - - - - - + xmlns:tools="http://schemas.android.com/tools" + package="io.getstream.photoview.sample"> + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/src/main/kotlin/io/getstream/photoview/sample/LauncherActivity.kt b/sample/src/main/kotlin/io/getstream/photoview/sample/LauncherActivity.kt index ca527107..cce5cdf1 100755 --- a/sample/src/main/kotlin/io/getstream/photoview/sample/LauncherActivity.kt +++ b/sample/src/main/kotlin/io/getstream/photoview/sample/LauncherActivity.kt @@ -69,6 +69,7 @@ class LauncherActivity : AppCompatActivity() { 4 -> GlideSampleActivity::class.java 5 -> ActivityTransitionActivity::class.java 6 -> ImmersiveActivity::class.java + 7 -> PhotoViewDialogActivity::class.java else -> SimpleSampleActivity::class.java } val context = holder.itemView.context @@ -115,6 +116,7 @@ class LauncherActivity : AppCompatActivity() { "Glide Sample", "Activity Transition Sample", "Immersive Sample", + "PhotoView Dialog", ) } } diff --git a/sample/src/main/kotlin/io/getstream/photoview/sample/PhotoViewDialogActivity.kt b/sample/src/main/kotlin/io/getstream/photoview/sample/PhotoViewDialogActivity.kt new file mode 100644 index 00000000..7d225d41 --- /dev/null +++ b/sample/src/main/kotlin/io/getstream/photoview/sample/PhotoViewDialogActivity.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2024 Stream.IO, Inc. + * Copyright 2011, 2012 Chris Banes. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getstream.photoview.sample + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.bumptech.glide.Glide +import io.getstream.photoview.dialog.PhotoViewDialog +import io.getstream.photoview.sample.databinding.ActivityPhotoviewDialogBinding + +class PhotoViewDialogActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val binding = ActivityPhotoviewDialogBinding.inflate(layoutInflater) + setContentView(binding.root) + + val urls = listOf( + "https://images.unsplash.com/photo-1577643816920-65b43ba99fba?ixlib=rb-1.2.1", + "https://images.unsplash.com/photo-1577643816920-65b43ba99fba?ixlib=rb-1.2.1", + "https://images.unsplash.com/photo-1577643816920-65b43ba99fba?ixlib=rb-1.2.1", + "https://images.unsplash.com/photo-1577643816920-65b43ba99fba?ixlib=rb-1.2.1", + "https://images.unsplash.com/photo-1577643816920-65b43ba99fba?ixlib=rb-1.2.1", + "https://images.unsplash.com/photo-1577643816920-65b43ba99fba?ixlib=rb-1.2.1", + ) + + val button = binding.button + button.setOnClickListener { + PhotoViewDialog.Builder(context = this, images = urls) { imageView, url -> + Glide.with(this) + .load(url) + .into(imageView) + }.build().show() + } + } +} diff --git a/sample/src/main/res/layout/activity_photoview_dialog.xml b/sample/src/main/res/layout/activity_photoview_dialog.xml new file mode 100755 index 00000000..e9adeac3 --- /dev/null +++ b/sample/src/main/res/layout/activity_photoview_dialog.xml @@ -0,0 +1,34 @@ + + + + +