Created
November 4, 2019 14:16
-
-
Save AndSky90/bb5cfe6e6413edb070e20547ec5a9277 to your computer and use it in GitHub Desktop.
PhotoView ImageGallery Custom
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<androidx.constraintlayout.widget.ConstraintLayout | |
xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:tools="http://schemas.android.com/tools" | |
xmlns:app="http://schemas.android.com/apk/res-auto" | |
android:fitsSystemWindows="true" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:background="@android:color/black" | |
tools:context=".ui.activity.ImageViewerActivity"> | |
<androidx.appcompat.widget.Toolbar | |
android:id="@+id/topToolbar" | |
app:layout_constraintTop_toTopOf="parent" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintEnd_toEndOf="parent" | |
android:layout_width="match_parent" | |
android:layout_height="?attr/actionBarSize" | |
tools:layout_height="@dimen/toolbar_height_56dp" | |
android:elevation="@dimen/elevation_default_3dp" | |
android:titleTextAppearance="@style/ImageViewerToolbarTextAppearance" | |
style="@style/ImageViewerToolbar"/> | |
<package.ui.custom.VerticalDragLayout | |
android:id="@+id/dragLayout" | |
android:transitionName="image" | |
android:layout_width="0dp" | |
android:layout_height="0dp" | |
app:layout_constraintTop_toTopOf="parent" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintBottom_toBottomOf="parent"> | |
<View | |
android:id="@+id/backgroundColorView" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:background="@android:color/black"/> | |
<package.ui.custom.MultiTouchViewPager | |
android:id="@+id/viewPagerView" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:overScrollMode="never"/> | |
</package.ui.custom.VerticalDragLayout> | |
<package.SocialsView | |
android:id="@+id/socials" | |
android:layout_width="0dp" | |
android:layout_height="wrap_content" | |
tools:layout_height="@dimen/toolbar_height_56dp" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintBottom_toBottomOf="parent"/> | |
<package.FeatureDataStateView | |
android:id="@+id/dataStateView" | |
android:layout_width="0dp" | |
android:layout_height="0dp" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintTop_toBottomOf="@id/topToolbar" | |
app:layout_constraintBottom_toTopOf="@id/socials" | |
android:background="@android:color/black" | |
app:whiteText="true" | |
android:visibility="gone"/> | |
<package.CircleLoadingBar | |
android:id="@+id/ImageViewerLoadingView" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintTop_toBottomOf="@id/topToolbar" | |
app:layout_constraintBottom_toBottomOf="parent" /> | |
</androidx.constraintlayout.widget.ConstraintLayout> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.os.Bundle | |
import android.view.Menu | |
import android.view.View | |
import androidx.core.view.isVisible | |
import com.arellomobile.mvp.presenter.InjectPresenter | |
import com.arellomobile.mvp.presenter.ProvidePresenter | |
import kotlinx.android.synthetic.main.image_viewer.* | |
import javax.inject.Inject | |
import kotlin.math.abs | |
import kotlin.math.min | |
class ImageViewerActivity : BaseItemViewerActivity<ImageViewerPresenter>(), IImageViewerView { | |
override var baseRefreshBar: View? = null | |
override var baseLoadingView: View? = null | |
companion object { | |
private const val STATE_LAST_CHECKED_ITEM_INDEX = "state_last_checked_item_index" | |
/* private const val ARG_LIST_OF_MEDIA = "arg_list_of_media" | |
private const val ARG_INITIALLY_CHECKED_ITEM_INDEX = "arg_initially_checked_item_index" | |
private const val ARG_FORCE_ROTATION = "arg_force_rotation"*/ | |
} | |
private var photoList: List<Item>? = null | |
//индекс элемента в общем списке элементов (из бандла), требует преобразования в фото-индекс | |
private var positionInCommonList: Int = 0 | |
//индекс элемента в отфильтрованном списке фотографий (фото-индекс) | |
private var currentImageIndex: Int = 0 | |
private var lastCheckedImageGuid = "" | |
private lateinit var mediaViewController: MediaViewController | |
private val dismissPathLength by lazy { resources.getDimensionPixelSize(R.dimen.dismiss_path_length) } | |
private var toolbarVisibility = true | |
private var fromFile = false | |
@InjectPresenter | |
override lateinit var presenter: ImageViewerPresenter | |
@ProvidePresenter | |
fun providePresenter() = ImageViewerPresenter(ImageViewerRepository()) | |
@Inject | |
lateinit var coreNetwork: ICoreNetwork | |
override fun onSaveInstanceState(outState: Bundle) { | |
outState.putInt(STATE_LAST_CHECKED_ITEM_INDEX, currentImageIndex) | |
super.onSaveInstanceState(outState) | |
} | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
setContentView(R.layout.image_viewer) | |
currentImageIndex = savedInstanceState?.getInt(STATE_LAST_CHECKED_ITEM_INDEX) ?: 0 | |
window.decorView.systemUiVisibility = 0 | |
UITools.setMIUIStatusBarDarkIcon(this, false) | |
createSupportActionBar(topToolbar) | |
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_action_close_24_white) | |
supportActionBar?.title = "" | |
setSystemBarColor(this, android.R.color.black) | |
baseLoadingView = ImageViewerLoadingView | |
dragLayout.setOnDragListener { dy -> | |
dragLayout.alpha = 1 - min(abs(dy / (5 * dismissPathLength)), 1f) | |
viewPagerView.translationY = -dy | |
} | |
dragLayout.setOnReleaseDragListener { dy -> | |
if (abs(dy) > dismissPathLength) { | |
viewPagerView.isVisible = false | |
finish() | |
} else { | |
dragLayout.alpha = 1f | |
viewPagerView.translationY = 0f | |
} | |
} | |
mediaViewController = MediaViewController( | |
viewPager = viewPagerView, | |
onCurrentItemChangeListener = { index -> | |
currentImageIndex = index | |
log("index: $index") | |
refreshToolbarsData() | |
}, | |
//onPlayerControllerVisibilityListener = {}, | |
onImageZoomListener = { isZoomed -> dragLayout.draggingIsEnabled = !isZoomed }, | |
onImageClickListener = { changeToolbarVisibility() }/*, | |
soc = socials, | |
onErrorImage = { showError("Ошибка просмотрщика фото")}*/ | |
) | |
} | |
/**обновляем надпись в тулбаре ( "# из $") и социальные фичи*/ | |
private fun refreshToolbarsData() { | |
supportActionBar?.title = | |
getString(R.string.toolbar_NUMBER_of_SIZE_title, currentImageIndex + 1, photoList!!.size) | |
socials.setData(photoList!![currentImageIndex].features, photoList!![currentImageIndex].guid) | |
isActionGenericViewerVisible = !photoList!![currentImageIndex].genericItems.isNullOrEmpty() | |
} | |
override fun setData(data: Item) { | |
val currentItem = data.items?.get(positionInCommonList) //берем нужный итем из нефильтрованного массива | |
photoList = data.items?.filter { it.type.equals("card:photo", true) } //фильтруем массив | |
if (!photoList.isNullOrEmpty() && currentItem != null) { | |
currentImageIndex = | |
photoList!!.indexOf(currentItem) //узнаём индекс этого же итема в фильтрованном массиве | |
lastCheckedImageGuid = currentItem.guid | |
refreshToolbarsData() | |
mediaViewController.bind(photoList!!) | |
mediaViewController.setCurrentItemIndex(currentImageIndex) | |
} else { | |
showError(ItemErrorType.EMPTY_DATA) | |
} | |
} | |
override fun setPhotoIndex(itemIndex: Int) { | |
positionInCommonList = itemIndex | |
} | |
/**показываем одиночный файл*/ | |
override fun showImageFromFilePath(path: String) { | |
hideError() | |
supportActionBar?.title = getString(R.string.title_photo_text) | |
val imageItem = listOf( | |
Item( | |
guid = "0", | |
type = "card:photo", | |
links = ItemLinks(image = path) | |
) | |
) | |
mediaViewController.apply { | |
bind(imageItem) | |
setCurrentItemIndex(0) | |
} | |
} | |
/**скрываем и отображаем тулбары (срабатывает по одиночному тапу)*/ | |
private fun changeToolbarVisibility() { | |
val animDur = 400L | |
toolbarVisibility = !toolbarVisibility | |
topToolbar.animate().setDuration(animDur).alpha(if (toolbarVisibility) 1f else 0f) | |
socials.animate().setDuration(animDur).alpha(if (toolbarVisibility) 1f else 0f) | |
} | |
override fun onCreateOptionsMenu(menu: Menu?): Boolean { | |
if (!fromFile) { | |
menuInflater.inflate(R.menu.image_viewer_toolbar_menu, menu) | |
val action = menu?.findItem(R.id.info_generic_menu_item) | |
action?.isVisible = isActionGenericViewerVisible | |
} | |
return super.onCreateOptionsMenu(menu) | |
} | |
override fun onPrepareOptionsMenu(menu: Menu?): Boolean { | |
menu?.findItem(R.id.info_generic_menu_item)?.setOnMenuItemClickListener { | |
if (!photoList.isNullOrEmpty()) { | |
navigator.openGenericViewer(lastCheckedImageGuid, -1) | |
} | |
true | |
} | |
return true | |
} | |
override fun showError(error: ItemErrorType) { | |
dragLayout.visibility = View.GONE | |
dataStateView.showError(ItemContentType.IMAGE, error) | |
} | |
override fun hideError() { | |
dataStateView.visibility = View.GONE | |
dragLayout.visibility = View.VISIBLE | |
} | |
override fun setFlagFromFile(flag: Boolean) { | |
fromFile = flag | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.content.Context | |
import android.util.SparseArray | |
import android.view.View | |
import android.view.ViewGroup | |
import androidx.core.util.valueIterator | |
import androidx.viewpager.widget.PagerAdapter | |
import com.bumptech.glide.Glide | |
import com.github.chrisbanes.photoview.PhotoView | |
/*import com.google.android.exoplayer2.ExoPlayer | |
import com.google.android.exoplayer2.source.ExtractorMediaSource | |
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection | |
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector | |
import com.google.android.exoplayer2.ui.PlayerView | |
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter | |
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory | |
import com.google.android.exoplayer2.util.Util | |
import java.util.* | |
internal class MediaPagerAdapter( | |
private val listOfImages: List<Item>, | |
context: Context, | |
// onPlayerControllerVisibilityListener: (Boolean) -> Unit, | |
onImageZoomListener: (isZoomed: Boolean) -> Unit, | |
onImageClickListener: () -> Unit/*, | |
soc: SocialsView*/ | |
// , onErrorImage: () -> Unit | |
) : PagerAdapter() { | |
private val mediaPagePool = MediaPagePool( | |
context, | |
/* ExoPlayerFactory(context), | |
onPlayerControllerVisibilityListener,*/ | |
onImageZoomListener, | |
onImageClickListener/*, | |
soc */ | |
// , onErrorImage | |
) | |
private val mediaPagesInUse = SparseArray<MediaPage>() | |
override fun instantiateItem(container: ViewGroup, position: Int): Any { | |
val media = listOfImages[position] | |
/* val mediaPage = when (media.type) { | |
MediaType.VIDEO -> mediaPagePool.getVideoPage().apply { this.media = media } | |
MediaType.IMAGE -> mediaPagePool.getImagePage().apply { this.media = media } | |
}*/ | |
val mediaPage = mediaPagePool.getImagePage().apply { this.media = media } | |
container.addView(mediaPage.view) | |
mediaPagesInUse.put(position, mediaPage) | |
return mediaPage | |
} | |
override fun destroyItem(container: ViewGroup, position: Int, key: Any) { | |
val mediaPage = (key as MediaPage) | |
container.removeView(mediaPage.view) | |
mediaPagesInUse.remove(position) | |
mediaPagePool.releaseMediaPage(mediaPage) | |
} | |
override fun isViewFromObject(view: View, key: Any): Boolean = ((key as MediaPage).view == view) | |
override fun getCount(): Int = listOfImages.size | |
private var lastPrimaryItem = -1 // Initially invalid. | |
override fun setPrimaryItem(container: ViewGroup, position: Int, key: Any) { | |
// It's crucial to make this method idempotent. | |
// It is called multiple times when the player controller is visible. | |
// This causes the pause button to not work. | |
if (position != lastPrimaryItem) { | |
lastPrimaryItem = position | |
// (key as? VideoPage)?.startOrResume() | |
for (mediaPage in mediaPagesInUse.valueIterator()) { | |
if (mediaPage is ImagePage && mediaPage !== key) { | |
mediaPage.resetScale() | |
} | |
} | |
} | |
} | |
/* fun resumeVideo(position: Int) { | |
if (lastPrimaryItem == position) { | |
(mediaPagesInUse[position] as? VideoPage)?.startOrResume() | |
} | |
} | |
fun pauseVideoAndHideController() { | |
for (mediaPage in mediaPagesInUse.valueIterator()) { | |
(mediaPage as? VideoPage)?.apply { | |
pause() | |
hideController() | |
} | |
} | |
}*/ | |
fun clear() { | |
/* for (mediaPage in mediaPagesInUse.valueIterator()) { | |
(mediaPage as? VideoPage)?.releasePlayer() | |
}*/ | |
mediaPagesInUse.clear() | |
mediaPagePool.clear() | |
} | |
} | |
/** | |
* Abstraction over gallery page. Subclasses are responsible for view creation, media binding/unbinding and media | |
* control. | |
*/ | |
private sealed class MediaPage { | |
abstract val view: View | |
abstract var media: Item? | |
} | |
/*private class VideoPage( | |
context: Context, | |
private val exoPlayerWrapper: ExoPlayerWrapper, | |
onPlayerControllerVisibilityListener: (Boolean) -> Unit | |
) : MediaPage() { | |
override val view: PlayerView = ExoPlayerView(context).apply { | |
exoPlayerWrapper.attachTo(this) | |
controllerAutoShow = false | |
controllerHideOnTouch = false | |
hideController() | |
setControllerVisibilityListener { visibility -> | |
onPlayerControllerVisibilityListener((visibility == View.VISIBLE)) | |
} | |
} | |
override var media: Item? = null | |
set(value) { | |
field = value | |
when (value) { | |
null -> exoPlayerWrapper.pause() | |
else -> exoPlayerWrapper.setMediaSource(value.settings?.self!!) | |
} | |
} | |
fun startOrResume() = exoPlayerWrapper.startOrResume() | |
fun pause() = exoPlayerWrapper.pause() | |
fun hideController() = view.hideController() | |
fun releasePlayer() = exoPlayerWrapper.release() | |
}*/ | |
private class ImagePage( | |
context: Context, | |
private val onImageZoomListener: (isZoomed: Boolean) -> Unit, | |
private val onImageClickListener: () -> Unit/*, | |
private val soc: SocialsView | |
private val onErrorImage: () -> Unit*/ | |
) : MediaPage() { | |
override val view: PhotoView = PhotoView(context).apply { | |
minimumScale = 1.0F | |
maximumScale = 2.0F | |
setOnScaleChangeListener { _, _, _ -> onImageZoomListener(scale > 1.05F) } | |
setOnClickListener { onImageClickListener() } | |
} | |
override var media: Item? = null | |
set(value) { | |
field = value | |
if (value == null) { | |
Glide.with(view).clear(view) | |
} else { | |
if (value.links?.image != null) { | |
GlideTools.setImageWithGlide( | |
imageLink = value.links!!.image, | |
imageView = view, | |
errorImage = R.drawable.error_glide, | |
isCenterCrop = false | |
) | |
} else { | |
Glide.with(view) | |
.load(R.drawable.error_glide) | |
.into(view) | |
} | |
} | |
} | |
fun resetScale() { | |
view.setScale(1.0F, false) | |
onImageZoomListener(false) | |
} | |
} | |
/** | |
* This class is a base for page view (players and other objects) recycling mechanism. | |
*/ | |
private class MediaPagePool( | |
private val context: Context, | |
/*private val playerFactory: ExoPlayerFactory, | |
private val onPlayerControllerVisibilityListener: (Boolean) -> Unit,*/ | |
private val onImageZoomListener: (isZoomed: Boolean) -> Unit, | |
private val onImageClickListener: () -> Unit/*, | |
private val soc: SocialsView*/ | |
// , private val onErrorImage: () -> Unit | |
) { | |
// private val videoPagePool: Queue<VideoPage> = LinkedList() | |
private val imagePagePool: Queue<ImagePage> = LinkedList() | |
/* fun getVideoPage(): VideoPage = | |
videoPagePool.poll() | |
?: VideoPage( | |
context, | |
playerFactory.createPlayer(), | |
onPlayerControllerVisibilityListener | |
)*/ | |
fun getImagePage(): ImagePage = | |
imagePagePool.poll() | |
?: ImagePage(context, onImageZoomListener, onImageClickListener /*, soc, onErrorImage*/) | |
fun releaseMediaPage(mediaPage: MediaPage) = when (mediaPage) { | |
/* is VideoPage -> { | |
mediaPage.media = null | |
videoPagePool.offer(mediaPage) | |
}*/ | |
is ImagePage -> { | |
mediaPage.media = null | |
imagePagePool.offer(mediaPage) | |
} | |
} | |
fun clear() { | |
/* videoPagePool.forEach { it.releasePlayer() } | |
videoPagePool.clear()*/ | |
imagePagePool.clear() | |
} | |
} | |
/** | |
* ExoPlayer instance and ExtractorMediaSource.Factory instance are bind with the same BandwidthMeter instance. | |
* We need to keep a reference to ExtractorMediaSource.Factory instance to be able to change video URL. | |
* Also this class gathered together all methods we need to work with the player. | |
* Do not expose reference to the player to not allow abuse of its usage, hence to lower code entanglement. | |
*/ | |
/*private class ExoPlayerWrapper( | |
private val exoPlayer: ExoPlayer, | |
private val mediaSourceFactory: ExtractorMediaSource.Factory | |
) { | |
fun attachTo(playerView: PlayerView) { | |
playerView.player = exoPlayer | |
} | |
fun startOrResume() { | |
exoPlayer.playWhenReady = true | |
} | |
fun pause() { | |
exoPlayer.playWhenReady = false | |
} | |
fun setMediaSource(url: String) { | |
exoPlayer.prepare(mediaSourceFactory.createMediaSource(Uri.parse(url))) | |
} | |
fun release() = exoPlayer.release() | |
} | |
private class ExoPlayerFactory(private val context: Context) { | |
private val userAgent: String = Util.getUserAgent(context, "Gallery") | |
fun createPlayer(): ExoPlayerWrapper { | |
val bandwidthMeter = DefaultBandwidthMeter() | |
return ExoPlayerWrapper( | |
com.google.android.exoplayer2.ExoPlayerFactory.newSimpleInstance( | |
context, | |
DefaultTrackSelector( | |
AdaptiveTrackSelection.Factory(bandwidthMeter) | |
) | |
), | |
ExtractorMediaSource.Factory( | |
DefaultDataSourceFactory( | |
context, | |
userAgent, | |
bandwidthMeter | |
) | |
) | |
) | |
} | |
}*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.viewpager.widget.ViewPager | |
internal class MediaViewController( | |
private val viewPager: ViewPager, | |
private val onCurrentItemChangeListener: (Int) -> Unit, | |
// private val onPlayerControllerVisibilityListener: (Boolean) -> Unit, | |
private val onImageZoomListener: (isZoomed: Boolean) -> Unit, | |
private val onImageClickListener: () -> Unit | |
//, private val soc: SocialsView | |
//, private val onErrorImage: () -> Unit | |
) { | |
private var adapter: MediaPagerAdapter? = null | |
fun bind(listOfImages: List<Item>) { | |
adapter = MediaPagerAdapter( | |
listOfImages, | |
viewPager.context, | |
// onPlayerControllerVisibilityListener, | |
onImageZoomListener, | |
onImageClickListener/*, | |
soc*/ | |
// , onErrorImage | |
) | |
viewPager.adapter = adapter | |
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { | |
override fun onPageScrollStateChanged(state: Int) { | |
/* when (state) { | |
ViewPager.SCROLL_STATE_DRAGGING, | |
ViewPager.SCROLL_STATE_SETTLING -> adapter.pauseVideoAndHideController() | |
ViewPager.SCROLL_STATE_IDLE -> adapter.resumeVideo(viewPager.currentItem) | |
}*/ | |
} | |
override fun onPageScrolled( | |
position: Int, | |
positionOffset: Float, | |
positionOffsetPixels: Int | |
) { | |
} | |
override fun onPageSelected(position: Int) { | |
// Do not use this callback to start video. There are no views instantiated on first ViewPager layout. | |
// Thus adapter has not player to start. Moreover if initial position is 0 this callback is not called. | |
onCurrentItemChangeListener(position) | |
} | |
}) | |
} | |
fun setCurrentItemIndex(currentItemIndex: Int) { | |
viewPager.currentItem = currentItemIndex | |
} | |
//fun release() = adapter?.clear() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.annotation.SuppressLint | |
import android.content.Context | |
import android.util.AttributeSet | |
import android.view.MotionEvent | |
import androidx.viewpager.widget.ViewPager | |
/** | |
* This ViewPager copied from https://github.com/stfalcon-studio/FrescoImageViewer | |
*/ | |
internal class MultiTouchViewPager @JvmOverloads constructor( | |
context: Context, | |
attrs: AttributeSet? = null | |
) : ViewPager(context, attrs) { | |
private var isDisallowIntercept = true | |
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { | |
isDisallowIntercept = disallowIntercept | |
super.requestDisallowInterceptTouchEvent(disallowIntercept) | |
} | |
override fun dispatchTouchEvent(ev: MotionEvent) = | |
if (ev.pointerCount > 1 && isDisallowIntercept) { | |
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 | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.annotation.SuppressLint | |
import android.content.Context | |
import android.util.AttributeSet | |
import android.view.MotionEvent | |
import android.view.ViewConfiguration | |
import android.widget.FrameLayout | |
import kotlin.math.abs | |
import kotlin.math.atan2 | |
/** | |
* Allow listen vertical drag motions. | |
*/ | |
internal class VerticalDragLayout @JvmOverloads constructor( | |
context: Context, | |
attrs: AttributeSet? = null, | |
defStyleAttr: Int = 0 | |
) : FrameLayout(context, attrs, defStyleAttr) { | |
var draggingIsEnabled = true | |
set(value) { | |
field = value | |
reset() | |
} | |
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop | |
//null - not detect move, true - vertical, false - horizontal | |
private var isDetectedVerticalMove: Boolean? = null | |
private var startX = 0f | |
private var startY = 0f | |
private var startInnerMoveY = 0f | |
private var onDragListener: (dy: Float) -> Unit = {} | |
private var onReleaseDragListener: (dy: Float) -> Unit = {} | |
fun setOnDragListener(listener: (dy: Float) -> Unit) { | |
onDragListener = listener | |
} | |
fun setOnReleaseDragListener(listener: (dy: Float) -> Unit) { | |
onReleaseDragListener = listener | |
} | |
private fun reset() { | |
isDetectedVerticalMove = null | |
startX = 0f | |
startY = 0f | |
startInnerMoveY = 0f | |
} | |
override fun dispatchTouchEvent(ev: MotionEvent): Boolean { | |
when (ev.action) { | |
MotionEvent.ACTION_DOWN -> { | |
if (ev.pointerCount == 1) { | |
startX = ev.x | |
startY = ev.y | |
} else { | |
isDetectedVerticalMove = null | |
startX = 0f | |
startY = 0f | |
} | |
} | |
MotionEvent.ACTION_UP, | |
MotionEvent.ACTION_CANCEL -> { | |
isDetectedVerticalMove = null | |
startX = 0f | |
startY = 0f | |
} | |
MotionEvent.ACTION_MOVE -> { | |
if (draggingIsEnabled | |
&& isDetectedVerticalMove == null | |
&& ev.pointerCount == 1 | |
&& abs(startY - ev.y) > touchSlop | |
) { | |
val direction = getDirection(ev, startX, startY) | |
isDetectedVerticalMove = (direction == Direction.UP || direction == Direction.DOWN) | |
} | |
} | |
} | |
return super.dispatchTouchEvent(ev) | |
} | |
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { | |
return ev.pointerCount == 1 && ev.action == MotionEvent.ACTION_MOVE && isDetectedVerticalMove == true | |
} | |
@SuppressLint("ClickableViewAccessibility") | |
override fun onTouchEvent(ev: MotionEvent): Boolean { | |
when (ev.action) { | |
MotionEvent.ACTION_UP, | |
MotionEvent.ACTION_CANCEL -> { | |
if (ev.pointerCount == 1) { | |
onReleaseDragListener(startInnerMoveY - ev.y) | |
} | |
startInnerMoveY = 0f | |
} | |
MotionEvent.ACTION_MOVE -> { | |
if (startInnerMoveY == 0f) { | |
startInnerMoveY = ev.y | |
} | |
onDragListener(startInnerMoveY - ev.y) | |
} | |
} | |
return true | |
} | |
private fun getDirection(ev: MotionEvent, x: Float, y: Float) = | |
Direction[getAngle(x, y, ev.x, ev.y)] | |
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 enum class Direction { | |
UP, | |
DOWN, | |
LEFT, | |
RIGHT; | |
companion object { | |
private const val V_ANGLE = 30F | |
operator fun get(angle: Double): Direction = when { | |
inRange(angle, 90f - V_ANGLE, 90f + V_ANGLE) -> UP | |
inRange(angle, 0f, 90f - V_ANGLE) || inRange(angle, 270f + V_ANGLE, 360f) -> RIGHT | |
inRange(angle, 270f - V_ANGLE, 270f + V_ANGLE) -> DOWN | |
else -> LEFT | |
} | |
private fun inRange(angle: Double, init: Float, end: Float) = | |
angle >= init && angle < end | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment