From de59dda16e7e2a9067f7494fe341a012570d3f58 Mon Sep 17 00:00:00 2001 From: Johan von Forstner Date: Sun, 29 Mar 2020 22:07:29 +0200 Subject: [PATCH] add gallery fullscreen view --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 2 + .../java/com/johan/evmap/GalleryActivity.kt | 106 ++++++++++++++++++ .../main/java/com/johan/evmap/MapsActivity.kt | 85 +++++++++++++- .../com/johan/evmap/adapter/GalleryAdapter.kt | 105 +++++++++++++++-- .../com/johan/evmap/api/GoingElectricModel.kt | 4 +- .../com/johan/evmap/ui/BindingAdapters.kt | 8 ++ app/src/main/res/layout/activity_gallery.xml | 22 ++++ .../res/layout/gallery_item_fullscreen.xml | 8 ++ app/src/main/res/values/donottranslate.xml | 4 + 10 files changed, 329 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/johan/evmap/GalleryActivity.kt create mode 100644 app/src/main/res/layout/activity_gallery.xml create mode 100644 app/src/main/res/layout/gallery_item_fullscreen.xml create mode 100644 app/src/main/res/values/donottranslate.xml diff --git a/app/build.gradle b/app/build.gradle index a8ec2a6a..4be4f976 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -59,6 +59,7 @@ dependencies { implementation 'com.squareup.retrofit2:converter-moshi:2.7.2' implementation 'com.squareup.moshi:moshi-kotlin:1.9.2' implementation 'com.squareup.picasso:picasso:2.71828' + implementation 'com.github.MikeOrtiz:TouchImageView:2.3.3' // debug tools implementation 'com.facebook.stetho:stetho:1.5.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9117872e..c91f246a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,6 +34,8 @@ + + \ No newline at end of file diff --git a/app/src/main/java/com/johan/evmap/GalleryActivity.kt b/app/src/main/java/com/johan/evmap/GalleryActivity.kt new file mode 100644 index 00000000..80ba6d1d --- /dev/null +++ b/app/src/main/java/com/johan/evmap/GalleryActivity.kt @@ -0,0 +1,106 @@ +package com.johan.evmap + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.app.SharedElementCallback +import androidx.databinding.DataBindingUtil +import androidx.viewpager2.widget.ViewPager2 +import com.johan.evmap.adapter.GalleryAdapter +import com.johan.evmap.adapter.galleryTransitionName +import com.johan.evmap.databinding.ActivityGalleryBinding +import com.ortiz.touchview.TouchImageView + + +class GalleryActivity : AppCompatActivity() { + companion object { + const val EXTRA_POSITION = "position" + const val EXTRA_PHOTOS = "photos" + private const val SAVED_CURRENT_PAGE_POSITION = "current_page_position" + } + + private lateinit var binding: ActivityGalleryBinding + private var isReturning: Boolean = false + private var startingPosition: Int = 0 + private var currentPosition: Int = 0 + private lateinit var galleryAdapter: GalleryAdapter + private var currentPage: TouchImageView? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_gallery) + ActivityCompat.postponeEnterTransition(this) + ActivityCompat.setEnterSharedElementCallback(this, enterElementCallback) + + startingPosition = intent.getIntExtra(EXTRA_POSITION, 0) + currentPosition = + savedInstanceState?.getInt(SAVED_CURRENT_PAGE_POSITION) ?: startingPosition + + galleryAdapter = GalleryAdapter(this, detailView = true, pageToLoad = currentPosition) { + ActivityCompat.startPostponedEnterTransition(this) + } + binding.gallery.setPageTransformer { page, position -> + val v = page as TouchImageView + currentPage = v + } + binding.gallery.adapter = galleryAdapter + binding.photos = intent.getParcelableArrayListExtra(EXTRA_PHOTOS) + + binding.gallery.post { + binding.gallery.setCurrentItem(currentPosition, false) + binding.gallery.registerOnPageChangeCallback(object : + ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + currentPosition = position + } + }) + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putInt(SAVED_CURRENT_PAGE_POSITION, currentPosition) + } + + override fun finishAfterTransition() { + isReturning = true + val data = Intent() + data.putExtra(MapsActivity.EXTRA_STARTING_GALLERY_POSITION, startingPosition) + data.putExtra(MapsActivity.EXTRA_CURRENT_GALLERY_POSITION, currentPosition) + setResult(Activity.RESULT_OK, data) + super.finishAfterTransition() + } + + private val enterElementCallback: SharedElementCallback = object : SharedElementCallback() { + override fun onMapSharedElements( + names: MutableList, + sharedElements: MutableMap + ) { + if (isReturning) { + val index = binding.gallery.currentItem + val currentPage = currentPage ?: return + + if (startingPosition != currentPosition) { + names.clear() + names.add(galleryTransitionName(index)) + + sharedElements.clear() + sharedElements[galleryTransitionName(index)] = currentPage + } + } + } + } + + override fun onBackPressed() { + val image = currentPage + if (image == null || image.currentZoom in 0.95f..1.05f) { + super.onBackPressed() + } else { + image.setZoomAnimated(1f, 0.5f, 0.5f) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/johan/evmap/MapsActivity.kt b/app/src/main/java/com/johan/evmap/MapsActivity.kt index 3061ccd6..ae2559ce 100644 --- a/app/src/main/java/com/johan/evmap/MapsActivity.kt +++ b/app/src/main/java/com/johan/evmap/MapsActivity.kt @@ -1,14 +1,19 @@ package com.johan.evmap import android.Manifest +import android.annotation.SuppressLint import android.content.Intent import android.content.pm.PackageManager import android.content.res.Configuration import android.net.Uri import android.os.Bundle +import android.view.View +import android.view.ViewTreeObserver import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat +import androidx.core.app.ActivityOptionsCompat +import androidx.core.app.SharedElementCallback import androidx.core.content.ContextCompat import androidx.databinding.DataBindingUtil import androidx.lifecycle.MutableLiveData @@ -27,6 +32,7 @@ import com.google.android.material.snackbar.Snackbar import com.johan.evmap.adapter.ConnectorAdapter import com.johan.evmap.adapter.DetailAdapter import com.johan.evmap.adapter.GalleryAdapter +import com.johan.evmap.adapter.galleryTransitionName import com.johan.evmap.api.* import com.johan.evmap.databinding.ActivityMapsBinding import com.johan.evmap.ui.ClusterIconGenerator @@ -61,17 +67,21 @@ class MapsActivity : AppCompatActivity(), OnMapReadyCallback { private var markers: Map = emptyMap() private var clusterMarkers: List = emptyList() + private var reenterState: Bundle? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) + api = GoingElectricApi.create(getString(R.string.goingelectric_key)) + binding = DataBindingUtil.setContentView(this, R.layout.activity_maps) binding.lifecycleOwner = this binding.vm = vm - fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) + ActivityCompat.setExitSharedElementCallback(this, exitElementCallback) val mapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment mapFragment.getMapAsync(this) - api = GoingElectricApi.create(getString(R.string.goingelectric_key)) setupAdapters() @@ -144,8 +154,22 @@ class MapsActivity : AppCompatActivity(), OnMapReadyCallback { } private fun setupAdapters() { + val galleryClickListener = object : GalleryAdapter.ItemClickListener { + override fun onItemClick(view: View, position: Int) { + val photos = vm.charger.value?.photos ?: return + val intent = Intent(this@MapsActivity, GalleryActivity::class.java).apply { + putExtra(GalleryActivity.EXTRA_PHOTOS, ArrayList(photos)) + putExtra(GalleryActivity.EXTRA_POSITION, position) + } + val options = ActivityOptionsCompat.makeSceneTransitionAnimation( + this@MapsActivity, view, view.transitionName + ) + startActivity(intent, options.toBundle()) + } + } + binding.gallery.apply { - adapter = GalleryAdapter(this@MapsActivity) + adapter = GalleryAdapter(this@MapsActivity, galleryClickListener) layoutManager = LinearLayoutManager(this@MapsActivity, LinearLayoutManager.HORIZONTAL, false) addItemDecoration(DividerItemDecoration( @@ -211,6 +235,7 @@ class MapsActivity : AppCompatActivity(), OnMapReadyCallback { } } + @SuppressLint("MissingPermission") private fun enableLocation(animate: Boolean) { val map = this.map ?: return map.isMyLocationEnabled = true @@ -333,4 +358,58 @@ class MapsActivity : AppCompatActivity(), OnMapReadyCallback { else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults) } } + + private val exitElementCallback = object : SharedElementCallback() { + override fun onMapSharedElements( + names: MutableList, + sharedElements: MutableMap + ) { + if (reenterState != null) { + val startingPosition = reenterState!!.getInt(EXTRA_STARTING_GALLERY_POSITION) + val currentPosition = reenterState!!.getInt(EXTRA_CURRENT_GALLERY_POSITION) + if (startingPosition != currentPosition) { + // Current element has changed, need to override previous exit transitions + val newTransitionName = galleryTransitionName(currentPosition) + val newSharedElement = + binding.gallery.findViewHolderForAdapterPosition(currentPosition)?.itemView + if (newSharedElement != null) { + names.clear() + names.add(newTransitionName) + + sharedElements.clear() + sharedElements[newTransitionName] = newSharedElement + } + } + reenterState = null + } + } + } + + override fun onActivityReenter(resultCode: Int, data: Intent) { + // returning to gallery + super.onActivityReenter(resultCode, data) + reenterState = Bundle(data.extras) + reenterState?.let { + val startingPosition = it.getInt(EXTRA_STARTING_GALLERY_POSITION) + val currentPosition = it.getInt(EXTRA_CURRENT_GALLERY_POSITION) + if (startingPosition != currentPosition) binding.gallery.scrollToPosition( + currentPosition + ) + ActivityCompat.postponeEnterTransition(this) + + binding.gallery.viewTreeObserver.addOnPreDrawListener(object : + ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + binding.gallery.viewTreeObserver.removeOnPreDrawListener(this) + ActivityCompat.startPostponedEnterTransition(this@MapsActivity) + return true + } + }) + } + } + + companion object { + const val EXTRA_STARTING_GALLERY_POSITION = "extra_starting_item_position" + const val EXTRA_CURRENT_GALLERY_POSITION = "extra_current_item_position" + } } diff --git a/app/src/main/java/com/johan/evmap/adapter/GalleryAdapter.kt b/app/src/main/java/com/johan/evmap/adapter/GalleryAdapter.kt index 8fef5d04..04f555d7 100644 --- a/app/src/main/java/com/johan/evmap/adapter/GalleryAdapter.kt +++ b/app/src/main/java/com/johan/evmap/adapter/GalleryAdapter.kt @@ -1,41 +1,122 @@ package com.johan.evmap.adapter +import android.annotation.SuppressLint import android.content.Context -import android.view.LayoutInflater -import android.view.ViewGroup +import android.view.* import android.widget.ImageView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.johan.evmap.R import com.johan.evmap.api.ChargerPhoto +import com.ortiz.touchview.TouchImageView +import com.squareup.picasso.Callback import com.squareup.picasso.Picasso -class GalleryAdapter(context: Context) : + +class GalleryAdapter( + context: Context, + val itemClickListener: ItemClickListener? = null, + val detailView: Boolean = false, + val pageToLoad: Int? = null, + val loadedListener: (() -> Unit)? = null +) : ListAdapter(ChargerPhotoDiffCallback()) { class ViewHolder(val view: ImageView) : RecyclerView.ViewHolder(view) - val apikey = context.getString(R.string.goingelectric_key) + interface ItemClickListener { + fun onItemClick(view: View, position: Int) + } + val apikey = context.getString(R.string.goingelectric_key) + var loaded = false + + @SuppressLint("ClickableViewAccessibility") override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.gallery_item, parent, false) as ImageView + val inflater = LayoutInflater.from(parent.context) + val view: ImageView + if (detailView) { + view = inflater.inflate(R.layout.gallery_item_fullscreen, parent, false) as ImageView + view.setOnTouchListener { view, event -> + var result = true + //can scroll horizontally checks if there's still a part of the image + //that can be scrolled until you reach the edge + if (event.pointerCount >= 2 || view.canScrollHorizontally(1) && view.canScrollHorizontally( + -1 + ) + ) { + //multi-touch event + result = when (event.action) { + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> { + // Disallow RecyclerView to intercept touch events. + parent.requestDisallowInterceptTouchEvent(true) + // Disable touch on view + false + } + MotionEvent.ACTION_UP -> { + // Allow RecyclerView to intercept touch events. + parent.requestDisallowInterceptTouchEvent(false) + true + } + else -> true + } + } + result + } + } else { + view = inflater.inflate(R.layout.gallery_item, parent, false) as ImageView + } return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { + if (detailView) { + (holder.view as TouchImageView).resetZoom() + } Picasso.get() .load( - "https://api.goingelectric.de/chargepoints/photo/?key=${apikey}&id=${getItem( - position - ).id}&height=${holder.view.height}" + "https://api.goingelectric.de/chargepoints/photo/?key=${apikey}" + + "&id=${getItem(position).id}" + + if (detailView) { + "&size=1000" + } else { + "&height=${holder.view.height}" + } ) - .into(holder.view) + .into(holder.view, object : Callback { + override fun onSuccess() { + if (!loaded && loadedListener != null && pageToLoad == position) { + holder.view.viewTreeObserver.addOnPreDrawListener(object : + ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + holder.view.viewTreeObserver.removeOnPreDrawListener(this) + loadedListener.invoke() + return true + } + }) + loaded = true + } + } + + override fun onError(e: Exception?) { + if (!loaded && loadedListener != null && pageToLoad == position) { + loadedListener.invoke() + loaded = true + } + } + + }) + holder.view.transitionName = galleryTransitionName(position) + if (itemClickListener != null) { + holder.view.setOnClickListener { + itemClickListener.onItemClick(holder.view, position) + } + } } - - } +fun galleryTransitionName(position: Int) = "gallery_$position" + class ChargerPhotoDiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: ChargerPhoto, newItem: ChargerPhoto): Boolean { return oldItem.id == newItem.id diff --git a/app/src/main/java/com/johan/evmap/api/GoingElectricModel.kt b/app/src/main/java/com/johan/evmap/api/GoingElectricModel.kt index d17fdf60..e1ac12b1 100644 --- a/app/src/main/java/com/johan/evmap/api/GoingElectricModel.kt +++ b/app/src/main/java/com/johan/evmap/api/GoingElectricModel.kt @@ -1,6 +1,7 @@ package com.johan.evmap.api import android.content.Context +import android.os.Parcelable import androidx.core.text.HtmlCompat import com.johan.evmap.R import com.johan.evmap.adapter.Equatable @@ -144,7 +145,8 @@ data class Hours( ) @JsonClass(generateAdapter = true) -data class ChargerPhoto(val id: String) +@Parcelize +data class ChargerPhoto(val id: String) : Parcelable @JsonClass(generateAdapter = true) data class ChargeLocationCluster( diff --git a/app/src/main/java/com/johan/evmap/ui/BindingAdapters.kt b/app/src/main/java/com/johan/evmap/ui/BindingAdapters.kt index d8926b70..27db5aef 100644 --- a/app/src/main/java/com/johan/evmap/ui/BindingAdapters.kt +++ b/app/src/main/java/com/johan/evmap/ui/BindingAdapters.kt @@ -7,6 +7,7 @@ import android.widget.ImageView import androidx.databinding.BindingAdapter import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.floatingactionbutton.FloatingActionButton import com.johan.evmap.R @@ -39,6 +40,13 @@ fun setRecyclerViewData(recyclerView: RecyclerView, items: List?) { } } +@BindingAdapter("data") +fun setRecyclerViewData(recyclerView: ViewPager2, items: List?) { + if (recyclerView.adapter is ListAdapter<*, *>) { + (recyclerView.adapter as ListAdapter).submitList(items) + } +} + @BindingAdapter("connectorIcon") fun getConnectorItem(view: ImageView, type: String) { view.setImageResource( diff --git a/app/src/main/res/layout/activity_gallery.xml b/app/src/main/res/layout/activity_gallery.xml new file mode 100644 index 00000000..28d30364 --- /dev/null +++ b/app/src/main/res/layout/activity_gallery.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/gallery_item_fullscreen.xml b/app/src/main/res/layout/gallery_item_fullscreen.xml new file mode 100644 index 00000000..d6cbc35c --- /dev/null +++ b/app/src/main/res/layout/gallery_item_fullscreen.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml new file mode 100644 index 00000000..e7829642 --- /dev/null +++ b/app/src/main/res/values/donottranslate.xml @@ -0,0 +1,4 @@ + + + picture + \ No newline at end of file