Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow apps to provide customized ViewHolders #17

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.stfalcon.imageviewer.listeners.OnDismissListener;
import com.stfalcon.imageviewer.listeners.OnImageChangeListener;
import com.stfalcon.imageviewer.loader.ImageLoader;
import com.stfalcon.imageviewer.viewer.viewholder.ViewHolderLoader;
import com.stfalcon.imageviewer.viewer.builder.BuilderData;
import com.stfalcon.imageviewer.viewer.dialog.ImageViewerDialog;

Expand Down Expand Up @@ -127,11 +128,22 @@ public Builder(Context context, T[] images, ImageLoader<T> imageLoader) {
this(context, new ArrayList<>(Arrays.asList(images)), imageLoader);
}

public Builder(Context context, T[] images, ImageLoader<T> imageLoader,
ViewHolderLoader<T> viewHolderLoader) {
this(context, new ArrayList<>(Arrays.asList(images)), imageLoader, viewHolderLoader);
}

public Builder(Context context, List<T> images, ImageLoader<T> imageLoader) {
this.context = context;
this.data = new BuilderData<>(images, imageLoader);
}

public Builder(Context context, List<T> images, ImageLoader<T> imageLoader,
ViewHolderLoader<T> viewHolderLoader) {
this.context = context;
this.data = new BuilderData<>(images, imageLoader, viewHolderLoader);
}

/**
* Sets a position to start viewer from.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import android.view.ViewGroup
import androidx.viewpager.widget.PagerAdapter
import com.stfalcon.imageviewer.common.extensions.forEach

internal abstract class RecyclingPagerAdapter<VH : RecyclingPagerAdapter.ViewHolder>
abstract class RecyclingPagerAdapter<VH : RecyclingPagerAdapter.ViewHolder>
: PagerAdapter() {

companion object {
Expand Down Expand Up @@ -107,7 +107,7 @@ internal abstract class RecyclingPagerAdapter<VH : RecyclingPagerAdapter.ViewHol
}
}

internal abstract class ViewHolder(internal val itemView: View) {
abstract class ViewHolder(val itemView: View) {

companion object {
private val STATE = ViewHolder::class.java.simpleName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,42 @@ import android.content.Context
import android.view.View
import android.view.ViewGroup
import com.github.chrisbanes.photoview.PhotoView
import com.stfalcon.imageviewer.common.extensions.resetScale
import com.stfalcon.imageviewer.common.pager.RecyclingPagerAdapter
import com.stfalcon.imageviewer.loader.ImageLoader
import com.stfalcon.imageviewer.viewer.viewholder.ViewHolderLoader
import com.stfalcon.imageviewer.viewer.viewholder.DefaultViewHolder

internal class ImagesPagerAdapter<T>(
private val context: Context,
_images: List<T>,
private val imageLoader: ImageLoader<T>,
private val isZoomingAllowed: Boolean
) : RecyclingPagerAdapter<ImagesPagerAdapter<T>.ViewHolder>() {

private val isZoomingAllowed: Boolean,
private val viewHolderLoader: ViewHolderLoader<T>
) : RecyclingPagerAdapter<DefaultViewHolder<T>>() {
private var images = _images
private val holders = mutableListOf<ViewHolder>()
private val holders = mutableListOf<DefaultViewHolder<T>>()
private var primaryPos = -1

companion object {
val photoViewId = View.generateViewId()
}

fun isScaled(position: Int): Boolean =
holders.firstOrNull { it.position == position }?.isScaled ?: false
holders.firstOrNull { it.position == position }?.isScaled() ?: false

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DefaultViewHolder<T> {
val photoView = PhotoView(context).apply {
id = photoViewId
isEnabled = isZoomingAllowed
setOnViewDragListener { _, _ -> setAllowParentInterceptOnEdge(scale == 1.0f) }
}

return ViewHolder(photoView).also { holders.add(it) }
return viewHolderLoader.loadViewHolder(photoView).also {
it.imageLoader = imageLoader
holders.add(it) }
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(position)
override fun onBindViewHolder(holder: DefaultViewHolder<T>, position: Int) = holder.bind(position, images[position])

override fun getItemCount() = images.size

Expand All @@ -58,19 +67,15 @@ internal class ImagesPagerAdapter<T>(
internal fun resetScale(position: Int) =
holders.firstOrNull { it.position == position }?.resetScale()

internal inner class ViewHolder(itemView: View)
: RecyclingPagerAdapter.ViewHolder(itemView) {
fun onDialogClosed() = holders.forEach { it.onDialogClosed() }

internal var isScaled: Boolean = false
get() = photoView.scale > 1f
override fun setPrimaryItem(container: ViewGroup, position: Int, `object`: Any) {
super.setPrimaryItem(container, position, `object`)

private val photoView: PhotoView = itemView as PhotoView

fun bind(position: Int) {
this.position = position
imageLoader.loadImage(photoView, images[position])
// Only fire when the primary item has actually changed
if (position != primaryPos) {
primaryPos = position
holders.forEach { it.setIsVisible(it.position == primaryPos) }
}

fun resetScale() = photoView.resetScale(animate = true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import android.view.View
import android.widget.ImageView
import com.stfalcon.imageviewer.listeners.OnDismissListener
import com.stfalcon.imageviewer.listeners.OnImageChangeListener
import com.stfalcon.imageviewer.viewer.viewholder.DefaultViewHolderLoader
import com.stfalcon.imageviewer.loader.ImageLoader
import com.stfalcon.imageviewer.viewer.viewholder.ViewHolderLoader

internal class BuilderData<T>(
val images: List<T>,
Expand All @@ -38,4 +40,10 @@ internal class BuilderData<T>(
var isZoomingAllowed = true
var isSwipeToDismissAllowed = true
var transitionView: ImageView? = null
var viewHolderLoader: ViewHolderLoader<T>? = DefaultViewHolderLoader()

constructor(images: List<T>, imageLoader: ImageLoader<T>, viewHolderLoader: ViewHolderLoader<T>)
: this(images, imageLoader) {
this.viewHolderLoader = viewHolderLoader
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ internal class ImageViewerDialog<T>(
overlayView = builderData.overlayView

setBackgroundColor(builderData.backgroundColor)
setImages(builderData.images, builderData.startPosition, builderData.imageLoader)


setImages(builderData.images, builderData.startPosition, builderData.imageLoader,
builderData.viewHolderLoader)

onPageChange = { position -> builderData.imageChangeListener?.onImageChange(position) }
onDismiss = { dialog.dismiss() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ import com.stfalcon.imageviewer.common.gestures.direction.SwipeDirection.UP
import com.stfalcon.imageviewer.common.gestures.direction.SwipeDirectionDetector
import com.stfalcon.imageviewer.common.gestures.dismiss.SwipeToDismissHandler
import com.stfalcon.imageviewer.common.pager.MultiTouchViewPager
import com.stfalcon.imageviewer.viewer.viewholder.DefaultViewHolderLoader
import com.stfalcon.imageviewer.loader.ImageLoader
import com.stfalcon.imageviewer.viewer.viewholder.ViewHolderLoader
import com.stfalcon.imageviewer.viewer.adapter.ImagesPagerAdapter

internal class ImageViewerView<T> @JvmOverloads constructor(
Expand Down Expand Up @@ -177,10 +179,12 @@ internal class ImageViewerView<T> @JvmOverloads constructor(
findViewById<View>(R.id.backgroundView).setBackgroundColor(color)
}

internal fun setImages(images: List<T>, startPosition: Int, imageLoader: ImageLoader<T>) {
internal fun setImages(images: List<T>, startPosition: Int, imageLoader: ImageLoader<T>,
viewHolderLoader: ViewHolderLoader<T>?) {
this.images = images
this.imageLoader = imageLoader
this.imagesAdapter = ImagesPagerAdapter(context, images, imageLoader, isZoomingAllowed)
this.imagesAdapter = ImagesPagerAdapter(context, images, imageLoader, isZoomingAllowed,
viewHolderLoader ?: DefaultViewHolderLoader())
this.imagesPager.adapter = imagesAdapter
this.startPosition = startPosition
}
Expand Down Expand Up @@ -253,6 +257,7 @@ internal class ImageViewerView<T> @JvmOverloads constructor(
private fun prepareViewsForTransition() {
transitionImageContainer.makeVisible()
imagesPager.makeGone()
(imagesPager.adapter as? ImagesPagerAdapter<T>)?.onDialogClosed()
}

private fun prepareViewsForViewer() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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 com.stfalcon.imageviewer.viewer.viewholder

import android.view.View
import com.github.chrisbanes.photoview.PhotoView
import com.stfalcon.imageviewer.common.extensions.resetScale
import com.stfalcon.imageviewer.common.pager.RecyclingPagerAdapter
import com.stfalcon.imageviewer.loader.ImageLoader
import com.stfalcon.imageviewer.viewer.adapter.ImagesPagerAdapter


/**
* Subclasses may customize how the image pages are presented to the user.
* By overriding the constructor, subclasses may add additional views. The constructor is passed
* a zoomable PhotoView which is configured to behave well. Subclasses may customize this View,
* incorporate it into a more complex hierarchy, or ignore it altogether. The final View or
* ViewGroup must be passed to the super() constructor.
* By overriding the bind() method, subclasses may customize how images are loaded into views.
*/
open class DefaultViewHolder<T>(itemView: View)
: RecyclingPagerAdapter.ViewHolder(itemView) {

internal var imageLoader: ImageLoader<T>? = null

// If a subclass has incorporated the PhotoView into a ViewGroup, find it
// to ensure correct behavior by default
private var photoView: PhotoView? =
if (itemView is PhotoView) itemView
else itemView.findViewById(ImagesPagerAdapter.photoViewId)

// Subclasses should return True when they wish to handle Back button presses
// (e.g. when the image is zoomed in and should be un-zoomed when Back is pressed)
open fun isScaled() : Boolean = (photoView?.scale ?: 1f) > 1f

// Subclasses can respond to Back button presses here when isScaled() returns True
open fun resetScale() = photoView?.resetScale(animate = true)

open fun bind(position: Int, image: T) {
this.position = position
imageLoader?.loadImage(photoView, image)
}

// Subclasses may respond when the dialog window is closed (e.g. to stop video playback)
open fun onDialogClosed() {}

// Subclasses may respond when this ViewHolder's View moves on or off the screen
open fun setIsVisible(isVisible: Boolean) {}
}
Original file line number Diff line number Diff line change
@@ -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 com.stfalcon.imageviewer.viewer.viewholder

import com.github.chrisbanes.photoview.PhotoView


class DefaultViewHolderLoader<T> : ViewHolderLoader<T> {
override fun loadViewHolder(photoView: PhotoView) = DefaultViewHolder<T>(photoView)
}
Original file line number Diff line number Diff line change
@@ -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 com.stfalcon.imageviewer.viewer.viewholder;

import com.github.chrisbanes.photoview.PhotoView;

/**
* Interface definition for a callback to be invoked when a ViewHolder must be created.
*
* Applications can use their own subclasses of DefaultViewHolder by passing a ViewHolderLoader
* implementation to StfalconImageViewer.Builder that returns their DefaultViewHolder subclass.
*
* N.B.! This class is written in Java for convenient use of lambdas due to languages compatibility issues.
*/
public interface ViewHolderLoader<T> {

/**
* Fires every time a new ViewHolder should be created
*
* @param photoView a {@link PhotoView} object, configured to behave well
*/
DefaultViewHolder<T> loadViewHolder(PhotoView photoView);
}