mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
Merge branch 'main' into fix-multiupload
This commit is contained in:
commit
1e1e5d763f
42 changed files with 2035 additions and 2190 deletions
|
|
@ -212,8 +212,8 @@ android {
|
|||
defaultConfig {
|
||||
//applicationId 'fr.free.nrw.commons'
|
||||
|
||||
versionCode 1043
|
||||
versionName '5.1.2'
|
||||
versionCode 1046
|
||||
versionName '5.1.3'
|
||||
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
||||
|
||||
minSdkVersion 21
|
||||
|
|
|
|||
|
|
@ -125,6 +125,19 @@ class Media constructor(
|
|||
categoriesHiddenStatus = categoriesHiddenStatus
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns Author if it's not null or empty, otherwise
|
||||
* returns user
|
||||
* @return Author or User
|
||||
*/
|
||||
fun getAuthorOrUser(): String? {
|
||||
return if (!author.isNullOrEmpty()) {
|
||||
author
|
||||
} else{
|
||||
user
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets media display title
|
||||
* @return Media title
|
||||
|
|
|
|||
|
|
@ -98,14 +98,9 @@ class GridViewAdapter(
|
|||
*/
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
private fun setUploaderView(item: Media, uploader: TextView) {
|
||||
if (!item.author.isNullOrEmpty()) {
|
||||
uploader.visibility = View.VISIBLE
|
||||
uploader.text = context.getString(
|
||||
R.string.image_uploaded_by,
|
||||
item.user
|
||||
)
|
||||
} else {
|
||||
uploader.visibility = View.GONE
|
||||
}
|
||||
uploader.text = context.getString(
|
||||
R.string.image_uploaded_by,
|
||||
item.getAuthorOrUser()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ an upload might take a dozen seconds. */
|
|||
this.contribution = contribution
|
||||
this.position = position
|
||||
binding.contributionTitle.text = contribution.media.mostRelevantCaption
|
||||
binding.authorView.text = contribution.media.author
|
||||
binding.authorView.text = contribution.media.getAuthorOrUser()
|
||||
|
||||
//Removes flicker of loading image.
|
||||
binding.contributionImage.hierarchy.fadeDuration = 0
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ interface ContributionsContract {
|
|||
|
||||
interface View {
|
||||
fun showMessage(localizedMessage: String)
|
||||
fun getContext(): Context
|
||||
fun getContext(): Context?
|
||||
}
|
||||
|
||||
interface UserActionListener : BasePresenter<View> {
|
||||
|
|
|
|||
|
|
@ -74,12 +74,9 @@ import java.util.Date
|
|||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
class ContributionsFragment
|
||||
|
||||
: CommonsDaggerSupportFragment(), FragmentManager.OnBackStackChangedListener,
|
||||
class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.OnBackStackChangedListener,
|
||||
LocationUpdateListener, MediaDetailProvider, SensorEventListener, ICampaignsView,
|
||||
ContributionsContract.View,
|
||||
ContributionsListFragment.Callback {
|
||||
ContributionsContract.View, ContributionsListFragment.Callback {
|
||||
@JvmField
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
|
|
@ -307,9 +304,11 @@ class ContributionsFragment
|
|||
}
|
||||
}
|
||||
notification.setOnClickListener { view: View? ->
|
||||
startYourself(
|
||||
context, "unread"
|
||||
)
|
||||
context?.let {
|
||||
startYourself(
|
||||
it, "unread"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -500,7 +499,7 @@ class ContributionsFragment
|
|||
|
||||
private fun setUploadCount() {
|
||||
okHttpJsonApiClient
|
||||
?.getUploadCount((activity as MainActivity).sessionManager?.currentAccount!!.name)
|
||||
?.getUploadCount(sessionManager?.currentAccount!!.name)
|
||||
?.subscribeOn(Schedulers.io())
|
||||
?.observeOn(AndroidSchedulers.mainThread())?.let {
|
||||
compositeDisposable.add(
|
||||
|
|
@ -889,14 +888,16 @@ class ContributionsFragment
|
|||
* this function updates the number of contributions
|
||||
*/
|
||||
fun upDateUploadCount() {
|
||||
WorkManager.getInstance(context)
|
||||
.getWorkInfosForUniqueWorkLiveData(UploadWorker::class.java.simpleName).observe(
|
||||
viewLifecycleOwner
|
||||
) { workInfos: List<WorkInfo?> ->
|
||||
if (workInfos.size > 0) {
|
||||
setUploadCount()
|
||||
context?.let {
|
||||
WorkManager.getInstance(it)
|
||||
.getWorkInfosForUniqueWorkLiveData(UploadWorker::class.java.simpleName).observe(
|
||||
viewLifecycleOwner
|
||||
) { workInfos: List<WorkInfo?> ->
|
||||
if (workInfos.size > 0) {
|
||||
setUploadCount()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -953,7 +954,7 @@ class ContributionsFragment
|
|||
Timber.d("Skipping re-upload for non-failed %s", contribution.toString())
|
||||
}
|
||||
} else {
|
||||
showLongToast(context, R.string.this_function_needs_network_connection)
|
||||
context?.let { showLongToast(it, R.string.this_function_needs_network_connection) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,9 +80,11 @@ class ContributionsPresenter @Inject internal constructor(
|
|||
.save(contribution)
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.subscribe {
|
||||
makeOneTimeWorkRequest(
|
||||
view!!.getContext().applicationContext, ExistingWorkPolicy.KEEP
|
||||
)
|
||||
view!!.getContext()?.applicationContext?.let {
|
||||
makeOneTimeWorkRequest(
|
||||
it, ExistingWorkPolicy.KEEP
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ class DeleteHelper @Inject constructor(
|
|||
|
||||
val userPageString = "\n{{subst:idw|${media.filename}}} ~~~~"
|
||||
|
||||
val creator = media.author
|
||||
val creator = media.getAuthorOrUser()
|
||||
?: throw RuntimeException("Failed to nominate for deletion")
|
||||
|
||||
return pageEditClient.prependEdit(
|
||||
|
|
|
|||
|
|
@ -63,9 +63,4 @@ abstract class CommonsDaggerSupportFragment : Fragment(), HasSupportFragmentInje
|
|||
|
||||
return getInstance(activity.applicationContext)
|
||||
}
|
||||
|
||||
// Ensure getContext() returns a non-null Context
|
||||
override fun getContext(): Context {
|
||||
return super.getContext() ?: throw IllegalStateException("Context is null")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class MediaConverter
|
|||
metadata.licenseShortName(),
|
||||
metadata.prefixedLicenseUrl,
|
||||
getAuthor(metadata),
|
||||
getAuthor(metadata),
|
||||
imageInfo.getUser(),
|
||||
MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories()),
|
||||
metadata.latLng,
|
||||
entity.labels().mapValues { it.value.value() },
|
||||
|
|
|
|||
|
|
@ -52,12 +52,7 @@ class SearchImagesViewHolder(
|
|||
binding.categoryImageView.setOnClickListener { onImageClicked(item.second) }
|
||||
binding.categoryImageTitle.text = media.mostRelevantCaption
|
||||
binding.categoryImageView.setImageURI(media.thumbUrl)
|
||||
if (media.author?.isNotEmpty() == true) {
|
||||
binding.categoryImageAuthor.visibility = View.VISIBLE
|
||||
binding.categoryImageAuthor.text =
|
||||
containerView.context.getString(R.string.image_uploaded_by, media.user)
|
||||
} else {
|
||||
binding.categoryImageAuthor.visibility = View.GONE
|
||||
}
|
||||
binding.categoryImageAuthor.text =
|
||||
containerView.context.getString(R.string.image_uploaded_by, media.getAuthorOrUser())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -328,7 +328,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
|||
.append("\n\n");
|
||||
|
||||
builder.append("User that you want to report: ")
|
||||
.append(media.getAuthor())
|
||||
.append(media.getUser())
|
||||
.append("\n\n");
|
||||
|
||||
if (sessionManager.getUserName() != null) {
|
||||
|
|
@ -423,7 +423,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
|||
// Initialize bookmark object
|
||||
bookmark = new Bookmark(
|
||||
m.getFilename(),
|
||||
m.getAuthor(),
|
||||
m.getAuthorOrUser(),
|
||||
BookmarkPicturesContentProvider.uriForName(m.getFilename())
|
||||
);
|
||||
updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image));
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ class ZoomableActivity : BaseActivity() {
|
|||
* Handles down swipe action
|
||||
*/
|
||||
private fun onDownSwiped() {
|
||||
if (binding.zoomable.zoomableController?.isIdentity == false) {
|
||||
if (!binding.zoomable.getZoomableController().isIdentity()) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -342,7 +342,7 @@ class ZoomableActivity : BaseActivity() {
|
|||
* Handles up swipe action
|
||||
*/
|
||||
private fun onUpSwiped() {
|
||||
if (binding.zoomable.zoomableController?.isIdentity == false) {
|
||||
if (!binding.zoomable.getZoomableController().isIdentity()) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -415,7 +415,7 @@ class ZoomableActivity : BaseActivity() {
|
|||
* Handles right swipe action
|
||||
*/
|
||||
private fun onRightSwiped(showAlreadyActionedImages: Boolean) {
|
||||
if (binding.zoomable.zoomableController?.isIdentity == false) {
|
||||
if (!binding.zoomable.getZoomableController().isIdentity()) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -452,7 +452,7 @@ class ZoomableActivity : BaseActivity() {
|
|||
* Handles left swipe action
|
||||
*/
|
||||
private fun onLeftSwiped(showAlreadyActionedImages: Boolean) {
|
||||
if (binding.zoomable.zoomableController?.isIdentity == false) {
|
||||
if (!binding.zoomable.getZoomableController().isIdentity()) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,256 +0,0 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.gestures;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
/**
|
||||
* Component that detects and tracks multiple pointers based on touch events.
|
||||
*
|
||||
* Each time a pointer gets pressed or released, the current gesture (if any) will end, and a new
|
||||
* one will be started (if there are still pressed pointers left). It is guaranteed that the number
|
||||
* of pointers within the single gesture will remain the same during the whole gesture.
|
||||
*/
|
||||
public class MultiPointerGestureDetector {
|
||||
|
||||
/** The listener for receiving notifications when gestures occur. */
|
||||
public interface Listener {
|
||||
/** A callback called right before the gesture is about to start. */
|
||||
public void onGestureBegin(MultiPointerGestureDetector detector);
|
||||
|
||||
/** A callback called each time the gesture gets updated. */
|
||||
public void onGestureUpdate(MultiPointerGestureDetector detector);
|
||||
|
||||
/** A callback called right after the gesture has finished. */
|
||||
public void onGestureEnd(MultiPointerGestureDetector detector);
|
||||
}
|
||||
|
||||
private static final int MAX_POINTERS = 2;
|
||||
|
||||
private boolean mGestureInProgress;
|
||||
private int mPointerCount;
|
||||
private int mNewPointerCount;
|
||||
private final int mId[] = new int[MAX_POINTERS];
|
||||
private final float mStartX[] = new float[MAX_POINTERS];
|
||||
private final float mStartY[] = new float[MAX_POINTERS];
|
||||
private final float mCurrentX[] = new float[MAX_POINTERS];
|
||||
private final float mCurrentY[] = new float[MAX_POINTERS];
|
||||
|
||||
private Listener mListener = null;
|
||||
|
||||
public MultiPointerGestureDetector() {
|
||||
reset();
|
||||
}
|
||||
|
||||
/** Factory method that creates a new instance of MultiPointerGestureDetector */
|
||||
public static MultiPointerGestureDetector newInstance() {
|
||||
return new MultiPointerGestureDetector();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener.
|
||||
*
|
||||
* @param listener listener to set
|
||||
*/
|
||||
public void setListener(Listener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/** Resets the component to the initial state. */
|
||||
public void reset() {
|
||||
mGestureInProgress = false;
|
||||
mPointerCount = 0;
|
||||
for (int i = 0; i < MAX_POINTERS; i++) {
|
||||
mId[i] = MotionEvent.INVALID_POINTER_ID;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method can be overridden in order to perform threshold check or something similar.
|
||||
*
|
||||
* @return whether or not to start a new gesture
|
||||
*/
|
||||
protected boolean shouldStartGesture() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Starts a new gesture and calls the listener just before starting it. */
|
||||
private void startGesture() {
|
||||
if (!mGestureInProgress) {
|
||||
if (mListener != null) {
|
||||
mListener.onGestureBegin(this);
|
||||
}
|
||||
mGestureInProgress = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Stops the current gesture and calls the listener right after stopping it. */
|
||||
private void stopGesture() {
|
||||
if (mGestureInProgress) {
|
||||
mGestureInProgress = false;
|
||||
if (mListener != null) {
|
||||
mListener.onGestureEnd(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the index of the i-th pressed pointer. Normally, the index will be equal to i, except in
|
||||
* the case when the pointer is released.
|
||||
*
|
||||
* @return index of the specified pointer or -1 if not found (i.e. not enough pointers are down)
|
||||
*/
|
||||
private int getPressedPointerIndex(MotionEvent event, int i) {
|
||||
final int count = event.getPointerCount();
|
||||
final int action = event.getActionMasked();
|
||||
final int index = event.getActionIndex();
|
||||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
|
||||
if (i >= index) {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return (i < count) ? i : -1;
|
||||
}
|
||||
|
||||
/** Gets the number of pressed pointers (fingers down). */
|
||||
private static int getPressedPointerCount(MotionEvent event) {
|
||||
int count = event.getPointerCount();
|
||||
int action = event.getActionMasked();
|
||||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
|
||||
count--;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private void updatePointersOnTap(MotionEvent event) {
|
||||
mPointerCount = 0;
|
||||
for (int i = 0; i < MAX_POINTERS; i++) {
|
||||
int index = getPressedPointerIndex(event, i);
|
||||
if (index == -1) {
|
||||
mId[i] = MotionEvent.INVALID_POINTER_ID;
|
||||
} else {
|
||||
mId[i] = event.getPointerId(index);
|
||||
mCurrentX[i] = mStartX[i] = event.getX(index);
|
||||
mCurrentY[i] = mStartY[i] = event.getY(index);
|
||||
mPointerCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePointersOnMove(MotionEvent event) {
|
||||
for (int i = 0; i < MAX_POINTERS; i++) {
|
||||
int index = event.findPointerIndex(mId[i]);
|
||||
if (index != -1) {
|
||||
mCurrentX[i] = event.getX(index);
|
||||
mCurrentY[i] = event.getY(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the given motion event.
|
||||
*
|
||||
* @param event event to handle
|
||||
* @return whether or not the event was handled
|
||||
*/
|
||||
public boolean onTouchEvent(final MotionEvent event) {
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
{
|
||||
// update pointers
|
||||
updatePointersOnMove(event);
|
||||
// start a new gesture if not already started
|
||||
if (!mGestureInProgress && mPointerCount > 0 && shouldStartGesture()) {
|
||||
startGesture();
|
||||
}
|
||||
// notify listener
|
||||
if (mGestureInProgress && mListener != null) {
|
||||
mListener.onGestureUpdate(this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
case MotionEvent.ACTION_UP:
|
||||
{
|
||||
// restart gesture whenever the number of pointers changes
|
||||
mNewPointerCount = getPressedPointerCount(event);
|
||||
stopGesture();
|
||||
updatePointersOnTap(event);
|
||||
if (mPointerCount > 0 && shouldStartGesture()) {
|
||||
startGesture();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
{
|
||||
mNewPointerCount = 0;
|
||||
stopGesture();
|
||||
reset();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Restarts the current gesture (if any). */
|
||||
public void restartGesture() {
|
||||
if (!mGestureInProgress) {
|
||||
return;
|
||||
}
|
||||
stopGesture();
|
||||
for (int i = 0; i < MAX_POINTERS; i++) {
|
||||
mStartX[i] = mCurrentX[i];
|
||||
mStartY[i] = mCurrentY[i];
|
||||
}
|
||||
startGesture();
|
||||
}
|
||||
|
||||
/** Gets whether there is a gesture in progress */
|
||||
public boolean isGestureInProgress() {
|
||||
return mGestureInProgress;
|
||||
}
|
||||
|
||||
/** Gets the number of pointers after the current gesture */
|
||||
public int getNewPointerCount() {
|
||||
return mNewPointerCount;
|
||||
}
|
||||
|
||||
/** Gets the number of pointers in the current gesture */
|
||||
public int getPointerCount() {
|
||||
return mPointerCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the start X coordinates for the all pointers Mutable array is exposed for performance
|
||||
* reasons and is not to be modified by the callers.
|
||||
*/
|
||||
public float[] getStartX() {
|
||||
return mStartX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the start Y coordinates for the all pointers Mutable array is exposed for performance
|
||||
* reasons and is not to be modified by the callers.
|
||||
*/
|
||||
public float[] getStartY() {
|
||||
return mStartY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current X coordinates for the all pointers Mutable array is exposed for performance
|
||||
* reasons and is not to be modified by the callers.
|
||||
*/
|
||||
public float[] getCurrentX() {
|
||||
return mCurrentX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current Y coordinates for the all pointers Mutable array is exposed for performance
|
||||
* reasons and is not to be modified by the callers.
|
||||
*/
|
||||
public float[] getCurrentY() {
|
||||
return mCurrentY;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.gestures
|
||||
|
||||
import android.view.MotionEvent
|
||||
|
||||
/**
|
||||
* Component that detects and tracks multiple pointers based on touch events.
|
||||
*
|
||||
* Each time a pointer gets pressed or released, the current gesture (if any) will end, and a new
|
||||
* one will be started (if there are still pressed pointers left). It is guaranteed that the number
|
||||
* of pointers within the single gesture will remain the same during the whole gesture.
|
||||
*/
|
||||
open class MultiPointerGestureDetector {
|
||||
|
||||
/** The listener for receiving notifications when gestures occur. */
|
||||
interface Listener {
|
||||
/** A callback called right before the gesture is about to start. */
|
||||
fun onGestureBegin(detector: MultiPointerGestureDetector)
|
||||
|
||||
/** A callback called each time the gesture gets updated. */
|
||||
fun onGestureUpdate(detector: MultiPointerGestureDetector)
|
||||
|
||||
/** A callback called right after the gesture has finished. */
|
||||
fun onGestureEnd(detector: MultiPointerGestureDetector)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_POINTERS = 2
|
||||
|
||||
/** Factory method that creates a new instance of MultiPointerGestureDetector */
|
||||
fun newInstance(): MultiPointerGestureDetector {
|
||||
return MultiPointerGestureDetector()
|
||||
}
|
||||
}
|
||||
|
||||
private var mGestureInProgress = false
|
||||
private var mPointerCount = 0
|
||||
private var mNewPointerCount = 0
|
||||
private val mId = IntArray(MAX_POINTERS) { MotionEvent.INVALID_POINTER_ID }
|
||||
private val mStartX = FloatArray(MAX_POINTERS)
|
||||
private val mStartY = FloatArray(MAX_POINTERS)
|
||||
private val mCurrentX = FloatArray(MAX_POINTERS)
|
||||
private val mCurrentY = FloatArray(MAX_POINTERS)
|
||||
|
||||
private var mListener: Listener? = null
|
||||
|
||||
init {
|
||||
reset()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener.
|
||||
*
|
||||
* @param listener listener to set
|
||||
*/
|
||||
fun setListener(listener: Listener?) {
|
||||
mListener = listener
|
||||
}
|
||||
|
||||
/** Resets the component to the initial state. */
|
||||
fun reset() {
|
||||
mGestureInProgress = false
|
||||
mPointerCount = 0
|
||||
for (i in 0 until MAX_POINTERS) {
|
||||
mId[i] = MotionEvent.INVALID_POINTER_ID
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method can be overridden in order to perform threshold check or something similar.
|
||||
*
|
||||
* @return whether or not to start a new gesture
|
||||
*/
|
||||
protected open fun shouldStartGesture(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/** Starts a new gesture and calls the listener just before starting it. */
|
||||
private fun startGesture() {
|
||||
if (!mGestureInProgress) {
|
||||
mListener?.onGestureBegin(this)
|
||||
mGestureInProgress = true
|
||||
}
|
||||
}
|
||||
|
||||
/** Stops the current gesture and calls the listener right after stopping it. */
|
||||
private fun stopGesture() {
|
||||
if (mGestureInProgress) {
|
||||
mGestureInProgress = false
|
||||
mListener?.onGestureEnd(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the index of the i-th pressed pointer. Normally, the index will be equal to i, except in
|
||||
* the case when the pointer is released.
|
||||
*
|
||||
* @return index of the specified pointer or -1 if not found (i.e. not enough pointers are down)
|
||||
*/
|
||||
private fun getPressedPointerIndex(event: MotionEvent, i: Int): Int {
|
||||
val count = event.pointerCount
|
||||
val action = event.actionMasked
|
||||
val index = event.actionIndex
|
||||
var adjustedIndex = i
|
||||
|
||||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
|
||||
if (adjustedIndex >= index) {
|
||||
adjustedIndex++
|
||||
}
|
||||
}
|
||||
return if (adjustedIndex < count) adjustedIndex else -1
|
||||
}
|
||||
|
||||
/** Gets the number of pressed pointers (fingers down). */
|
||||
private fun getPressedPointerCount(event: MotionEvent): Int {
|
||||
var count = event.pointerCount
|
||||
val action = event.actionMasked
|
||||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
|
||||
count--
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
private fun updatePointersOnTap(event: MotionEvent) {
|
||||
mPointerCount = 0
|
||||
for (i in 0 until MAX_POINTERS) {
|
||||
val index = getPressedPointerIndex(event, i)
|
||||
if (index == -1) {
|
||||
mId[i] = MotionEvent.INVALID_POINTER_ID
|
||||
} else {
|
||||
mId[i] = event.getPointerId(index)
|
||||
mCurrentX[i] = event.getX(index)
|
||||
mStartX[i] = mCurrentX[i]
|
||||
mCurrentY[i] = event.getY(index)
|
||||
mStartY[i] = mCurrentY[i]
|
||||
mPointerCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePointersOnMove(event: MotionEvent) {
|
||||
for (i in 0 until MAX_POINTERS) {
|
||||
val index = event.findPointerIndex(mId[i])
|
||||
if (index != -1) {
|
||||
mCurrentX[i] = event.getX(index)
|
||||
mCurrentY[i] = event.getY(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the given motion event.
|
||||
*
|
||||
* @param event event to handle
|
||||
* @return whether or not the event was handled
|
||||
*/
|
||||
fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
// update pointers
|
||||
updatePointersOnMove(event)
|
||||
// start a new gesture if not already started
|
||||
if (!mGestureInProgress && mPointerCount > 0 && shouldStartGesture()) {
|
||||
startGesture()
|
||||
}
|
||||
// notify listener
|
||||
if (mGestureInProgress) {
|
||||
mListener?.onGestureUpdate(this)
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_DOWN,
|
||||
MotionEvent.ACTION_POINTER_DOWN,
|
||||
MotionEvent.ACTION_POINTER_UP,
|
||||
MotionEvent.ACTION_UP -> {
|
||||
// restart gesture whenever the number of pointers changes
|
||||
mNewPointerCount = getPressedPointerCount(event)
|
||||
stopGesture()
|
||||
updatePointersOnTap(event)
|
||||
if (mPointerCount > 0 && shouldStartGesture()) {
|
||||
startGesture()
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
mNewPointerCount = 0
|
||||
stopGesture()
|
||||
reset()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/** Restarts the current gesture (if any). */
|
||||
fun restartGesture() {
|
||||
if (!mGestureInProgress) {
|
||||
return
|
||||
}
|
||||
stopGesture()
|
||||
for (i in 0 until MAX_POINTERS) {
|
||||
mStartX[i] = mCurrentX[i]
|
||||
mStartY[i] = mCurrentY[i]
|
||||
}
|
||||
startGesture()
|
||||
}
|
||||
|
||||
/** Gets whether there is a gesture in progress */
|
||||
fun isGestureInProgress(): Boolean {
|
||||
return mGestureInProgress
|
||||
}
|
||||
|
||||
/** Gets the number of pointers after the current gesture */
|
||||
fun getNewPointerCount(): Int {
|
||||
return mNewPointerCount
|
||||
}
|
||||
|
||||
/** Gets the number of pointers in the current gesture */
|
||||
fun getPointerCount(): Int {
|
||||
return mPointerCount
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the start X coordinates for all pointers Mutable array is exposed for performance
|
||||
* reasons and is not to be modified by the callers.
|
||||
*/
|
||||
fun getStartX(): FloatArray {
|
||||
return mStartX
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the start Y coordinates for all pointers Mutable array is exposed for performance
|
||||
* reasons and is not to be modified by the callers.
|
||||
*/
|
||||
fun getStartY(): FloatArray {
|
||||
return mStartY
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current X coordinates for all pointers Mutable array is exposed for performance
|
||||
* reasons and is not to be modified by the callers.
|
||||
*/
|
||||
fun getCurrentX(): FloatArray {
|
||||
return mCurrentX
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current Y coordinates for all pointers Mutable array is exposed for performance
|
||||
* reasons and is not to be modified by the callers.
|
||||
*/
|
||||
fun getCurrentY(): FloatArray {
|
||||
return mCurrentY
|
||||
}
|
||||
}
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.gestures;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
/**
|
||||
* Component that detects translation, scale and rotation based on touch events.
|
||||
*
|
||||
* This class notifies its listeners whenever a gesture begins, updates or ends. The instance of
|
||||
* this detector is passed to the listeners, so it can be queried for pivot, translation, scale or
|
||||
* rotation.
|
||||
*/
|
||||
public class TransformGestureDetector implements MultiPointerGestureDetector.Listener {
|
||||
|
||||
/** The listener for receiving notifications when gestures occur. */
|
||||
public interface Listener {
|
||||
/** A callback called right before the gesture is about to start. */
|
||||
public void onGestureBegin(TransformGestureDetector detector);
|
||||
|
||||
/** A callback called each time the gesture gets updated. */
|
||||
public void onGestureUpdate(TransformGestureDetector detector);
|
||||
|
||||
/** A callback called right after the gesture has finished. */
|
||||
public void onGestureEnd(TransformGestureDetector detector);
|
||||
}
|
||||
|
||||
private final MultiPointerGestureDetector mDetector;
|
||||
|
||||
private Listener mListener = null;
|
||||
|
||||
public TransformGestureDetector(MultiPointerGestureDetector multiPointerGestureDetector) {
|
||||
mDetector = multiPointerGestureDetector;
|
||||
mDetector.setListener(this);
|
||||
}
|
||||
|
||||
/** Factory method that creates a new instance of TransformGestureDetector */
|
||||
public static TransformGestureDetector newInstance() {
|
||||
return new TransformGestureDetector(MultiPointerGestureDetector.newInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener.
|
||||
*
|
||||
* @param listener listener to set
|
||||
*/
|
||||
public void setListener(Listener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/** Resets the component to the initial state. */
|
||||
public void reset() {
|
||||
mDetector.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the given motion event.
|
||||
*
|
||||
* @param event event to handle
|
||||
* @return whether or not the event was handled
|
||||
*/
|
||||
public boolean onTouchEvent(final MotionEvent event) {
|
||||
return mDetector.onTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGestureBegin(MultiPointerGestureDetector detector) {
|
||||
if (mListener != null) {
|
||||
mListener.onGestureBegin(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGestureUpdate(MultiPointerGestureDetector detector) {
|
||||
if (mListener != null) {
|
||||
mListener.onGestureUpdate(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGestureEnd(MultiPointerGestureDetector detector) {
|
||||
if (mListener != null) {
|
||||
mListener.onGestureEnd(this);
|
||||
}
|
||||
}
|
||||
|
||||
private float calcAverage(float[] arr, int len) {
|
||||
float sum = 0;
|
||||
for (int i = 0; i < len; i++) {
|
||||
sum += arr[i];
|
||||
}
|
||||
return (len > 0) ? sum / len : 0;
|
||||
}
|
||||
|
||||
/** Restarts the current gesture (if any). */
|
||||
public void restartGesture() {
|
||||
mDetector.restartGesture();
|
||||
}
|
||||
|
||||
/** Gets whether there is a gesture in progress */
|
||||
public boolean isGestureInProgress() {
|
||||
return mDetector.isGestureInProgress();
|
||||
}
|
||||
|
||||
/** Gets the number of pointers after the current gesture */
|
||||
public int getNewPointerCount() {
|
||||
return mDetector.getNewPointerCount();
|
||||
}
|
||||
|
||||
/** Gets the number of pointers in the current gesture */
|
||||
public int getPointerCount() {
|
||||
return mDetector.getPointerCount();
|
||||
}
|
||||
|
||||
/** Gets the X coordinate of the pivot point */
|
||||
public float getPivotX() {
|
||||
return calcAverage(mDetector.getStartX(), mDetector.getPointerCount());
|
||||
}
|
||||
|
||||
/** Gets the Y coordinate of the pivot point */
|
||||
public float getPivotY() {
|
||||
return calcAverage(mDetector.getStartY(), mDetector.getPointerCount());
|
||||
}
|
||||
|
||||
/** Gets the X component of the translation */
|
||||
public float getTranslationX() {
|
||||
return calcAverage(mDetector.getCurrentX(), mDetector.getPointerCount())
|
||||
- calcAverage(mDetector.getStartX(), mDetector.getPointerCount());
|
||||
}
|
||||
|
||||
/** Gets the Y component of the translation */
|
||||
public float getTranslationY() {
|
||||
return calcAverage(mDetector.getCurrentY(), mDetector.getPointerCount())
|
||||
- calcAverage(mDetector.getStartY(), mDetector.getPointerCount());
|
||||
}
|
||||
|
||||
/** Gets the scale */
|
||||
public float getScale() {
|
||||
if (mDetector.getPointerCount() < 2) {
|
||||
return 1;
|
||||
} else {
|
||||
float startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0];
|
||||
float startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0];
|
||||
float currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0];
|
||||
float currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0];
|
||||
float startDist = (float) Math.hypot(startDeltaX, startDeltaY);
|
||||
float currentDist = (float) Math.hypot(currentDeltaX, currentDeltaY);
|
||||
return currentDist / startDist;
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets the rotation in radians */
|
||||
public float getRotation() {
|
||||
if (mDetector.getPointerCount() < 2) {
|
||||
return 0;
|
||||
} else {
|
||||
float startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0];
|
||||
float startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0];
|
||||
float currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0];
|
||||
float currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0];
|
||||
float startAngle = (float) Math.atan2(startDeltaY, startDeltaX);
|
||||
float currentAngle = (float) Math.atan2(currentDeltaY, currentDeltaX);
|
||||
return currentAngle - startAngle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.gestures
|
||||
|
||||
import android.view.MotionEvent
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.hypot
|
||||
|
||||
/**
|
||||
* Component that detects translation, scale and rotation based on touch events.
|
||||
*
|
||||
* This class notifies its listeners whenever a gesture begins, updates or ends. The instance of
|
||||
* this detector is passed to the listeners, so it can be queried for pivot, translation, scale or
|
||||
* rotation.
|
||||
*/
|
||||
class TransformGestureDetector(private val mDetector: MultiPointerGestureDetector) :
|
||||
MultiPointerGestureDetector.Listener {
|
||||
|
||||
/** The listener for receiving notifications when gestures occur. */
|
||||
interface Listener {
|
||||
/** A callback called right before the gesture is about to start. */
|
||||
fun onGestureBegin(detector: TransformGestureDetector)
|
||||
|
||||
/** A callback called each time the gesture gets updated. */
|
||||
fun onGestureUpdate(detector: TransformGestureDetector)
|
||||
|
||||
/** A callback called right after the gesture has finished. */
|
||||
fun onGestureEnd(detector: TransformGestureDetector)
|
||||
}
|
||||
|
||||
private var mListener: Listener? = null
|
||||
|
||||
init {
|
||||
mDetector.setListener(this)
|
||||
}
|
||||
|
||||
/** Factory method that creates a new instance of TransformGestureDetector */
|
||||
companion object {
|
||||
fun newInstance(): TransformGestureDetector {
|
||||
return TransformGestureDetector(MultiPointerGestureDetector.newInstance())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener.
|
||||
*
|
||||
* @param listener listener to set
|
||||
*/
|
||||
fun setListener(listener: Listener?) {
|
||||
mListener = listener
|
||||
}
|
||||
|
||||
/** Resets the component to the initial state. */
|
||||
fun reset() {
|
||||
mDetector.reset()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the given motion event.
|
||||
*
|
||||
* @param event event to handle
|
||||
* @return whether or not the event was handled
|
||||
*/
|
||||
fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
return mDetector.onTouchEvent(event)
|
||||
}
|
||||
|
||||
override fun onGestureBegin(detector: MultiPointerGestureDetector) {
|
||||
mListener?.onGestureBegin(this)
|
||||
}
|
||||
|
||||
override fun onGestureUpdate(detector: MultiPointerGestureDetector) {
|
||||
mListener?.onGestureUpdate(this)
|
||||
}
|
||||
|
||||
override fun onGestureEnd(detector: MultiPointerGestureDetector) {
|
||||
mListener?.onGestureEnd(this)
|
||||
}
|
||||
|
||||
private fun calcAverage(arr: FloatArray, len: Int): Float {
|
||||
val sum = arr.take(len).sum()
|
||||
return if (len > 0) sum / len else 0f
|
||||
}
|
||||
|
||||
/** Restarts the current gesture (if any). */
|
||||
fun restartGesture() {
|
||||
mDetector.restartGesture()
|
||||
}
|
||||
|
||||
/** Gets whether there is a gesture in progress */
|
||||
fun isGestureInProgress(): Boolean {
|
||||
return mDetector.isGestureInProgress()
|
||||
}
|
||||
|
||||
/** Gets the number of pointers after the current gesture */
|
||||
fun getNewPointerCount(): Int {
|
||||
return mDetector.getNewPointerCount()
|
||||
}
|
||||
|
||||
/** Gets the number of pointers in the current gesture */
|
||||
fun getPointerCount(): Int {
|
||||
return mDetector.getPointerCount()
|
||||
}
|
||||
|
||||
/** Gets the X coordinate of the pivot point */
|
||||
fun getPivotX(): Float {
|
||||
return calcAverage(mDetector.getStartX(), mDetector.getPointerCount())
|
||||
}
|
||||
|
||||
/** Gets the Y coordinate of the pivot point */
|
||||
fun getPivotY(): Float {
|
||||
return calcAverage(mDetector.getStartY(), mDetector.getPointerCount())
|
||||
}
|
||||
|
||||
/** Gets the X component of the translation */
|
||||
fun getTranslationX(): Float {
|
||||
return calcAverage(mDetector.getCurrentX(), mDetector.getPointerCount()) -
|
||||
calcAverage(mDetector.getStartX(), mDetector.getPointerCount())
|
||||
}
|
||||
|
||||
/** Gets the Y component of the translation */
|
||||
fun getTranslationY(): Float {
|
||||
return calcAverage(mDetector.getCurrentY(), mDetector.getPointerCount()) -
|
||||
calcAverage(mDetector.getStartY(), mDetector.getPointerCount())
|
||||
}
|
||||
|
||||
/** Gets the scale */
|
||||
fun getScale(): Float {
|
||||
return if (mDetector.getPointerCount() < 2) {
|
||||
1f
|
||||
} else {
|
||||
val startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0]
|
||||
val startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0]
|
||||
val currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0]
|
||||
val currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0]
|
||||
val startDist = hypot(startDeltaX, startDeltaY)
|
||||
val currentDist = hypot(currentDeltaX, currentDeltaY)
|
||||
currentDist / startDist
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets the rotation in radians */
|
||||
fun getRotation(): Float {
|
||||
return if (mDetector.getPointerCount() < 2) {
|
||||
0f
|
||||
} else {
|
||||
val startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0]
|
||||
val startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0]
|
||||
val currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0]
|
||||
val currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0]
|
||||
val startAngle = atan2(startDeltaY, startDeltaX)
|
||||
val currentAngle = atan2(currentDeltaY, currentDeltaX)
|
||||
currentAngle - startAngle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.zoomable;
|
||||
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.PointF;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import androidx.annotation.Nullable;
|
||||
import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector;
|
||||
|
||||
/**
|
||||
* Abstract class for ZoomableController that adds animation capabilities to
|
||||
* DefaultZoomableController.
|
||||
*/
|
||||
public abstract class AbstractAnimatedZoomableController extends DefaultZoomableController {
|
||||
|
||||
private boolean mIsAnimating;
|
||||
private final float[] mStartValues = new float[9];
|
||||
private final float[] mStopValues = new float[9];
|
||||
private final float[] mCurrentValues = new float[9];
|
||||
private final Matrix mNewTransform = new Matrix();
|
||||
private final Matrix mWorkingTransform = new Matrix();
|
||||
|
||||
public AbstractAnimatedZoomableController(TransformGestureDetector transformGestureDetector) {
|
||||
super(transformGestureDetector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
FLog.v(getLogTag(), "reset");
|
||||
stopAnimation();
|
||||
mWorkingTransform.reset();
|
||||
mNewTransform.reset();
|
||||
super.reset();
|
||||
}
|
||||
|
||||
/** Returns true if the zoomable transform is identity matrix, and the controller is idle. */
|
||||
@Override
|
||||
public boolean isIdentity() {
|
||||
return !isAnimating() && super.isIdentity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms to the desired scale and positions the image so that the given image point corresponds to
|
||||
* the given view point.
|
||||
*
|
||||
* <p>If this method is called while an animation or gesture is already in progress, the current
|
||||
* animation or gesture will be stopped first.
|
||||
*
|
||||
* @param scale desired scale, will be limited to {min, max} scale factor
|
||||
* @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1)
|
||||
* @param viewPoint 2D point in view's absolute coordinate system
|
||||
*/
|
||||
@Override
|
||||
public void zoomToPoint(float scale, PointF imagePoint, PointF viewPoint) {
|
||||
zoomToPoint(scale, imagePoint, viewPoint, LIMIT_ALL, 0, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms to the desired scale and positions the image so that the given image point corresponds to
|
||||
* the given view point.
|
||||
*
|
||||
* <p>If this method is called while an animation or gesture is already in progress, the current
|
||||
* animation or gesture will be stopped first.
|
||||
*
|
||||
* @param scale desired scale, will be limited to {min, max} scale factor
|
||||
* @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1)
|
||||
* @param viewPoint 2D point in view's absolute coordinate system
|
||||
* @param limitFlags whether to limit translation and/or scale.
|
||||
* @param durationMs length of animation of the zoom, or 0 if no animation desired
|
||||
* @param onAnimationComplete code to run when the animation completes. Ignored if durationMs=0
|
||||
*/
|
||||
public void zoomToPoint(
|
||||
float scale,
|
||||
PointF imagePoint,
|
||||
PointF viewPoint,
|
||||
@LimitFlag int limitFlags,
|
||||
long durationMs,
|
||||
@Nullable Runnable onAnimationComplete) {
|
||||
FLog.v(getLogTag(), "zoomToPoint: duration %d ms", durationMs);
|
||||
calculateZoomToPointTransform(mNewTransform, scale, imagePoint, viewPoint, limitFlags);
|
||||
setTransform(mNewTransform, durationMs, onAnimationComplete);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new zoomable transformation and animates to it if desired.
|
||||
*
|
||||
* <p>If this method is called while an animation or gesture is already in progress, the current
|
||||
* animation or gesture will be stopped first.
|
||||
*
|
||||
* @param newTransform new transform to make active
|
||||
* @param durationMs duration of the animation, or 0 to not animate
|
||||
* @param onAnimationComplete code to run when the animation completes. Ignored if durationMs=0
|
||||
*/
|
||||
public void setTransform(
|
||||
Matrix newTransform, long durationMs, @Nullable Runnable onAnimationComplete) {
|
||||
FLog.v(getLogTag(), "setTransform: duration %d ms", durationMs);
|
||||
if (durationMs <= 0) {
|
||||
setTransformImmediate(newTransform);
|
||||
} else {
|
||||
setTransformAnimated(newTransform, durationMs, onAnimationComplete);
|
||||
}
|
||||
}
|
||||
|
||||
private void setTransformImmediate(final Matrix newTransform) {
|
||||
FLog.v(getLogTag(), "setTransformImmediate");
|
||||
stopAnimation();
|
||||
mWorkingTransform.set(newTransform);
|
||||
super.setTransform(newTransform);
|
||||
getDetector().restartGesture();
|
||||
}
|
||||
|
||||
protected boolean isAnimating() {
|
||||
return mIsAnimating;
|
||||
}
|
||||
|
||||
protected void setAnimating(boolean isAnimating) {
|
||||
mIsAnimating = isAnimating;
|
||||
}
|
||||
|
||||
protected float[] getStartValues() {
|
||||
return mStartValues;
|
||||
}
|
||||
|
||||
protected float[] getStopValues() {
|
||||
return mStopValues;
|
||||
}
|
||||
|
||||
protected Matrix getWorkingTransform() {
|
||||
return mWorkingTransform;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGestureBegin(TransformGestureDetector detector) {
|
||||
FLog.v(getLogTag(), "onGestureBegin");
|
||||
stopAnimation();
|
||||
super.onGestureBegin(detector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGestureUpdate(TransformGestureDetector detector) {
|
||||
FLog.v(getLogTag(), "onGestureUpdate %s", isAnimating() ? "(ignored)" : "");
|
||||
if (isAnimating()) {
|
||||
return;
|
||||
}
|
||||
super.onGestureUpdate(detector);
|
||||
}
|
||||
|
||||
protected void calculateInterpolation(Matrix outMatrix, float fraction) {
|
||||
for (int i = 0; i < 9; i++) {
|
||||
mCurrentValues[i] = (1 - fraction) * mStartValues[i] + fraction * mStopValues[i];
|
||||
}
|
||||
outMatrix.setValues(mCurrentValues);
|
||||
}
|
||||
|
||||
public abstract void setTransformAnimated(
|
||||
final Matrix newTransform, long durationMs, @Nullable final Runnable onAnimationComplete);
|
||||
|
||||
protected abstract void stopAnimation();
|
||||
|
||||
protected abstract Class<?> getLogTag();
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.zoomable
|
||||
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.PointF
|
||||
import com.facebook.common.logging.FLog
|
||||
import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector
|
||||
|
||||
/**
|
||||
* Abstract class for ZoomableController that adds animation capabilities to
|
||||
* DefaultZoomableController.
|
||||
*/
|
||||
abstract class AbstractAnimatedZoomableController(
|
||||
transformGestureDetector: TransformGestureDetector
|
||||
) : DefaultZoomableController(transformGestureDetector) {
|
||||
|
||||
private var isAnimating: Boolean = false
|
||||
private val startValues = FloatArray(9)
|
||||
private val stopValues = FloatArray(9)
|
||||
private val currentValues = FloatArray(9)
|
||||
private val newTransform = Matrix()
|
||||
private val workingTransform = Matrix()
|
||||
|
||||
override fun reset() {
|
||||
FLog.v(logTag, "reset")
|
||||
stopAnimation()
|
||||
workingTransform.reset()
|
||||
newTransform.reset()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
/** Returns true if the zoomable transform is identity matrix, and the controller is idle. */
|
||||
override fun isIdentity(): Boolean {
|
||||
return !isAnimating && super.isIdentity()
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms to the desired scale and positions the image so that the given image point corresponds
|
||||
* to the given view point.
|
||||
*
|
||||
* If this method is called while an animation or gesture is already in progress, the current
|
||||
* animation or gesture will be stopped first.
|
||||
*
|
||||
* @param scale desired scale, will be limited to {min, max} scale factor
|
||||
* @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1)
|
||||
* @param viewPoint 2D point in view's absolute coordinate system
|
||||
*/
|
||||
override fun zoomToPoint(scale: Float, imagePoint: PointF, viewPoint: PointF) {
|
||||
zoomToPoint(scale, imagePoint, viewPoint, LIMIT_ALL, 0, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms to the desired scale and positions the image so that the given image point corresponds
|
||||
* to the given view point.
|
||||
*
|
||||
* If this method is called while an animation or gesture is already in progress, the current
|
||||
* animation or gesture will be stopped first.
|
||||
*
|
||||
* @param scale desired scale, will be limited to {min, max} scale factor
|
||||
* @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1)
|
||||
* @param viewPoint 2D point in view's absolute coordinate system
|
||||
* @param limitFlags whether to limit translation and/or scale.
|
||||
* @param durationMs length of animation of the zoom, or 0 if no animation desired
|
||||
* @param onAnimationComplete code to run when the animation completes. Ignored if durationMs=0
|
||||
*/
|
||||
fun zoomToPoint(
|
||||
scale: Float,
|
||||
imagePoint: PointF,
|
||||
viewPoint: PointF,
|
||||
@LimitFlag limitFlags: Int,
|
||||
durationMs: Long,
|
||||
onAnimationComplete: Runnable?
|
||||
) {
|
||||
FLog.v(logTag, "zoomToPoint: duration $durationMs ms")
|
||||
calculateZoomToPointTransform(newTransform, scale, imagePoint, viewPoint, limitFlags)
|
||||
setTransform(newTransform, durationMs, onAnimationComplete)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new zoomable transformation and animates to it if desired.
|
||||
*
|
||||
* If this method is called while an animation or gesture is already in progress, the current
|
||||
* animation or gesture will be stopped first.
|
||||
*
|
||||
* @param newTransform new transform to make active
|
||||
* @param durationMs duration of the animation, or 0 to not animate
|
||||
* @param onAnimationComplete code to run when the animation completes. Ignored if durationMs=0
|
||||
*/
|
||||
private fun setTransform(
|
||||
newTransform: Matrix,
|
||||
durationMs: Long,
|
||||
onAnimationComplete: Runnable?
|
||||
) {
|
||||
FLog.v(logTag, "setTransform: duration $durationMs ms")
|
||||
if (durationMs <= 0) {
|
||||
setTransformImmediate(newTransform)
|
||||
} else {
|
||||
setTransformAnimated(newTransform, durationMs, onAnimationComplete)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setTransformImmediate(newTransform: Matrix) {
|
||||
FLog.v(logTag, "setTransformImmediate")
|
||||
stopAnimation()
|
||||
workingTransform.set(newTransform)
|
||||
super.setTransform(newTransform)
|
||||
getDetector().restartGesture()
|
||||
}
|
||||
|
||||
protected fun getIsAnimating(): Boolean = isAnimating
|
||||
|
||||
protected fun setAnimating(isAnimating: Boolean) {
|
||||
this.isAnimating = isAnimating
|
||||
}
|
||||
|
||||
protected fun getStartValues(): FloatArray = startValues
|
||||
|
||||
protected fun getStopValues(): FloatArray = stopValues
|
||||
|
||||
protected fun getWorkingTransform(): Matrix = workingTransform
|
||||
|
||||
override fun onGestureBegin(detector: TransformGestureDetector) {
|
||||
FLog.v(logTag, "onGestureBegin")
|
||||
stopAnimation()
|
||||
super.onGestureBegin(detector)
|
||||
}
|
||||
|
||||
override fun onGestureUpdate(detector: TransformGestureDetector) {
|
||||
FLog.v(logTag, "onGestureUpdate ${if (isAnimating) "(ignored)" else ""}")
|
||||
if (isAnimating) return
|
||||
super.onGestureUpdate(detector)
|
||||
}
|
||||
|
||||
protected fun calculateInterpolation(outMatrix: Matrix, fraction: Float) {
|
||||
for (i in 0..8) {
|
||||
currentValues[i] = (1 - fraction) * startValues[i] + fraction * stopValues[i]
|
||||
}
|
||||
outMatrix.setValues(currentValues)
|
||||
}
|
||||
|
||||
abstract fun setTransformAnimated(
|
||||
newTransform: Matrix, durationMs: Long, onAnimationComplete: Runnable?
|
||||
)
|
||||
|
||||
protected abstract fun stopAnimation()
|
||||
|
||||
protected abstract val logTag: Class<*>
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.zoomable;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.graphics.Matrix;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import com.facebook.common.internal.Preconditions;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import androidx.annotation.Nullable;
|
||||
import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector;
|
||||
|
||||
/**
|
||||
* ZoomableController that adds animation capabilities to DefaultZoomableController using standard
|
||||
* Android animation classes
|
||||
*/
|
||||
public class AnimatedZoomableController extends AbstractAnimatedZoomableController {
|
||||
|
||||
private static final Class<?> TAG = AnimatedZoomableController.class;
|
||||
|
||||
private final ValueAnimator mValueAnimator;
|
||||
|
||||
public static AnimatedZoomableController newInstance() {
|
||||
return new AnimatedZoomableController(TransformGestureDetector.newInstance());
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public AnimatedZoomableController(TransformGestureDetector transformGestureDetector) {
|
||||
super(transformGestureDetector);
|
||||
mValueAnimator = ValueAnimator.ofFloat(0, 1);
|
||||
mValueAnimator.setInterpolator(new DecelerateInterpolator());
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@Override
|
||||
public void setTransformAnimated(
|
||||
final Matrix newTransform, long durationMs, @Nullable final Runnable onAnimationComplete) {
|
||||
FLog.v(getLogTag(), "setTransformAnimated: duration %d ms", durationMs);
|
||||
stopAnimation();
|
||||
Preconditions.checkArgument(durationMs > 0);
|
||||
Preconditions.checkState(!isAnimating());
|
||||
setAnimating(true);
|
||||
mValueAnimator.setDuration(durationMs);
|
||||
getTransform().getValues(getStartValues());
|
||||
newTransform.getValues(getStopValues());
|
||||
mValueAnimator.addUpdateListener(
|
||||
new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator valueAnimator) {
|
||||
calculateInterpolation(getWorkingTransform(), (float) valueAnimator.getAnimatedValue());
|
||||
AnimatedZoomableController.super.setTransform(getWorkingTransform());
|
||||
}
|
||||
});
|
||||
mValueAnimator.addListener(
|
||||
new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {
|
||||
FLog.v(getLogTag(), "setTransformAnimated: animation cancelled");
|
||||
onAnimationStopped();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
FLog.v(getLogTag(), "setTransformAnimated: animation finished");
|
||||
onAnimationStopped();
|
||||
}
|
||||
|
||||
private void onAnimationStopped() {
|
||||
if (onAnimationComplete != null) {
|
||||
onAnimationComplete.run();
|
||||
}
|
||||
setAnimating(false);
|
||||
getDetector().restartGesture();
|
||||
}
|
||||
});
|
||||
mValueAnimator.start();
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@Override
|
||||
public void stopAnimation() {
|
||||
if (!isAnimating()) {
|
||||
return;
|
||||
}
|
||||
FLog.v(getLogTag(), "stopAnimation");
|
||||
mValueAnimator.cancel();
|
||||
mValueAnimator.removeAllUpdateListeners();
|
||||
mValueAnimator.removeAllListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> getLogTag() {
|
||||
return TAG;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.zoomable
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ValueAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Matrix
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import com.facebook.common.logging.FLog
|
||||
import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector
|
||||
|
||||
/**
|
||||
* ZoomableController that adds animation capabilities to DefaultZoomableController using standard
|
||||
* Android animation classes
|
||||
*/
|
||||
class AnimatedZoomableController private constructor() :
|
||||
AbstractAnimatedZoomableController(TransformGestureDetector.newInstance()) {
|
||||
|
||||
private val valueAnimator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
interpolator = DecelerateInterpolator()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(): AnimatedZoomableController {
|
||||
return AnimatedZoomableController()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun setTransformAnimated(
|
||||
newTransform: Matrix, durationMs: Long, onAnimationComplete: Runnable?
|
||||
) {
|
||||
FLog.v(logTag, "setTransformAnimated: duration $durationMs ms")
|
||||
stopAnimation()
|
||||
require(durationMs > 0) { "Duration must be greater than zero" }
|
||||
check(!getIsAnimating()) { "Animation is already in progress" }
|
||||
setAnimating(true)
|
||||
valueAnimator.duration = durationMs
|
||||
getTransform().getValues(getStartValues())
|
||||
newTransform.getValues(getStopValues())
|
||||
valueAnimator.addUpdateListener { animator ->
|
||||
calculateInterpolation(getWorkingTransform(), animator.animatedValue as Float)
|
||||
super.setTransform(getWorkingTransform())
|
||||
}
|
||||
valueAnimator.addListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationCancel(animation: Animator) {
|
||||
FLog.v(logTag, "setTransformAnimated: animation cancelled")
|
||||
onAnimationStopped()
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
FLog.v(logTag, "setTransformAnimated: animation finished")
|
||||
onAnimationStopped()
|
||||
}
|
||||
|
||||
private fun onAnimationStopped() {
|
||||
onAnimationComplete?.run()
|
||||
setAnimating(false)
|
||||
getDetector().restartGesture()
|
||||
}
|
||||
})
|
||||
valueAnimator.start()
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun stopAnimation() {
|
||||
if (!getIsAnimating()) return
|
||||
FLog.v(logTag, "stopAnimation")
|
||||
valueAnimator.cancel()
|
||||
valueAnimator.removeAllUpdateListeners()
|
||||
valueAnimator.removeAllListeners()
|
||||
}
|
||||
|
||||
override val logTag: Class<*> = AnimatedZoomableController::class.java
|
||||
}
|
||||
|
|
@ -1,646 +0,0 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.zoomable;
|
||||
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.RectF;
|
||||
import android.view.MotionEvent;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/** Zoomable controller that calculates transformation based on touch events. */
|
||||
public class DefaultZoomableController
|
||||
implements ZoomableController, TransformGestureDetector.Listener {
|
||||
|
||||
/** Interface for handling call backs when the image bounds are set. */
|
||||
public interface ImageBoundsListener {
|
||||
void onImageBoundsSet(RectF imageBounds);
|
||||
}
|
||||
|
||||
@IntDef(
|
||||
flag = true,
|
||||
value = {LIMIT_NONE, LIMIT_TRANSLATION_X, LIMIT_TRANSLATION_Y, LIMIT_SCALE, LIMIT_ALL})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface LimitFlag {}
|
||||
|
||||
public static final int LIMIT_NONE = 0;
|
||||
public static final int LIMIT_TRANSLATION_X = 1;
|
||||
public static final int LIMIT_TRANSLATION_Y = 2;
|
||||
public static final int LIMIT_SCALE = 4;
|
||||
public static final int LIMIT_ALL = LIMIT_TRANSLATION_X | LIMIT_TRANSLATION_Y | LIMIT_SCALE;
|
||||
|
||||
private static final float EPS = 1e-3f;
|
||||
|
||||
private static final Class<?> TAG = DefaultZoomableController.class;
|
||||
|
||||
private static final RectF IDENTITY_RECT = new RectF(0, 0, 1, 1);
|
||||
|
||||
private TransformGestureDetector mGestureDetector;
|
||||
|
||||
private @Nullable ImageBoundsListener mImageBoundsListener;
|
||||
|
||||
private @Nullable Listener mListener = null;
|
||||
|
||||
private boolean mIsEnabled = false;
|
||||
private boolean mIsRotationEnabled = false;
|
||||
private boolean mIsScaleEnabled = true;
|
||||
private boolean mIsTranslationEnabled = true;
|
||||
private boolean mIsGestureZoomEnabled = true;
|
||||
|
||||
private float mMinScaleFactor = 1.0f;
|
||||
private float mMaxScaleFactor = 2.0f;
|
||||
|
||||
// View bounds, in view-absolute coordinates
|
||||
private final RectF mViewBounds = new RectF();
|
||||
// Non-transformed image bounds, in view-absolute coordinates
|
||||
private final RectF mImageBounds = new RectF();
|
||||
// Transformed image bounds, in view-absolute coordinates
|
||||
private final RectF mTransformedImageBounds = new RectF();
|
||||
|
||||
private final Matrix mPreviousTransform = new Matrix();
|
||||
private final Matrix mActiveTransform = new Matrix();
|
||||
private final Matrix mActiveTransformInverse = new Matrix();
|
||||
private final float[] mTempValues = new float[9];
|
||||
private final RectF mTempRect = new RectF();
|
||||
private boolean mWasTransformCorrected;
|
||||
|
||||
public static DefaultZoomableController newInstance() {
|
||||
return new DefaultZoomableController(TransformGestureDetector.newInstance());
|
||||
}
|
||||
|
||||
public DefaultZoomableController(TransformGestureDetector gestureDetector) {
|
||||
mGestureDetector = gestureDetector;
|
||||
mGestureDetector.setListener(this);
|
||||
}
|
||||
|
||||
/** Rests the controller. */
|
||||
public void reset() {
|
||||
FLog.v(TAG, "reset");
|
||||
mGestureDetector.reset();
|
||||
mPreviousTransform.reset();
|
||||
mActiveTransform.reset();
|
||||
onTransformChanged();
|
||||
}
|
||||
|
||||
/** Sets the zoomable listener. */
|
||||
@Override
|
||||
public void setListener(Listener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/** Sets whether the controller is enabled or not. */
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
mIsEnabled = enabled;
|
||||
if (!enabled) {
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets whether the controller is enabled or not. */
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return mIsEnabled;
|
||||
}
|
||||
|
||||
/** Sets whether the rotation gesture is enabled or not. */
|
||||
public void setRotationEnabled(boolean enabled) {
|
||||
mIsRotationEnabled = enabled;
|
||||
}
|
||||
|
||||
/** Gets whether the rotation gesture is enabled or not. */
|
||||
public boolean isRotationEnabled() {
|
||||
return mIsRotationEnabled;
|
||||
}
|
||||
|
||||
/** Sets whether the scale gesture is enabled or not. */
|
||||
public void setScaleEnabled(boolean enabled) {
|
||||
mIsScaleEnabled = enabled;
|
||||
}
|
||||
|
||||
/** Gets whether the scale gesture is enabled or not. */
|
||||
public boolean isScaleEnabled() {
|
||||
return mIsScaleEnabled;
|
||||
}
|
||||
|
||||
/** Sets whether the translation gesture is enabled or not. */
|
||||
public void setTranslationEnabled(boolean enabled) {
|
||||
mIsTranslationEnabled = enabled;
|
||||
}
|
||||
|
||||
/** Gets whether the translations gesture is enabled or not. */
|
||||
public boolean isTranslationEnabled() {
|
||||
return mIsTranslationEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum scale factor allowed.
|
||||
*
|
||||
* <p>Hierarchy's scaling (if any) is not taken into account.
|
||||
*/
|
||||
public void setMinScaleFactor(float minScaleFactor) {
|
||||
mMinScaleFactor = minScaleFactor;
|
||||
}
|
||||
|
||||
/** Gets the minimum scale factor allowed. */
|
||||
public float getMinScaleFactor() {
|
||||
return mMinScaleFactor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum scale factor allowed.
|
||||
*
|
||||
* <p>Hierarchy's scaling (if any) is not taken into account.
|
||||
*/
|
||||
public void setMaxScaleFactor(float maxScaleFactor) {
|
||||
mMaxScaleFactor = maxScaleFactor;
|
||||
}
|
||||
|
||||
/** Gets the maximum scale factor allowed. */
|
||||
public float getMaxScaleFactor() {
|
||||
return mMaxScaleFactor;
|
||||
}
|
||||
|
||||
/** Sets whether gesture zooms are enabled or not. */
|
||||
public void setGestureZoomEnabled(boolean isGestureZoomEnabled) {
|
||||
mIsGestureZoomEnabled = isGestureZoomEnabled;
|
||||
}
|
||||
|
||||
/** Gets whether gesture zooms are enabled or not. */
|
||||
public boolean isGestureZoomEnabled() {
|
||||
return mIsGestureZoomEnabled;
|
||||
}
|
||||
|
||||
/** Gets the current scale factor. */
|
||||
@Override
|
||||
public float getScaleFactor() {
|
||||
return getMatrixScaleFactor(mActiveTransform);
|
||||
}
|
||||
|
||||
/** Sets the image bounds, in view-absolute coordinates. */
|
||||
@Override
|
||||
public void setImageBounds(RectF imageBounds) {
|
||||
if (!imageBounds.equals(mImageBounds)) {
|
||||
mImageBounds.set(imageBounds);
|
||||
onTransformChanged();
|
||||
if (mImageBoundsListener != null) {
|
||||
mImageBoundsListener.onImageBoundsSet(mImageBounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets the non-transformed image bounds, in view-absolute coordinates. */
|
||||
public RectF getImageBounds() {
|
||||
return mImageBounds;
|
||||
}
|
||||
|
||||
/** Gets the transformed image bounds, in view-absolute coordinates */
|
||||
private RectF getTransformedImageBounds() {
|
||||
return mTransformedImageBounds;
|
||||
}
|
||||
|
||||
/** Sets the view bounds. */
|
||||
@Override
|
||||
public void setViewBounds(RectF viewBounds) {
|
||||
mViewBounds.set(viewBounds);
|
||||
}
|
||||
|
||||
/** Gets the view bounds. */
|
||||
public RectF getViewBounds() {
|
||||
return mViewBounds;
|
||||
}
|
||||
|
||||
/** Sets the image bounds listener. */
|
||||
public void setImageBoundsListener(@Nullable ImageBoundsListener imageBoundsListener) {
|
||||
mImageBoundsListener = imageBoundsListener;
|
||||
}
|
||||
|
||||
/** Gets the image bounds listener. */
|
||||
public @Nullable ImageBoundsListener getImageBoundsListener() {
|
||||
return mImageBoundsListener;
|
||||
}
|
||||
|
||||
/** Returns true if the zoomable transform is identity matrix. */
|
||||
@Override
|
||||
public boolean isIdentity() {
|
||||
return isMatrixIdentity(mActiveTransform, 1e-3f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the transform was corrected during the last update.
|
||||
*
|
||||
* <p>We should rename this method to `wasTransformedWithoutCorrection` and just return the
|
||||
* internal flag directly. However, this requires interface change and negation of meaning.
|
||||
*/
|
||||
@Override
|
||||
public boolean wasTransformCorrected() {
|
||||
return mWasTransformCorrected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the matrix that transforms image-absolute coordinates to view-absolute coordinates. The
|
||||
* zoomable transformation is taken into account.
|
||||
*
|
||||
* <p>Internal matrix is exposed for performance reasons and is not to be modified by the callers.
|
||||
*/
|
||||
@Override
|
||||
public Matrix getTransform() {
|
||||
return mActiveTransform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the matrix that transforms image-relative coordinates to view-absolute coordinates. The
|
||||
* zoomable transformation is taken into account.
|
||||
*/
|
||||
public void getImageRelativeToViewAbsoluteTransform(Matrix outMatrix) {
|
||||
outMatrix.setRectToRect(IDENTITY_RECT, mTransformedImageBounds, Matrix.ScaleToFit.FILL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps point from view-absolute to image-relative coordinates. This takes into account the
|
||||
* zoomable transformation.
|
||||
*/
|
||||
public PointF mapViewToImage(PointF viewPoint) {
|
||||
float[] points = mTempValues;
|
||||
points[0] = viewPoint.x;
|
||||
points[1] = viewPoint.y;
|
||||
mActiveTransform.invert(mActiveTransformInverse);
|
||||
mActiveTransformInverse.mapPoints(points, 0, points, 0, 1);
|
||||
mapAbsoluteToRelative(points, points, 1);
|
||||
return new PointF(points[0], points[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps point from image-relative to view-absolute coordinates. This takes into account the
|
||||
* zoomable transformation.
|
||||
*/
|
||||
public PointF mapImageToView(PointF imagePoint) {
|
||||
float[] points = mTempValues;
|
||||
points[0] = imagePoint.x;
|
||||
points[1] = imagePoint.y;
|
||||
mapRelativeToAbsolute(points, points, 1);
|
||||
mActiveTransform.mapPoints(points, 0, points, 0, 1);
|
||||
return new PointF(points[0], points[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps array of 2D points from view-absolute to image-relative coordinates. This does NOT take
|
||||
* into account the zoomable transformation. Points are represented by a float array of [x0, y0,
|
||||
* x1, y1, ...].
|
||||
*
|
||||
* @param destPoints destination array (may be the same as source array)
|
||||
* @param srcPoints source array
|
||||
* @param numPoints number of points to map
|
||||
*/
|
||||
private void mapAbsoluteToRelative(float[] destPoints, float[] srcPoints, int numPoints) {
|
||||
for (int i = 0; i < numPoints; i++) {
|
||||
destPoints[i * 2 + 0] = (srcPoints[i * 2 + 0] - mImageBounds.left) / mImageBounds.width();
|
||||
destPoints[i * 2 + 1] = (srcPoints[i * 2 + 1] - mImageBounds.top) / mImageBounds.height();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps array of 2D points from image-relative to view-absolute coordinates. This does NOT take
|
||||
* into account the zoomable transformation. Points are represented by float array of [x0, y0, x1,
|
||||
* y1, ...].
|
||||
*
|
||||
* @param destPoints destination array (may be the same as source array)
|
||||
* @param srcPoints source array
|
||||
* @param numPoints number of points to map
|
||||
*/
|
||||
private void mapRelativeToAbsolute(float[] destPoints, float[] srcPoints, int numPoints) {
|
||||
for (int i = 0; i < numPoints; i++) {
|
||||
destPoints[i * 2 + 0] = srcPoints[i * 2 + 0] * mImageBounds.width() + mImageBounds.left;
|
||||
destPoints[i * 2 + 1] = srcPoints[i * 2 + 1] * mImageBounds.height() + mImageBounds.top;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms to the desired scale and positions the image so that the given image point corresponds to
|
||||
* the given view point.
|
||||
*
|
||||
* @param scale desired scale, will be limited to {min, max} scale factor
|
||||
* @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1)
|
||||
* @param viewPoint 2D point in view's absolute coordinate system
|
||||
*/
|
||||
public void zoomToPoint(float scale, PointF imagePoint, PointF viewPoint) {
|
||||
FLog.v(TAG, "zoomToPoint");
|
||||
calculateZoomToPointTransform(mActiveTransform, scale, imagePoint, viewPoint, LIMIT_ALL);
|
||||
onTransformChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the zoom transformation that would zoom to the desired scale and position the image
|
||||
* so that the given image point corresponds to the given view point.
|
||||
*
|
||||
* @param outTransform the matrix to store the result to
|
||||
* @param scale desired scale, will be limited to {min, max} scale factor
|
||||
* @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1)
|
||||
* @param viewPoint 2D point in view's absolute coordinate system
|
||||
* @param limitFlags whether to limit translation and/or scale.
|
||||
* @return whether or not the transform has been corrected due to limitation
|
||||
*/
|
||||
protected boolean calculateZoomToPointTransform(
|
||||
Matrix outTransform,
|
||||
float scale,
|
||||
PointF imagePoint,
|
||||
PointF viewPoint,
|
||||
@LimitFlag int limitFlags) {
|
||||
float[] viewAbsolute = mTempValues;
|
||||
viewAbsolute[0] = imagePoint.x;
|
||||
viewAbsolute[1] = imagePoint.y;
|
||||
mapRelativeToAbsolute(viewAbsolute, viewAbsolute, 1);
|
||||
float distanceX = viewPoint.x - viewAbsolute[0];
|
||||
float distanceY = viewPoint.y - viewAbsolute[1];
|
||||
boolean transformCorrected = false;
|
||||
outTransform.setScale(scale, scale, viewAbsolute[0], viewAbsolute[1]);
|
||||
transformCorrected |= limitScale(outTransform, viewAbsolute[0], viewAbsolute[1], limitFlags);
|
||||
outTransform.postTranslate(distanceX, distanceY);
|
||||
transformCorrected |= limitTranslation(outTransform, limitFlags);
|
||||
return transformCorrected;
|
||||
}
|
||||
|
||||
/** Sets a new zoom transformation. */
|
||||
public void setTransform(Matrix newTransform) {
|
||||
FLog.v(TAG, "setTransform");
|
||||
mActiveTransform.set(newTransform);
|
||||
onTransformChanged();
|
||||
}
|
||||
|
||||
/** Gets the gesture detector. */
|
||||
protected TransformGestureDetector getDetector() {
|
||||
return mGestureDetector;
|
||||
}
|
||||
|
||||
/** Notifies controller of the received touch event. */
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
FLog.v(TAG, "onTouchEvent: action: ", event.getAction());
|
||||
if (mIsEnabled && mIsGestureZoomEnabled) {
|
||||
return mGestureDetector.onTouchEvent(event);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* TransformGestureDetector.Listener methods */
|
||||
|
||||
@Override
|
||||
public void onGestureBegin(TransformGestureDetector detector) {
|
||||
FLog.v(TAG, "onGestureBegin");
|
||||
mPreviousTransform.set(mActiveTransform);
|
||||
onTransformBegin();
|
||||
// We only received a touch down event so far, and so we don't know yet in which direction a
|
||||
// future move event will follow. Therefore, if we can't scroll in all directions, we have to
|
||||
// assume the worst case where the user tries to scroll out of edge, which would cause
|
||||
// transformation to be corrected.
|
||||
mWasTransformCorrected = !canScrollInAllDirection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGestureUpdate(TransformGestureDetector detector) {
|
||||
FLog.v(TAG, "onGestureUpdate");
|
||||
boolean transformCorrected = calculateGestureTransform(mActiveTransform, LIMIT_ALL);
|
||||
onTransformChanged();
|
||||
if (transformCorrected) {
|
||||
mGestureDetector.restartGesture();
|
||||
}
|
||||
// A transformation happened, but was it without correction?
|
||||
mWasTransformCorrected = transformCorrected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGestureEnd(TransformGestureDetector detector) {
|
||||
FLog.v(TAG, "onGestureEnd");
|
||||
onTransformEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the zoom transformation based on the current gesture.
|
||||
*
|
||||
* @param outTransform the matrix to store the result to
|
||||
* @param limitTypes whether to limit translation and/or scale.
|
||||
* @return whether or not the transform has been corrected due to limitation
|
||||
*/
|
||||
protected boolean calculateGestureTransform(Matrix outTransform, @LimitFlag int limitTypes) {
|
||||
TransformGestureDetector detector = mGestureDetector;
|
||||
boolean transformCorrected = false;
|
||||
outTransform.set(mPreviousTransform);
|
||||
if (mIsRotationEnabled) {
|
||||
float angle = detector.getRotation() * (float) (180 / Math.PI);
|
||||
outTransform.postRotate(angle, detector.getPivotX(), detector.getPivotY());
|
||||
}
|
||||
if (mIsScaleEnabled) {
|
||||
float scale = detector.getScale();
|
||||
outTransform.postScale(scale, scale, detector.getPivotX(), detector.getPivotY());
|
||||
}
|
||||
transformCorrected |=
|
||||
limitScale(outTransform, detector.getPivotX(), detector.getPivotY(), limitTypes);
|
||||
if (mIsTranslationEnabled) {
|
||||
outTransform.postTranslate(detector.getTranslationX(), detector.getTranslationY());
|
||||
}
|
||||
transformCorrected |= limitTranslation(outTransform, limitTypes);
|
||||
return transformCorrected;
|
||||
}
|
||||
|
||||
private void onTransformBegin() {
|
||||
if (mListener != null && isEnabled()) {
|
||||
mListener.onTransformBegin(mActiveTransform);
|
||||
}
|
||||
}
|
||||
|
||||
private void onTransformChanged() {
|
||||
mActiveTransform.mapRect(mTransformedImageBounds, mImageBounds);
|
||||
if (mListener != null && isEnabled()) {
|
||||
mListener.onTransformChanged(mActiveTransform);
|
||||
}
|
||||
}
|
||||
|
||||
private void onTransformEnd() {
|
||||
if (mListener != null && isEnabled()) {
|
||||
mListener.onTransformEnd(mActiveTransform);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the scaling factor within the specified limits.
|
||||
*
|
||||
* @param pivotX x coordinate of the pivot point
|
||||
* @param pivotY y coordinate of the pivot point
|
||||
* @param limitTypes whether to limit scale.
|
||||
* @return whether limiting has been applied or not
|
||||
*/
|
||||
private boolean limitScale(
|
||||
Matrix transform, float pivotX, float pivotY, @LimitFlag int limitTypes) {
|
||||
if (!shouldLimit(limitTypes, LIMIT_SCALE)) {
|
||||
return false;
|
||||
}
|
||||
float currentScale = getMatrixScaleFactor(transform);
|
||||
float targetScale = limit(currentScale, mMinScaleFactor, mMaxScaleFactor);
|
||||
if (targetScale != currentScale) {
|
||||
float scale = targetScale / currentScale;
|
||||
transform.postScale(scale, scale, pivotX, pivotY);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limits the translation so that there are no empty spaces on the sides if possible.
|
||||
*
|
||||
* <p>The image is attempted to be centered within the view bounds if the transformed image is
|
||||
* smaller. There will be no empty spaces within the view bounds if the transformed image is
|
||||
* bigger. This applies to each dimension (horizontal and vertical) independently.
|
||||
*
|
||||
* @param limitTypes whether to limit translation along the specific axis.
|
||||
* @return whether limiting has been applied or not
|
||||
*/
|
||||
private boolean limitTranslation(Matrix transform, @LimitFlag int limitTypes) {
|
||||
if (!shouldLimit(limitTypes, LIMIT_TRANSLATION_X | LIMIT_TRANSLATION_Y)) {
|
||||
return false;
|
||||
}
|
||||
RectF b = mTempRect;
|
||||
b.set(mImageBounds);
|
||||
transform.mapRect(b);
|
||||
float offsetLeft =
|
||||
shouldLimit(limitTypes, LIMIT_TRANSLATION_X)
|
||||
? getOffset(
|
||||
b.left, b.right, mViewBounds.left, mViewBounds.right, mImageBounds.centerX())
|
||||
: 0;
|
||||
float offsetTop =
|
||||
shouldLimit(limitTypes, LIMIT_TRANSLATION_Y)
|
||||
? getOffset(
|
||||
b.top, b.bottom, mViewBounds.top, mViewBounds.bottom, mImageBounds.centerY())
|
||||
: 0;
|
||||
if (offsetLeft != 0 || offsetTop != 0) {
|
||||
transform.postTranslate(offsetLeft, offsetTop);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the specified limit flag is present in the limits provided.
|
||||
*
|
||||
* <p>If the flag contains multiple flags together using a bitwise OR, this only checks that at
|
||||
* least one of the flags is included.
|
||||
*
|
||||
* @param limits the limits to apply
|
||||
* @param flag the limit flag(s) to check for
|
||||
* @return true if the flag (or one of the flags) is included in the limits
|
||||
*/
|
||||
private static boolean shouldLimit(@LimitFlag int limits, @LimitFlag int flag) {
|
||||
return (limits & flag) != LIMIT_NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset necessary to make sure that: - the image is centered within the limit if the
|
||||
* image is smaller than the limit - there is no empty space on left/right if the image is bigger
|
||||
* than the limit
|
||||
*/
|
||||
private float getOffset(
|
||||
float imageStart, float imageEnd, float limitStart, float limitEnd, float limitCenter) {
|
||||
float imageWidth = imageEnd - imageStart, limitWidth = limitEnd - limitStart;
|
||||
float limitInnerWidth = Math.min(limitCenter - limitStart, limitEnd - limitCenter) * 2;
|
||||
// center if smaller than limitInnerWidth
|
||||
if (imageWidth < limitInnerWidth) {
|
||||
return limitCenter - (imageEnd + imageStart) / 2;
|
||||
}
|
||||
// to the edge if in between and limitCenter is not (limitLeft + limitRight) / 2
|
||||
if (imageWidth < limitWidth) {
|
||||
if (limitCenter < (limitStart + limitEnd) / 2) {
|
||||
return limitStart - imageStart;
|
||||
} else {
|
||||
return limitEnd - imageEnd;
|
||||
}
|
||||
}
|
||||
// to the edge if larger than limitWidth and empty space visible
|
||||
if (imageStart > limitStart) {
|
||||
return limitStart - imageStart;
|
||||
}
|
||||
if (imageEnd < limitEnd) {
|
||||
return limitEnd - imageEnd;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Limits the value to the given min and max range. */
|
||||
private float limit(float value, float min, float max) {
|
||||
return Math.min(Math.max(min, value), max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the scale factor for the given matrix. This method assumes the equal scaling factor for X
|
||||
* and Y axis.
|
||||
*/
|
||||
private float getMatrixScaleFactor(Matrix transform) {
|
||||
transform.getValues(mTempValues);
|
||||
return mTempValues[Matrix.MSCALE_X];
|
||||
}
|
||||
|
||||
/** Same as {@code Matrix.isIdentity()}, but with tolerance {@code eps}. */
|
||||
private boolean isMatrixIdentity(Matrix transform, float eps) {
|
||||
// Checks whether the given matrix is close enough to the identity matrix:
|
||||
// 1 0 0
|
||||
// 0 1 0
|
||||
// 0 0 1
|
||||
// Or equivalently to the zero matrix, after subtracting 1.0f from the diagonal elements:
|
||||
// 0 0 0
|
||||
// 0 0 0
|
||||
// 0 0 0
|
||||
transform.getValues(mTempValues);
|
||||
mTempValues[0] -= 1.0f; // m00
|
||||
mTempValues[4] -= 1.0f; // m11
|
||||
mTempValues[8] -= 1.0f; // m22
|
||||
for (int i = 0; i < 9; i++) {
|
||||
if (Math.abs(mTempValues[i]) > eps) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Returns whether the scroll can happen in all directions. I.e. the image is not on any edge. */
|
||||
private boolean canScrollInAllDirection() {
|
||||
return mTransformedImageBounds.left < mViewBounds.left - EPS
|
||||
&& mTransformedImageBounds.top < mViewBounds.top - EPS
|
||||
&& mTransformedImageBounds.right > mViewBounds.right + EPS
|
||||
&& mTransformedImageBounds.bottom > mViewBounds.bottom + EPS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int computeHorizontalScrollRange() {
|
||||
return (int) mTransformedImageBounds.width();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int computeHorizontalScrollOffset() {
|
||||
return (int) (mViewBounds.left - mTransformedImageBounds.left);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int computeHorizontalScrollExtent() {
|
||||
return (int) mViewBounds.width();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int computeVerticalScrollRange() {
|
||||
return (int) mTransformedImageBounds.height();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int computeVerticalScrollOffset() {
|
||||
return (int) (mViewBounds.top - mTransformedImageBounds.top);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int computeVerticalScrollExtent() {
|
||||
return (int) mViewBounds.height();
|
||||
}
|
||||
|
||||
public Listener getListener() {
|
||||
return mListener;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,607 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.zoomable
|
||||
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.PointF
|
||||
import android.graphics.RectF
|
||||
import android.view.MotionEvent
|
||||
import androidx.annotation.IntDef
|
||||
import com.facebook.common.logging.FLog
|
||||
import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector
|
||||
import kotlin.math.abs
|
||||
|
||||
/** Zoomable controller that calculates transformation based on touch events. */
|
||||
open class DefaultZoomableController(
|
||||
private val mGestureDetector: TransformGestureDetector
|
||||
) : ZoomableController, TransformGestureDetector.Listener {
|
||||
|
||||
/** Interface for handling call backs when the image bounds are set. */
|
||||
fun interface ImageBoundsListener {
|
||||
fun onImageBoundsSet(imageBounds: RectF)
|
||||
}
|
||||
|
||||
@IntDef(
|
||||
flag = true,
|
||||
value = [LIMIT_NONE, LIMIT_TRANSLATION_X, LIMIT_TRANSLATION_Y, LIMIT_SCALE, LIMIT_ALL]
|
||||
)
|
||||
@Retention
|
||||
annotation class LimitFlag
|
||||
|
||||
companion object {
|
||||
const val LIMIT_NONE = 0
|
||||
const val LIMIT_TRANSLATION_X = 1
|
||||
const val LIMIT_TRANSLATION_Y = 2
|
||||
const val LIMIT_SCALE = 4
|
||||
const val LIMIT_ALL = LIMIT_TRANSLATION_X or LIMIT_TRANSLATION_Y or LIMIT_SCALE
|
||||
|
||||
private const val EPS = 1e-3f
|
||||
|
||||
private val TAG: Class<*> = DefaultZoomableController::class.java
|
||||
|
||||
private val IDENTITY_RECT = RectF(0f, 0f, 1f, 1f)
|
||||
|
||||
fun newInstance(): DefaultZoomableController {
|
||||
return DefaultZoomableController(TransformGestureDetector.newInstance())
|
||||
}
|
||||
}
|
||||
|
||||
private var mImageBoundsListener: ImageBoundsListener? = null
|
||||
|
||||
private var mListener: ZoomableController.Listener? = null
|
||||
|
||||
private var mIsEnabled = false
|
||||
private var mIsRotationEnabled = false
|
||||
private var mIsScaleEnabled = true
|
||||
private var mIsTranslationEnabled = true
|
||||
private var mIsGestureZoomEnabled = true
|
||||
|
||||
private var mMinScaleFactor = 1.0f
|
||||
private var mMaxScaleFactor = 2.0f
|
||||
|
||||
// View bounds, in view-absolute coordinates
|
||||
private val mViewBounds = RectF()
|
||||
// Non-transformed image bounds, in view-absolute coordinates
|
||||
private val mImageBounds = RectF()
|
||||
// Transformed image bounds, in view-absolute coordinates
|
||||
private val mTransformedImageBounds = RectF()
|
||||
|
||||
private val mPreviousTransform = Matrix()
|
||||
private val mActiveTransform = Matrix()
|
||||
private val mActiveTransformInverse = Matrix()
|
||||
private val mTempValues = FloatArray(9)
|
||||
private val mTempRect = RectF()
|
||||
private var mWasTransformCorrected = false
|
||||
|
||||
init {
|
||||
mGestureDetector.setListener(this)
|
||||
}
|
||||
|
||||
/** Rests the controller. */
|
||||
open fun reset() {
|
||||
FLog.v(TAG, "reset")
|
||||
mGestureDetector.reset()
|
||||
mPreviousTransform.reset()
|
||||
mActiveTransform.reset()
|
||||
onTransformChanged()
|
||||
}
|
||||
|
||||
/** Sets the zoomable listener. */
|
||||
override fun setListener(listener: ZoomableController.Listener?) {
|
||||
mListener = listener
|
||||
}
|
||||
|
||||
/** Sets whether the controller is enabled or not. */
|
||||
override fun setEnabled(enabled: Boolean) {
|
||||
mIsEnabled = enabled
|
||||
if (!enabled) {
|
||||
reset()
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets whether the controller is enabled or not. */
|
||||
override fun isEnabled(): Boolean {
|
||||
return mIsEnabled
|
||||
}
|
||||
|
||||
/** Sets whether the rotation gesture is enabled or not. */
|
||||
fun setRotationEnabled(enabled: Boolean) {
|
||||
mIsRotationEnabled = enabled
|
||||
}
|
||||
|
||||
/** Gets whether the rotation gesture is enabled or not. */
|
||||
fun isRotationEnabled(): Boolean {
|
||||
return mIsRotationEnabled
|
||||
}
|
||||
|
||||
/** Sets whether the scale gesture is enabled or not. */
|
||||
fun setScaleEnabled(enabled: Boolean) {
|
||||
mIsScaleEnabled = enabled
|
||||
}
|
||||
|
||||
/** Gets whether the scale gesture is enabled or not. */
|
||||
fun isScaleEnabled(): Boolean {
|
||||
return mIsScaleEnabled
|
||||
}
|
||||
|
||||
/** Sets whether the translation gesture is enabled or not. */
|
||||
fun setTranslationEnabled(enabled: Boolean) {
|
||||
mIsTranslationEnabled = enabled
|
||||
}
|
||||
|
||||
/** Gets whether the translations gesture is enabled or not. */
|
||||
fun isTranslationEnabled(): Boolean {
|
||||
return mIsTranslationEnabled
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum scale factor allowed.
|
||||
*
|
||||
* <p>Hierarchy's scaling (if any) is not taken into account.
|
||||
*/
|
||||
fun setMinScaleFactor(minScaleFactor: Float) {
|
||||
mMinScaleFactor = minScaleFactor
|
||||
}
|
||||
|
||||
/** Gets the minimum scale factor allowed. */
|
||||
fun getMinScaleFactor(): Float {
|
||||
return mMinScaleFactor
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum scale factor allowed.
|
||||
*
|
||||
* <p>Hierarchy's scaling (if any) is not taken into account.
|
||||
*/
|
||||
fun setMaxScaleFactor(maxScaleFactor: Float) {
|
||||
mMaxScaleFactor = maxScaleFactor
|
||||
}
|
||||
|
||||
/** Gets the maximum scale factor allowed. */
|
||||
fun getMaxScaleFactor(): Float {
|
||||
return mMaxScaleFactor
|
||||
}
|
||||
|
||||
/** Sets whether gesture zooms are enabled or not. */
|
||||
fun setGestureZoomEnabled(isGestureZoomEnabled: Boolean) {
|
||||
mIsGestureZoomEnabled = isGestureZoomEnabled
|
||||
}
|
||||
|
||||
/** Gets whether gesture zooms are enabled or not. */
|
||||
fun isGestureZoomEnabled(): Boolean {
|
||||
return mIsGestureZoomEnabled
|
||||
}
|
||||
|
||||
/** Gets the current scale factor. */
|
||||
override fun getScaleFactor(): Float {
|
||||
return getMatrixScaleFactor(mActiveTransform)
|
||||
}
|
||||
|
||||
/** Sets the image bounds, in view-absolute coordinates. */
|
||||
override fun setImageBounds(imageBounds: RectF) {
|
||||
if (imageBounds != mImageBounds) {
|
||||
mImageBounds.set(imageBounds)
|
||||
onTransformChanged()
|
||||
mImageBoundsListener?.onImageBoundsSet(mImageBounds)
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets the non-transformed image bounds, in view-absolute coordinates. */
|
||||
fun getImageBounds(): RectF {
|
||||
return mImageBounds
|
||||
}
|
||||
|
||||
/** Gets the transformed image bounds, in view-absolute coordinates */
|
||||
private fun getTransformedImageBounds(): RectF {
|
||||
return mTransformedImageBounds
|
||||
}
|
||||
|
||||
/** Sets the view bounds. */
|
||||
override fun setViewBounds(viewBounds: RectF) {
|
||||
mViewBounds.set(viewBounds)
|
||||
}
|
||||
|
||||
/** Gets the view bounds. */
|
||||
fun getViewBounds(): RectF {
|
||||
return mViewBounds
|
||||
}
|
||||
|
||||
/** Sets the image bounds listener. */
|
||||
fun setImageBoundsListener(imageBoundsListener: ImageBoundsListener?) {
|
||||
mImageBoundsListener = imageBoundsListener
|
||||
}
|
||||
|
||||
/** Gets the image bounds listener. */
|
||||
fun getImageBoundsListener(): ImageBoundsListener? {
|
||||
return mImageBoundsListener
|
||||
}
|
||||
|
||||
/** Returns true if the zoomable transform is identity matrix. */
|
||||
override fun isIdentity(): Boolean {
|
||||
return isMatrixIdentity(mActiveTransform, 1e-3f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the transform was corrected during the last update.
|
||||
*
|
||||
* <p>We should rename this method to `wasTransformedWithoutCorrection` and just return the
|
||||
* internal flag directly. However, this requires interface change and negation of meaning.
|
||||
*/
|
||||
override fun wasTransformCorrected(): Boolean {
|
||||
return mWasTransformCorrected
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the matrix that transforms image-absolute coordinates to view-absolute coordinates. The
|
||||
* zoomable transformation is taken into account.
|
||||
*
|
||||
* <p>Internal matrix is exposed for performance reasons and is not to be modified by the
|
||||
* callers.
|
||||
*/
|
||||
override fun getTransform(): Matrix {
|
||||
return mActiveTransform
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the matrix that transforms image-relative coordinates to view-absolute coordinates. The
|
||||
* zoomable transformation is taken into account.
|
||||
*/
|
||||
fun getImageRelativeToViewAbsoluteTransform(outMatrix: Matrix) {
|
||||
outMatrix.setRectToRect(IDENTITY_RECT, mTransformedImageBounds, Matrix.ScaleToFit.FILL)
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps point from view-absolute to image-relative coordinates. This takes into account the
|
||||
* zoomable transformation.
|
||||
*/
|
||||
fun mapViewToImage(viewPoint: PointF): PointF {
|
||||
val points = mTempValues
|
||||
points[0] = viewPoint.x
|
||||
points[1] = viewPoint.y
|
||||
mActiveTransform.invert(mActiveTransformInverse)
|
||||
mActiveTransformInverse.mapPoints(points, 0, points, 0, 1)
|
||||
mapAbsoluteToRelative(points, points, 1)
|
||||
return PointF(points[0], points[1])
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps point from image-relative to view-absolute coordinates. This takes into account the
|
||||
* zoomable transformation.
|
||||
*/
|
||||
fun mapImageToView(imagePoint: PointF): PointF {
|
||||
val points = mTempValues
|
||||
points[0] = imagePoint.x
|
||||
points[1] = imagePoint.y
|
||||
mapRelativeToAbsolute(points, points, 1)
|
||||
mActiveTransform.mapPoints(points, 0, points, 0, 1)
|
||||
return PointF(points[0], points[1])
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps array of 2D points from view-absolute to image-relative coordinates. This does NOT take
|
||||
* into account the zoomable transformation. Points are represented by a float array of [x0, y0,
|
||||
* x1, y1, ...].
|
||||
*
|
||||
* @param destPoints destination array (may be the same as source array)
|
||||
* @param srcPoints source array
|
||||
* @param numPoints number of points to map
|
||||
*/
|
||||
private fun mapAbsoluteToRelative(
|
||||
destPoints: FloatArray,
|
||||
srcPoints: FloatArray,
|
||||
numPoints: Int
|
||||
) {
|
||||
for (i in 0 until numPoints) {
|
||||
destPoints[i * 2] = (srcPoints[i * 2] - mImageBounds.left) / mImageBounds.width()
|
||||
destPoints[i * 2 + 1] =
|
||||
(srcPoints[i * 2 + 1] - mImageBounds.top) / mImageBounds.height()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps array of 2D points from image-relative to view-absolute coordinates. This does NOT take
|
||||
* into account the zoomable transformation. Points are represented by float array of
|
||||
* [x0, y0, x1, y1, ...].
|
||||
*
|
||||
* @param destPoints destination array (may be the same as source array)
|
||||
* @param srcPoints source array
|
||||
* @param numPoints number of points to map
|
||||
*/
|
||||
private fun mapRelativeToAbsolute(
|
||||
destPoints: FloatArray,
|
||||
srcPoints: FloatArray,
|
||||
numPoints: Int
|
||||
) {
|
||||
for (i in 0 until numPoints) {
|
||||
destPoints[i * 2] = srcPoints[i * 2] * mImageBounds.width() + mImageBounds.left
|
||||
destPoints[i * 2 + 1] = srcPoints[i * 2 + 1] * mImageBounds.height() + mImageBounds.top
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms to the desired scale and positions the image so that the given image point
|
||||
* corresponds to the given view point.
|
||||
*
|
||||
* @param scale desired scale, will be limited to {min, max} scale factor
|
||||
* @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1)
|
||||
* @param viewPoint 2D point in view's absolute coordinate system
|
||||
*/
|
||||
open fun zoomToPoint(scale: Float, imagePoint: PointF, viewPoint: PointF) {
|
||||
FLog.v(TAG, "zoomToPoint")
|
||||
calculateZoomToPointTransform(mActiveTransform, scale, imagePoint, viewPoint, LIMIT_ALL)
|
||||
onTransformChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the zoom transformation that would zoom to the desired scale and position
|
||||
* the image so that the given image point corresponds to the given view point.
|
||||
*
|
||||
* @param outTransform the matrix to store the result to
|
||||
* @param scale desired scale, will be limited to {min, max} scale factor
|
||||
* @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1)
|
||||
* @param viewPoint 2D point in view's absolute coordinate system
|
||||
* @param limitFlags whether to limit translation and/or scale.
|
||||
* @return whether or not the transform has been corrected due to limitation
|
||||
*/
|
||||
protected fun calculateZoomToPointTransform(
|
||||
outTransform: Matrix,
|
||||
scale: Float,
|
||||
imagePoint: PointF,
|
||||
viewPoint: PointF,
|
||||
@LimitFlag limitFlags: Int
|
||||
): Boolean {
|
||||
val viewAbsolute = mTempValues
|
||||
viewAbsolute[0] = imagePoint.x
|
||||
viewAbsolute[1] = imagePoint.y
|
||||
mapRelativeToAbsolute(viewAbsolute, viewAbsolute, 1)
|
||||
val distanceX = viewPoint.x - viewAbsolute[0]
|
||||
val distanceY = viewPoint.y - viewAbsolute[1]
|
||||
var transformCorrected = false
|
||||
outTransform.setScale(scale, scale, viewAbsolute[0], viewAbsolute[1])
|
||||
transformCorrected = transformCorrected or
|
||||
limitScale(outTransform, viewAbsolute[0], viewAbsolute[1], limitFlags)
|
||||
outTransform.postTranslate(distanceX, distanceY)
|
||||
transformCorrected = transformCorrected or limitTranslation(outTransform, limitFlags)
|
||||
return transformCorrected
|
||||
}
|
||||
|
||||
/** Sets a new zoom transformation. */
|
||||
fun setTransform(newTransform: Matrix) {
|
||||
FLog.v(TAG, "setTransform")
|
||||
mActiveTransform.set(newTransform)
|
||||
onTransformChanged()
|
||||
}
|
||||
|
||||
/** Gets the gesture detector. */
|
||||
protected fun getDetector(): TransformGestureDetector {
|
||||
return mGestureDetector
|
||||
}
|
||||
|
||||
/** Notifies controller of the received touch event. */
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
FLog.v(TAG, "onTouchEvent: action: ", event.action)
|
||||
return if (mIsEnabled && mIsGestureZoomEnabled) {
|
||||
mGestureDetector.onTouchEvent(event)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/* TransformGestureDetector.Listener methods */
|
||||
|
||||
override fun onGestureBegin(detector: TransformGestureDetector) {
|
||||
FLog.v(TAG, "onGestureBegin")
|
||||
mPreviousTransform.set(mActiveTransform)
|
||||
onTransformBegin()
|
||||
mWasTransformCorrected = !canScrollInAllDirection()
|
||||
}
|
||||
|
||||
override fun onGestureUpdate(detector: TransformGestureDetector) {
|
||||
FLog.v(TAG, "onGestureUpdate")
|
||||
val transformCorrected = calculateGestureTransform(mActiveTransform, LIMIT_ALL)
|
||||
onTransformChanged()
|
||||
if (transformCorrected) {
|
||||
mGestureDetector.restartGesture()
|
||||
}
|
||||
mWasTransformCorrected = transformCorrected
|
||||
}
|
||||
|
||||
override fun onGestureEnd(detector: TransformGestureDetector) {
|
||||
FLog.v(TAG, "onGestureEnd")
|
||||
onTransformEnd()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the zoom transformation based on the current gesture.
|
||||
*
|
||||
* @param outTransform the matrix to store the result to
|
||||
* @param limitTypes whether to limit translation and/or scale.
|
||||
* @return whether or not the transform has been corrected due to limitation
|
||||
*/
|
||||
private fun calculateGestureTransform(
|
||||
outTransform: Matrix,
|
||||
@LimitFlag limitTypes: Int
|
||||
): Boolean {
|
||||
val detector = mGestureDetector
|
||||
var transformCorrected = false
|
||||
outTransform.set(mPreviousTransform)
|
||||
if (mIsRotationEnabled) {
|
||||
val angle = detector.getRotation() * (180 / Math.PI).toFloat()
|
||||
outTransform.postRotate(angle, detector.getPivotX(), detector.getPivotY())
|
||||
}
|
||||
if (mIsScaleEnabled) {
|
||||
val scale = detector.getScale()
|
||||
outTransform.postScale(scale, scale, detector.getPivotX(), detector.getPivotY())
|
||||
}
|
||||
transformCorrected = transformCorrected or limitScale(
|
||||
outTransform,
|
||||
detector.getPivotX(),
|
||||
detector.getPivotY(),
|
||||
limitTypes
|
||||
)
|
||||
if (mIsTranslationEnabled) {
|
||||
outTransform.postTranslate(detector.getTranslationX(), detector.getTranslationY())
|
||||
}
|
||||
transformCorrected = transformCorrected or limitTranslation(outTransform, limitTypes)
|
||||
return transformCorrected
|
||||
}
|
||||
|
||||
private fun onTransformBegin() {
|
||||
if (mListener != null && isEnabled()) {
|
||||
mListener?.onTransformBegin(mActiveTransform)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTransformChanged() {
|
||||
mActiveTransform.mapRect(mTransformedImageBounds, mImageBounds)
|
||||
if (mListener != null && isEnabled()) {
|
||||
mListener?.onTransformChanged(mActiveTransform)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTransformEnd() {
|
||||
if (mListener != null && isEnabled()) {
|
||||
mListener?.onTransformEnd(mActiveTransform)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the scaling factor within the specified limits.
|
||||
*
|
||||
* @param pivotX x coordinate of the pivot point
|
||||
* @param pivotY y coordinate of the pivot point
|
||||
* @param limitTypes whether to limit scale.
|
||||
* @return whether limiting has been applied or not
|
||||
*/
|
||||
private fun limitScale(
|
||||
transform: Matrix, pivotX: Float, pivotY: Float, @LimitFlag limitTypes: Int
|
||||
): Boolean {
|
||||
if (!shouldLimit(limitTypes, LIMIT_SCALE)) {
|
||||
return false
|
||||
}
|
||||
val currentScale = getMatrixScaleFactor(transform)
|
||||
val targetScale = limit(currentScale, mMinScaleFactor, mMaxScaleFactor)
|
||||
return if (targetScale != currentScale) {
|
||||
val scale = targetScale / currentScale
|
||||
transform.postScale(scale, scale, pivotX, pivotY)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limits the translation so that there are no empty spaces on the sides if possible.
|
||||
*/
|
||||
private fun limitTranslation(transform: Matrix, @LimitFlag limitTypes: Int): Boolean {
|
||||
if (!shouldLimit(limitTypes, LIMIT_TRANSLATION_X or LIMIT_TRANSLATION_Y)) {
|
||||
return false
|
||||
}
|
||||
val b = mTempRect
|
||||
b.set(mImageBounds)
|
||||
transform.mapRect(b)
|
||||
val offsetLeft =
|
||||
if (shouldLimit(limitTypes, LIMIT_TRANSLATION_X)) getOffset(
|
||||
b.left, b.right, mViewBounds.left, mViewBounds.right, mImageBounds.centerX()
|
||||
) else 0f
|
||||
val offsetTop =
|
||||
if (shouldLimit(limitTypes, LIMIT_TRANSLATION_Y)) getOffset(
|
||||
b.top, b.bottom, mViewBounds.top, mViewBounds.bottom, mImageBounds.centerY()
|
||||
) else 0f
|
||||
|
||||
return if (offsetLeft != 0f || offsetTop != 0f) {
|
||||
transform.postTranslate(offsetLeft, offsetTop)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the specified limit flag is present in the limits provided.
|
||||
*/
|
||||
private fun shouldLimit(@LimitFlag limits: Int, @LimitFlag flag: Int): Boolean {
|
||||
return (limits and flag) != LIMIT_NONE
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset necessary to make sure that:
|
||||
* - The image is centered if it's smaller than the limit
|
||||
* - There is no empty space if the image is bigger than the limit
|
||||
*/
|
||||
private fun getOffset(
|
||||
imageStart: Float, imageEnd: Float, limitStart: Float, limitEnd: Float, limitCenter: Float
|
||||
): Float {
|
||||
val imageWidth = imageEnd - imageStart
|
||||
val limitWidth = limitEnd - limitStart
|
||||
val limitInnerWidth = minOf(limitCenter - limitStart, limitEnd - limitCenter) * 2
|
||||
|
||||
return when {
|
||||
imageWidth < limitInnerWidth -> limitCenter - (imageEnd + imageStart) / 2
|
||||
imageWidth < limitWidth -> if (limitCenter < (limitStart + limitEnd) / 2) {
|
||||
limitStart - imageStart
|
||||
} else {
|
||||
limitEnd - imageEnd
|
||||
}
|
||||
imageStart > limitStart -> limitStart - imageStart
|
||||
imageEnd < limitEnd -> limitEnd - imageEnd
|
||||
else -> 0f
|
||||
}
|
||||
}
|
||||
|
||||
/** Limits the value to the given min and max range. */
|
||||
private fun limit(value: Float, min: Float, max: Float): Float {
|
||||
return min.coerceAtLeast(value).coerceAtMost(max)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the scale factor for the given matrix. Assumes equal scaling for X and Y axis.
|
||||
*/
|
||||
private fun getMatrixScaleFactor(transform: Matrix): Float {
|
||||
transform.getValues(mTempValues)
|
||||
return mTempValues[Matrix.MSCALE_X]
|
||||
}
|
||||
|
||||
/** Checks if the matrix is an identity matrix within a given tolerance `eps`. */
|
||||
private fun isMatrixIdentity(transform: Matrix, eps: Float): Boolean {
|
||||
transform.getValues(mTempValues)
|
||||
mTempValues[0] -= 1.0f // m00
|
||||
mTempValues[4] -= 1.0f // m11
|
||||
mTempValues[8] -= 1.0f // m22
|
||||
return mTempValues.all { abs(it) <= eps }
|
||||
}
|
||||
|
||||
/** Returns whether the scroll can happen in all directions. */
|
||||
private fun canScrollInAllDirection(): Boolean {
|
||||
return mTransformedImageBounds.left < mViewBounds.left - EPS &&
|
||||
mTransformedImageBounds.top < mViewBounds.top - EPS &&
|
||||
mTransformedImageBounds.right > mViewBounds.right + EPS &&
|
||||
mTransformedImageBounds.bottom > mViewBounds.bottom + EPS
|
||||
}
|
||||
|
||||
override fun computeHorizontalScrollRange(): Int {
|
||||
return mTransformedImageBounds.width().toInt()
|
||||
}
|
||||
|
||||
override fun computeHorizontalScrollOffset(): Int {
|
||||
return (mViewBounds.left - mTransformedImageBounds.left).toInt()
|
||||
}
|
||||
|
||||
override fun computeHorizontalScrollExtent(): Int {
|
||||
return mViewBounds.width().toInt()
|
||||
}
|
||||
|
||||
override fun computeVerticalScrollRange(): Int {
|
||||
return mTransformedImageBounds.height().toInt()
|
||||
}
|
||||
|
||||
override fun computeVerticalScrollOffset(): Int {
|
||||
return (mViewBounds.top - mTransformedImageBounds.top).toInt()
|
||||
}
|
||||
|
||||
override fun computeVerticalScrollExtent(): Int {
|
||||
return mViewBounds.height().toInt()
|
||||
}
|
||||
|
||||
fun getListener(): ZoomableController.Listener? {
|
||||
return mListener
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.zoomable;
|
||||
|
||||
import android.graphics.PointF;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
/**
|
||||
* Tap gesture listener for double tap to zoom / unzoom and double-tap-and-drag to zoom.
|
||||
*
|
||||
* @see ZoomableDraweeView#setTapListener(GestureDetector.SimpleOnGestureListener)
|
||||
*/
|
||||
public class DoubleTapGestureListener extends GestureDetector.SimpleOnGestureListener {
|
||||
private static final int DURATION_MS = 300;
|
||||
private static final int DOUBLE_TAP_SCROLL_THRESHOLD = 20;
|
||||
|
||||
private final ZoomableDraweeView mDraweeView;
|
||||
private final PointF mDoubleTapViewPoint = new PointF();
|
||||
private final PointF mDoubleTapImagePoint = new PointF();
|
||||
private float mDoubleTapScale = 1;
|
||||
private boolean mDoubleTapScroll = false;
|
||||
|
||||
public DoubleTapGestureListener(ZoomableDraweeView zoomableDraweeView) {
|
||||
mDraweeView = zoomableDraweeView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTapEvent(MotionEvent e) {
|
||||
AbstractAnimatedZoomableController zc =
|
||||
(AbstractAnimatedZoomableController) mDraweeView.getZoomableController();
|
||||
PointF vp = new PointF(e.getX(), e.getY());
|
||||
PointF ip = zc.mapViewToImage(vp);
|
||||
switch (e.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
mDoubleTapViewPoint.set(vp);
|
||||
mDoubleTapImagePoint.set(ip);
|
||||
mDoubleTapScale = zc.getScaleFactor();
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
mDoubleTapScroll = mDoubleTapScroll || shouldStartDoubleTapScroll(vp);
|
||||
if (mDoubleTapScroll) {
|
||||
float scale = calcScale(vp);
|
||||
zc.zoomToPoint(scale, mDoubleTapImagePoint, mDoubleTapViewPoint);
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (mDoubleTapScroll) {
|
||||
float scale = calcScale(vp);
|
||||
zc.zoomToPoint(scale, mDoubleTapImagePoint, mDoubleTapViewPoint);
|
||||
} else {
|
||||
final float maxScale = zc.getMaxScaleFactor();
|
||||
final float minScale = zc.getMinScaleFactor();
|
||||
if (zc.getScaleFactor() < (maxScale + minScale) / 2) {
|
||||
zc.zoomToPoint(
|
||||
maxScale, ip, vp, DefaultZoomableController.LIMIT_ALL, DURATION_MS, null);
|
||||
} else {
|
||||
zc.zoomToPoint(
|
||||
minScale, ip, vp, DefaultZoomableController.LIMIT_ALL, DURATION_MS, null);
|
||||
}
|
||||
}
|
||||
mDoubleTapScroll = false;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean shouldStartDoubleTapScroll(PointF viewPoint) {
|
||||
double dist =
|
||||
Math.hypot(viewPoint.x - mDoubleTapViewPoint.x, viewPoint.y - mDoubleTapViewPoint.y);
|
||||
return dist > DOUBLE_TAP_SCROLL_THRESHOLD;
|
||||
}
|
||||
|
||||
private float calcScale(PointF currentViewPoint) {
|
||||
float dy = (currentViewPoint.y - mDoubleTapViewPoint.y);
|
||||
float t = 1 + Math.abs(dy) * 0.001f;
|
||||
return (dy < 0) ? mDoubleTapScale / t : mDoubleTapScale * t;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.zoomable
|
||||
|
||||
import android.graphics.PointF
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.hypot
|
||||
|
||||
/**
|
||||
* Tap gesture listener for double tap to zoom/unzoom and double-tap-and-drag to zoom.
|
||||
*
|
||||
* @see ZoomableDraweeView.setTapListener
|
||||
*/
|
||||
class DoubleTapGestureListener(private val draweeView: ZoomableDraweeView) :
|
||||
GestureDetector.SimpleOnGestureListener() {
|
||||
|
||||
companion object {
|
||||
private const val DURATION_MS = 300L
|
||||
private const val DOUBLE_TAP_SCROLL_THRESHOLD = 20
|
||||
}
|
||||
|
||||
private val doubleTapViewPoint = PointF()
|
||||
private val doubleTapImagePoint = PointF()
|
||||
private var doubleTapScale = 1f
|
||||
private var doubleTapScroll = false
|
||||
|
||||
override fun onDoubleTapEvent(e: MotionEvent): Boolean {
|
||||
val zc = draweeView.getZoomableController() as AbstractAnimatedZoomableController
|
||||
val vp = PointF(e.x, e.y)
|
||||
val ip = zc.mapViewToImage(vp)
|
||||
|
||||
when (e.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
doubleTapViewPoint.set(vp)
|
||||
doubleTapImagePoint.set(ip)
|
||||
doubleTapScale = zc.getScaleFactor()
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
doubleTapScroll = doubleTapScroll || shouldStartDoubleTapScroll(vp)
|
||||
if (doubleTapScroll) {
|
||||
val scale = calcScale(vp)
|
||||
zc.zoomToPoint(scale, doubleTapImagePoint, doubleTapViewPoint)
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (doubleTapScroll) {
|
||||
val scale = calcScale(vp)
|
||||
zc.zoomToPoint(scale, doubleTapImagePoint, doubleTapViewPoint)
|
||||
} else {
|
||||
val maxScale = zc.getMaxScaleFactor()
|
||||
val minScale = zc.getMinScaleFactor()
|
||||
val targetScale =
|
||||
if (zc.getScaleFactor() < (maxScale + minScale) / 2) maxScale else minScale
|
||||
|
||||
zc.zoomToPoint(
|
||||
targetScale,
|
||||
ip,
|
||||
vp,
|
||||
DefaultZoomableController.LIMIT_ALL,
|
||||
DURATION_MS,
|
||||
null
|
||||
)
|
||||
}
|
||||
doubleTapScroll = false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun shouldStartDoubleTapScroll(viewPoint: PointF): Boolean {
|
||||
val dist = hypot(
|
||||
(viewPoint.x - doubleTapViewPoint.x).toDouble(),
|
||||
(viewPoint.y - doubleTapViewPoint.y).toDouble()
|
||||
)
|
||||
return dist > DOUBLE_TAP_SCROLL_THRESHOLD
|
||||
}
|
||||
|
||||
private fun calcScale(currentViewPoint: PointF): Float {
|
||||
val dy = currentViewPoint.y - doubleTapViewPoint.y
|
||||
val t = 1 + abs(dy) * 0.001f
|
||||
return if (dy < 0) doubleTapScale / t else doubleTapScale * t
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.zoomable;
|
||||
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
/** Wrapper for SimpleOnGestureListener as GestureDetector does not allow changing its listener. */
|
||||
public class GestureListenerWrapper extends GestureDetector.SimpleOnGestureListener {
|
||||
|
||||
private GestureDetector.SimpleOnGestureListener mDelegate;
|
||||
|
||||
public GestureListenerWrapper() {
|
||||
mDelegate = new GestureDetector.SimpleOnGestureListener();
|
||||
}
|
||||
|
||||
public void setListener(GestureDetector.SimpleOnGestureListener listener) {
|
||||
mDelegate = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
mDelegate.onLongPress(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||||
return mDelegate.onScroll(e1, e2, distanceX, distanceY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
||||
return mDelegate.onFling(e1, e2, velocityX, velocityY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowPress(MotionEvent e) {
|
||||
mDelegate.onShowPress(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e) {
|
||||
return mDelegate.onDown(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
return mDelegate.onDoubleTap(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTapEvent(MotionEvent e) {
|
||||
return mDelegate.onDoubleTapEvent(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
return mDelegate.onSingleTapConfirmed(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e) {
|
||||
return mDelegate.onSingleTapUp(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.zoomable
|
||||
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
|
||||
/** Wrapper for SimpleOnGestureListener as GestureDetector does not allow changing its listener. */
|
||||
class GestureListenerWrapper : GestureDetector.SimpleOnGestureListener() {
|
||||
|
||||
private var delegate: GestureDetector.SimpleOnGestureListener =
|
||||
GestureDetector.SimpleOnGestureListener()
|
||||
|
||||
fun setListener(listener: GestureDetector.SimpleOnGestureListener) {
|
||||
delegate = listener
|
||||
}
|
||||
|
||||
override fun onLongPress(e: MotionEvent) {
|
||||
delegate.onLongPress(e)
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
): Boolean {
|
||||
return delegate.onScroll(e1, e2, distanceX, distanceY)
|
||||
}
|
||||
|
||||
override fun onFling(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent,
|
||||
velocityX: Float,
|
||||
velocityY: Float
|
||||
): Boolean {
|
||||
return delegate.onFling(e1, e2, velocityX, velocityY)
|
||||
}
|
||||
|
||||
override fun onShowPress(e: MotionEvent) {
|
||||
delegate.onShowPress(e)
|
||||
}
|
||||
|
||||
override fun onDown(e: MotionEvent): Boolean {
|
||||
return delegate.onDown(e)
|
||||
}
|
||||
|
||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||
return delegate.onDoubleTap(e)
|
||||
}
|
||||
|
||||
override fun onDoubleTapEvent(e: MotionEvent): Boolean {
|
||||
return delegate.onDoubleTapEvent(e)
|
||||
}
|
||||
|
||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||
return delegate.onSingleTapConfirmed(e)
|
||||
}
|
||||
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
return delegate.onSingleTapUp(e)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.zoomable;
|
||||
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Gesture listener that allows multiple child listeners to be added and notified about gesture
|
||||
* events.
|
||||
*
|
||||
* NOTE: The order of the listeners is important. Listeners can consume gesture events. For
|
||||
* example, if one of the child listeners consumes {@link #onLongPress(MotionEvent)} (the listener
|
||||
* returned true), subsequent listeners will not be notified about the event any more since it has
|
||||
* been consumed.
|
||||
*/
|
||||
public class MultiGestureListener extends GestureDetector.SimpleOnGestureListener {
|
||||
|
||||
private final List<GestureDetector.SimpleOnGestureListener> mListeners = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Adds a listener to the multi gesture listener.
|
||||
*
|
||||
* <p>NOTE: The order of the listeners is important since gesture events can be consumed.
|
||||
*
|
||||
* @param listener the listener to be added
|
||||
*/
|
||||
public synchronized void addListener(GestureDetector.SimpleOnGestureListener listener) {
|
||||
mListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given listener so that it will not be notified about future events.
|
||||
*
|
||||
* <p>NOTE: The order of the listeners is important since gesture events can be consumed.
|
||||
*
|
||||
* @param listener the listener to remove
|
||||
*/
|
||||
public synchronized void removeListener(GestureDetector.SimpleOnGestureListener listener) {
|
||||
mListeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean onSingleTapUp(MotionEvent e) {
|
||||
final int size = mListeners.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (mListeners.get(i).onSingleTapUp(e)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onLongPress(MotionEvent e) {
|
||||
final int size = mListeners.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
mListeners.get(i).onLongPress(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean onScroll(
|
||||
MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||||
final int size = mListeners.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (mListeners.get(i).onScroll(e1, e2, distanceX, distanceY)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean onFling(
|
||||
MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
||||
final int size = mListeners.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (mListeners.get(i).onFling(e1, e2, velocityX, velocityY)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onShowPress(MotionEvent e) {
|
||||
final int size = mListeners.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
mListeners.get(i).onShowPress(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean onDown(MotionEvent e) {
|
||||
final int size = mListeners.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (mListeners.get(i).onDown(e)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean onDoubleTap(MotionEvent e) {
|
||||
final int size = mListeners.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (mListeners.get(i).onDoubleTap(e)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean onDoubleTapEvent(MotionEvent e) {
|
||||
final int size = mListeners.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (mListeners.get(i).onDoubleTapEvent(e)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
final int size = mListeners.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (mListeners.get(i).onSingleTapConfirmed(e)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean onContextClick(MotionEvent e) {
|
||||
final int size = mListeners.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (mListeners.get(i).onContextClick(e)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.zoomable
|
||||
|
||||
import android.os.Build
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.util.Collections.synchronizedList
|
||||
|
||||
/**
|
||||
* Gesture listener that allows multiple child listeners to be added and notified about gesture
|
||||
* events.
|
||||
*
|
||||
* NOTE: The order of the listeners is important. Listeners can consume gesture events. For
|
||||
* example, if one of the child listeners consumes [onLongPress] (the listener returned true),
|
||||
* subsequent listeners will not be notified about the event anymore since it has been consumed.
|
||||
*/
|
||||
class MultiGestureListener : GestureDetector.SimpleOnGestureListener() {
|
||||
|
||||
private val listeners: MutableList<GestureDetector.SimpleOnGestureListener> =
|
||||
synchronizedList(mutableListOf())
|
||||
|
||||
/**
|
||||
* Adds a listener to the multi-gesture listener.
|
||||
*
|
||||
* NOTE: The order of the listeners is important since gesture events can be consumed.
|
||||
*
|
||||
* @param listener the listener to be added
|
||||
*/
|
||||
@Synchronized
|
||||
fun addListener(listener: GestureDetector.SimpleOnGestureListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given listener so that it will not be notified about future events.
|
||||
*
|
||||
* NOTE: The order of the listeners is important since gesture events can be consumed.
|
||||
*
|
||||
* @param listener the listener to remove
|
||||
*/
|
||||
@Synchronized
|
||||
fun removeListener(listener: GestureDetector.SimpleOnGestureListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
for (listener in listeners) {
|
||||
if (listener.onSingleTapUp(e)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onLongPress(e: MotionEvent) {
|
||||
for (listener in listeners) {
|
||||
listener.onLongPress(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onScroll(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
): Boolean {
|
||||
for (listener in listeners) {
|
||||
if (listener.onScroll(e1, e2, distanceX, distanceY)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onFling(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent,
|
||||
velocityX: Float,
|
||||
velocityY: Float
|
||||
): Boolean {
|
||||
for (listener in listeners) {
|
||||
if (listener.onFling(e1, e2, velocityX, velocityY)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onShowPress(e: MotionEvent) {
|
||||
for (listener in listeners) {
|
||||
listener.onShowPress(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onDown(e: MotionEvent): Boolean {
|
||||
for (listener in listeners) {
|
||||
if (listener.onDown(e)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||
for (listener in listeners) {
|
||||
if (listener.onDoubleTap(e)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onDoubleTapEvent(e: MotionEvent): Boolean {
|
||||
for (listener in listeners) {
|
||||
if (listener.onDoubleTapEvent(e)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||
for (listener in listeners) {
|
||||
if (listener.onSingleTapConfirmed(e)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
@Synchronized
|
||||
override fun onContextClick(e: MotionEvent): Boolean {
|
||||
for (listener in listeners) {
|
||||
if (listener.onContextClick(e)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.zoomable;
|
||||
|
||||
import android.graphics.Matrix;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class MultiZoomableControllerListener implements ZoomableController.Listener {
|
||||
|
||||
private final List<ZoomableController.Listener> mListeners = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public synchronized void onTransformBegin(Matrix transform) {
|
||||
for (ZoomableController.Listener listener : mListeners) {
|
||||
listener.onTransformBegin(transform);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onTransformChanged(Matrix transform) {
|
||||
for (ZoomableController.Listener listener : mListeners) {
|
||||
listener.onTransformChanged(transform);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onTransformEnd(Matrix transform) {
|
||||
for (ZoomableController.Listener listener : mListeners) {
|
||||
listener.onTransformEnd(transform);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void addListener(ZoomableController.Listener listener) {
|
||||
mListeners.add(listener);
|
||||
}
|
||||
|
||||
public synchronized void removeListener(ZoomableController.Listener listener) {
|
||||
mListeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.zoomable
|
||||
|
||||
import android.graphics.Matrix
|
||||
import java.util.ArrayList
|
||||
|
||||
/**
|
||||
* MultiZoomableControllerListener that allows multiple listeners to be added and notified about
|
||||
* transform events.
|
||||
*
|
||||
* NOTE: The order of the listeners is important. Listeners can consume transform events.
|
||||
*/
|
||||
class MultiZoomableControllerListener : ZoomableController.Listener {
|
||||
|
||||
private val listeners: MutableList<ZoomableController.Listener> = mutableListOf()
|
||||
|
||||
@Synchronized
|
||||
override fun onTransformBegin(transform: Matrix) {
|
||||
for (listener in listeners) {
|
||||
listener.onTransformBegin(transform)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onTransformChanged(transform: Matrix) {
|
||||
for (listener in listeners) {
|
||||
listener.onTransformChanged(transform)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onTransformEnd(transform: Matrix) {
|
||||
for (listener in listeners) {
|
||||
listener.onTransformEnd(transform)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addListener(listener: ZoomableController.Listener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeListener(listener: ZoomableController.Listener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.zoomable;
|
||||
package fr.free.nrw.commons.media.zoomControllers.zoomable
|
||||
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.RectF;
|
||||
import android.view.MotionEvent;
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.RectF
|
||||
import android.view.MotionEvent
|
||||
|
||||
/**
|
||||
* Interface for implementing a controller that works with {@link ZoomableDraweeView} to control the
|
||||
* Interface for implementing a controller that works with [ZoomableDraweeView] to control the
|
||||
* zoom.
|
||||
*/
|
||||
public interface ZoomableController {
|
||||
interface ZoomableController {
|
||||
|
||||
/** Listener interface. */
|
||||
interface Listener {
|
||||
|
|
@ -18,21 +18,21 @@ public interface ZoomableController {
|
|||
*
|
||||
* @param transform the current transform matrix
|
||||
*/
|
||||
void onTransformBegin(Matrix transform);
|
||||
fun onTransformBegin(transform: Matrix)
|
||||
|
||||
/**
|
||||
* Notifies the view that the transform changed.
|
||||
*
|
||||
* @param transform the new matrix
|
||||
*/
|
||||
void onTransformChanged(Matrix transform);
|
||||
fun onTransformChanged(transform: Matrix)
|
||||
|
||||
/**
|
||||
* Notifies the view that the transform ended.
|
||||
*
|
||||
* @param transform the current transform matrix
|
||||
*/
|
||||
void onTransformEnd(Matrix transform);
|
||||
fun onTransformEnd(transform: Matrix)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -40,22 +40,21 @@ public interface ZoomableController {
|
|||
*
|
||||
* @param enabled whether to enable the controller
|
||||
*/
|
||||
void setEnabled(boolean enabled);
|
||||
fun setEnabled(enabled: Boolean)
|
||||
|
||||
/**
|
||||
* Gets whether the controller is enabled. This should return the last value passed to {@link
|
||||
* #setEnabled}.
|
||||
*
|
||||
* Gets whether the controller is enabled. This should return the last value passed
|
||||
* to [setEnabled].
|
||||
* @return whether the controller is enabled.
|
||||
*/
|
||||
boolean isEnabled();
|
||||
fun isEnabled(): Boolean
|
||||
|
||||
/**
|
||||
* Sets the listener for the controller to call back when the matrix changes.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
void setListener(Listener listener);
|
||||
fun setListener(listener: Listener?)
|
||||
|
||||
/**
|
||||
* Gets the current scale factor. A convenience method for calculating the scale from the
|
||||
|
|
@ -63,10 +62,10 @@ public interface ZoomableController {
|
|||
*
|
||||
* @return the current scale factor
|
||||
*/
|
||||
float getScaleFactor();
|
||||
fun getScaleFactor(): Float
|
||||
|
||||
/** Returns true if the zoomable transform is identity matrix, and the controller is idle. */
|
||||
boolean isIdentity();
|
||||
fun isIdentity(): Boolean
|
||||
|
||||
/**
|
||||
* Returns true if the transform was corrected during the last update.
|
||||
|
|
@ -74,27 +73,27 @@ public interface ZoomableController {
|
|||
* <p>This mainly happens when a gesture would cause the image to get out of limits and the
|
||||
* transform gets corrected in order to prevent that.
|
||||
*/
|
||||
boolean wasTransformCorrected();
|
||||
fun wasTransformCorrected(): Boolean
|
||||
|
||||
/** See {@link androidx.core.view.ScrollingView}. */
|
||||
int computeHorizontalScrollRange();
|
||||
/** See [androidx.core.view.ScrollingView]. */
|
||||
fun computeHorizontalScrollRange(): Int
|
||||
|
||||
int computeHorizontalScrollOffset();
|
||||
fun computeHorizontalScrollOffset(): Int
|
||||
|
||||
int computeHorizontalScrollExtent();
|
||||
fun computeHorizontalScrollExtent(): Int
|
||||
|
||||
int computeVerticalScrollRange();
|
||||
fun computeVerticalScrollRange(): Int
|
||||
|
||||
int computeVerticalScrollOffset();
|
||||
fun computeVerticalScrollOffset(): Int
|
||||
|
||||
int computeVerticalScrollExtent();
|
||||
fun computeVerticalScrollExtent(): Int
|
||||
|
||||
/**
|
||||
* Gets the current transform.
|
||||
*
|
||||
* @return the transform
|
||||
*/
|
||||
Matrix getTransform();
|
||||
fun getTransform(): Matrix
|
||||
|
||||
/**
|
||||
* Sets the bounds of the image post transform prior to application of the zoomable
|
||||
|
|
@ -102,14 +101,14 @@ public interface ZoomableController {
|
|||
*
|
||||
* @param imageBounds the bounds of the image
|
||||
*/
|
||||
void setImageBounds(RectF imageBounds);
|
||||
fun setImageBounds(imageBounds: RectF)
|
||||
|
||||
/**
|
||||
* Sets the bounds of the view.
|
||||
*
|
||||
* @param viewBounds the bounds of the view
|
||||
*/
|
||||
void setViewBounds(RectF viewBounds);
|
||||
fun setViewBounds(viewBounds: RectF)
|
||||
|
||||
/**
|
||||
* Allows the controller to handle a touch event.
|
||||
|
|
@ -117,5 +116,5 @@ public interface ZoomableController {
|
|||
* @param event the touch event
|
||||
* @return whether the controller handled the event
|
||||
*/
|
||||
boolean onTouchEvent(MotionEvent event);
|
||||
fun onTouchEvent(event: MotionEvent): Boolean
|
||||
}
|
||||
|
|
@ -1,417 +0,0 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.zoomable;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ScrollingView;
|
||||
import com.facebook.common.internal.Preconditions;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.drawee.controller.AbstractDraweeController;
|
||||
import com.facebook.drawee.controller.BaseControllerListener;
|
||||
import com.facebook.drawee.controller.ControllerListener;
|
||||
import com.facebook.drawee.drawable.ScalingUtils;
|
||||
import com.facebook.drawee.generic.GenericDraweeHierarchy;
|
||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
||||
import com.facebook.drawee.generic.GenericDraweeHierarchyInflater;
|
||||
import com.facebook.drawee.interfaces.DraweeController;
|
||||
import com.facebook.drawee.view.DraweeView;
|
||||
|
||||
|
||||
/**
|
||||
* DraweeView that has zoomable capabilities.
|
||||
*
|
||||
* <p>Once the image loads, pinch-to-zoom and translation gestures are enabled.
|
||||
*/
|
||||
public class ZoomableDraweeView extends DraweeView<GenericDraweeHierarchy>
|
||||
implements ScrollingView {
|
||||
|
||||
private static final Class<?> TAG = ZoomableDraweeView.class;
|
||||
|
||||
private static final float HUGE_IMAGE_SCALE_FACTOR_THRESHOLD = 1.1f;
|
||||
|
||||
private final RectF mImageBounds = new RectF();
|
||||
private final RectF mViewBounds = new RectF();
|
||||
|
||||
private DraweeController mHugeImageController;
|
||||
private ZoomableController mZoomableController;
|
||||
private GestureDetector mTapGestureDetector;
|
||||
private boolean mAllowTouchInterceptionWhileZoomed = true;
|
||||
|
||||
private boolean mIsDialtoneEnabled = false;
|
||||
private boolean mZoomingEnabled = true;
|
||||
private TransformationListener transformationListener;
|
||||
|
||||
private final ControllerListener mControllerListener =
|
||||
new BaseControllerListener<Object>() {
|
||||
@Override
|
||||
public void onFinalImageSet(
|
||||
String id, @Nullable Object imageInfo, @Nullable Animatable animatable) {
|
||||
ZoomableDraweeView.this.onFinalImageSet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRelease(String id) {
|
||||
ZoomableDraweeView.this.onRelease();
|
||||
}
|
||||
};
|
||||
|
||||
private final ZoomableController.Listener mZoomableListener =
|
||||
new ZoomableController.Listener() {
|
||||
@Override
|
||||
public void onTransformBegin(Matrix transform) {}
|
||||
|
||||
@Override
|
||||
public void onTransformChanged(Matrix transform) {
|
||||
ZoomableDraweeView.this.onTransformChanged(transform);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransformEnd(Matrix transform) {
|
||||
if (null != transformationListener) {
|
||||
transformationListener.onTransformationEnd();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public void setTransformationListener(
|
||||
TransformationListener transformationListener) {
|
||||
this.transformationListener = transformationListener;
|
||||
}
|
||||
|
||||
private final GestureListenerWrapper mTapListenerWrapper = new GestureListenerWrapper();
|
||||
|
||||
public ZoomableDraweeView(Context context, GenericDraweeHierarchy hierarchy) {
|
||||
super(context);
|
||||
setHierarchy(hierarchy);
|
||||
init();
|
||||
}
|
||||
|
||||
public ZoomableDraweeView(Context context) {
|
||||
super(context);
|
||||
inflateHierarchy(context, null);
|
||||
init();
|
||||
}
|
||||
|
||||
public ZoomableDraweeView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
inflateHierarchy(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public ZoomableDraweeView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
inflateHierarchy(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
protected void inflateHierarchy(Context context, @Nullable AttributeSet attrs) {
|
||||
Resources resources = context.getResources();
|
||||
GenericDraweeHierarchyBuilder builder =
|
||||
new GenericDraweeHierarchyBuilder(resources)
|
||||
.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER);
|
||||
GenericDraweeHierarchyInflater.updateBuilder(builder, context, attrs);
|
||||
setAspectRatio(builder.getDesiredAspectRatio());
|
||||
setHierarchy(builder.build());
|
||||
}
|
||||
|
||||
private void init() {
|
||||
mZoomableController = createZoomableController();
|
||||
mZoomableController.setListener(mZoomableListener);
|
||||
mTapGestureDetector = new GestureDetector(getContext(), mTapListenerWrapper);
|
||||
}
|
||||
|
||||
public void setIsDialtoneEnabled(boolean isDialtoneEnabled) {
|
||||
mIsDialtoneEnabled = isDialtoneEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the original image bounds, in view-absolute coordinates.
|
||||
*
|
||||
* <p>The original image bounds are those reported by the hierarchy. The hierarchy itself may
|
||||
* apply scaling on its own (e.g. due to scale type) so the reported bounds are not necessarily
|
||||
* the same as the actual bitmap dimensions. In other words, the original image bounds correspond
|
||||
* to the image bounds within this view when no zoomable transformation is applied, but including
|
||||
* the potential scaling of the hierarchy. Having the actual bitmap dimensions abstracted away
|
||||
* from this view greatly simplifies implementation because the actual bitmap may change (e.g.
|
||||
* when a high-res image arrives and replaces the previously set low-res image). With proper
|
||||
* hierarchy scaling (e.g. FIT_CENTER), this underlying change will not affect this view nor the
|
||||
* zoomable transformation in any way.
|
||||
*/
|
||||
protected void getImageBounds(RectF outBounds) {
|
||||
getHierarchy().getActualImageBounds(outBounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the bounds used to limit the translation, in view-absolute coordinates.
|
||||
*
|
||||
* <p>These bounds are passed to the zoomable controller in order to limit the translation. The
|
||||
* image is attempted to be centered within the limit bounds if the transformed image is smaller.
|
||||
* There will be no empty spaces within the limit bounds if the transformed image is bigger. This
|
||||
* applies to each dimension (horizontal and vertical) independently.
|
||||
*
|
||||
* <p>Unless overridden by a subclass, these bounds are same as the view bounds.
|
||||
*/
|
||||
protected void getLimitBounds(RectF outBounds) {
|
||||
outBounds.set(0, 0, getWidth(), getHeight());
|
||||
}
|
||||
|
||||
/** Sets a custom zoomable controller, instead of using the default one. */
|
||||
public void setZoomableController(ZoomableController zoomableController) {
|
||||
Preconditions.checkNotNull(zoomableController);
|
||||
mZoomableController.setListener(null);
|
||||
mZoomableController = zoomableController;
|
||||
mZoomableController.setListener(mZoomableListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the zoomable controller.
|
||||
*
|
||||
* <p>Zoomable controller can be used to zoom to point, or to map point from view to image
|
||||
* coordinates for instance.
|
||||
*/
|
||||
public ZoomableController getZoomableController() {
|
||||
return mZoomableController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the parent view can intercept touch events while zoomed. This can be used, for
|
||||
* example, to swipe between images in a view pager while zoomed.
|
||||
*
|
||||
* @return true if touch events can be intercepted
|
||||
*/
|
||||
public boolean allowsTouchInterceptionWhileZoomed() {
|
||||
return mAllowTouchInterceptionWhileZoomed;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is set to true, parent views can intercept touch events while the view is zoomed. For
|
||||
* example, this can be used to swipe between images in a view pager while zoomed.
|
||||
*
|
||||
* @param allowTouchInterceptionWhileZoomed true if the parent needs to intercept touches
|
||||
*/
|
||||
public void setAllowTouchInterceptionWhileZoomed(boolean allowTouchInterceptionWhileZoomed) {
|
||||
mAllowTouchInterceptionWhileZoomed = allowTouchInterceptionWhileZoomed;
|
||||
}
|
||||
|
||||
/** Sets the tap listener. */
|
||||
public void setTapListener(GestureDetector.SimpleOnGestureListener tapListener) {
|
||||
mTapListenerWrapper.setListener(tapListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether long-press tap detection is enabled. Unfortunately, long-press conflicts with
|
||||
* onDoubleTapEvent.
|
||||
*/
|
||||
public void setIsLongpressEnabled(boolean enabled) {
|
||||
mTapGestureDetector.setIsLongpressEnabled(enabled);
|
||||
}
|
||||
|
||||
public void setZoomingEnabled(boolean zoomingEnabled) {
|
||||
mZoomingEnabled = zoomingEnabled;
|
||||
mZoomableController.setEnabled(false);
|
||||
}
|
||||
|
||||
/** Sets the image controller. */
|
||||
@Override
|
||||
public void setController(@Nullable DraweeController controller) {
|
||||
setControllers(controller, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the controllers for the normal and huge image.
|
||||
*
|
||||
* <p>The huge image controller is used after the image gets scaled above a certain threshold.
|
||||
*
|
||||
* <p>IMPORTANT: in order to avoid a flicker when switching to the huge image, the huge image
|
||||
* controller should have the normal-image-uri set as its low-res-uri.
|
||||
*
|
||||
* @param controller controller to be initially used
|
||||
* @param hugeImageController controller to be used after the client starts zooming-in
|
||||
*/
|
||||
public void setControllers(
|
||||
@Nullable DraweeController controller, @Nullable DraweeController hugeImageController) {
|
||||
setControllersInternal(null, null);
|
||||
mZoomableController.setEnabled(false);
|
||||
setControllersInternal(controller, hugeImageController);
|
||||
}
|
||||
|
||||
private void setControllersInternal(
|
||||
@Nullable DraweeController controller, @Nullable DraweeController hugeImageController) {
|
||||
removeControllerListener(getController());
|
||||
addControllerListener(controller);
|
||||
mHugeImageController = hugeImageController;
|
||||
super.setController(controller);
|
||||
}
|
||||
|
||||
private void maybeSetHugeImageController() {
|
||||
if (mHugeImageController != null
|
||||
&& mZoomableController.getScaleFactor() > HUGE_IMAGE_SCALE_FACTOR_THRESHOLD) {
|
||||
setControllersInternal(mHugeImageController, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeControllerListener(DraweeController controller) {
|
||||
if (controller instanceof AbstractDraweeController) {
|
||||
((AbstractDraweeController) controller).removeControllerListener(mControllerListener);
|
||||
}
|
||||
}
|
||||
|
||||
private void addControllerListener(DraweeController controller) {
|
||||
if (controller instanceof AbstractDraweeController) {
|
||||
((AbstractDraweeController) controller).addControllerListener(mControllerListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
int saveCount = canvas.save();
|
||||
canvas.concat(mZoomableController.getTransform());
|
||||
try {
|
||||
super.onDraw(canvas);
|
||||
} catch (Exception e) {
|
||||
DraweeController controller = getController();
|
||||
if (controller != null && controller instanceof AbstractDraweeController) {
|
||||
Object callerContext = ((AbstractDraweeController) controller).getCallerContext();
|
||||
if (callerContext != null) {
|
||||
throw new RuntimeException(
|
||||
String.format("Exception in onDraw, callerContext=%s", callerContext.toString()), e);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
canvas.restoreToCount(saveCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
int a = event.getActionMasked();
|
||||
FLog.v(getLogTag(), "onTouchEvent: %d, view %x, received", a, this.hashCode());
|
||||
if (!mIsDialtoneEnabled && mTapGestureDetector.onTouchEvent(event)) {
|
||||
FLog.v(
|
||||
getLogTag(),
|
||||
"onTouchEvent: %d, view %x, handled by tap gesture detector",
|
||||
a,
|
||||
this.hashCode());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!mIsDialtoneEnabled && mZoomableController.onTouchEvent(event)) {
|
||||
FLog.v(
|
||||
getLogTag(),
|
||||
"onTouchEvent: %d, view %x, handled by zoomable controller",
|
||||
a,
|
||||
this.hashCode());
|
||||
if (!mAllowTouchInterceptionWhileZoomed && !mZoomableController.isIdentity()) {
|
||||
getParent().requestDisallowInterceptTouchEvent(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (super.onTouchEvent(event)) {
|
||||
FLog.v(getLogTag(), "onTouchEvent: %d, view %x, handled by the super", a, this.hashCode());
|
||||
return true;
|
||||
}
|
||||
// None of our components reported that they handled the touch event. Upon returning false
|
||||
// from this method, our parent won't send us any more events for this gesture. Unfortunately,
|
||||
// some components may have started a delayed action, such as a long-press timer, and since we
|
||||
// won't receive an ACTION_UP that would cancel that timer, a false event may be triggered.
|
||||
// To prevent that we explicitly send one last cancel event when returning false.
|
||||
MotionEvent cancelEvent = MotionEvent.obtain(event);
|
||||
cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
|
||||
mTapGestureDetector.onTouchEvent(cancelEvent);
|
||||
mZoomableController.onTouchEvent(cancelEvent);
|
||||
cancelEvent.recycle();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int computeHorizontalScrollRange() {
|
||||
return mZoomableController.computeHorizontalScrollRange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int computeHorizontalScrollOffset() {
|
||||
return mZoomableController.computeHorizontalScrollOffset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int computeHorizontalScrollExtent() {
|
||||
return mZoomableController.computeHorizontalScrollExtent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int computeVerticalScrollRange() {
|
||||
return mZoomableController.computeVerticalScrollRange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int computeVerticalScrollOffset() {
|
||||
return mZoomableController.computeVerticalScrollOffset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int computeVerticalScrollExtent() {
|
||||
return mZoomableController.computeVerticalScrollExtent();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
FLog.v(getLogTag(), "onLayout: view %x", this.hashCode());
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
updateZoomableControllerBounds();
|
||||
}
|
||||
|
||||
private void onFinalImageSet() {
|
||||
FLog.v(getLogTag(), "onFinalImageSet: view %x", this.hashCode());
|
||||
if (!mZoomableController.isEnabled() && mZoomingEnabled) {
|
||||
mZoomableController.setEnabled(true);
|
||||
updateZoomableControllerBounds();
|
||||
}
|
||||
}
|
||||
|
||||
private void onRelease() {
|
||||
FLog.v(getLogTag(), "onRelease: view %x", this.hashCode());
|
||||
mZoomableController.setEnabled(false);
|
||||
}
|
||||
|
||||
protected void onTransformChanged(Matrix transform) {
|
||||
FLog.v(getLogTag(), "onTransformChanged: view %x, transform: %s", this.hashCode(), transform);
|
||||
maybeSetHugeImageController();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
protected void updateZoomableControllerBounds() {
|
||||
getImageBounds(mImageBounds);
|
||||
getLimitBounds(mViewBounds);
|
||||
mZoomableController.setImageBounds(mImageBounds);
|
||||
mZoomableController.setViewBounds(mViewBounds);
|
||||
FLog.v(
|
||||
getLogTag(),
|
||||
"updateZoomableControllerBounds: view %x, view bounds: %s, image bounds: %s",
|
||||
this.hashCode(),
|
||||
mViewBounds,
|
||||
mImageBounds);
|
||||
}
|
||||
|
||||
protected Class<?> getLogTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
protected ZoomableController createZoomableController() {
|
||||
return AnimatedZoomableController.newInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this, If someone is willing to listen to scale change
|
||||
*/
|
||||
public interface TransformationListener{
|
||||
void onTransformationEnd();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,320 @@
|
|||
package fr.free.nrw.commons.media.zoomControllers.zoomable
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.util.AttributeSet
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
|
||||
import androidx.core.view.ScrollingView
|
||||
import com.facebook.common.internal.Preconditions
|
||||
import com.facebook.common.logging.FLog
|
||||
import com.facebook.drawee.controller.AbstractDraweeController
|
||||
import com.facebook.drawee.controller.BaseControllerListener
|
||||
import com.facebook.drawee.drawable.ScalingUtils
|
||||
import com.facebook.drawee.generic.GenericDraweeHierarchy
|
||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder
|
||||
import com.facebook.drawee.generic.GenericDraweeHierarchyInflater
|
||||
import com.facebook.drawee.interfaces.DraweeController
|
||||
import com.facebook.drawee.view.DraweeView
|
||||
|
||||
/**
|
||||
* DraweeView that has zoomable capabilities.
|
||||
*
|
||||
* <p>Once the image loads, pinch-to-zoom and translation gestures are enabled.
|
||||
*/
|
||||
open class ZoomableDraweeView : DraweeView<GenericDraweeHierarchy>, ScrollingView {
|
||||
|
||||
companion object {
|
||||
private val TAG = ZoomableDraweeView::class.java
|
||||
private const val HUGE_IMAGE_SCALE_FACTOR_THRESHOLD = 1.1f
|
||||
}
|
||||
|
||||
private val imageBounds = RectF()
|
||||
private val viewBounds = RectF()
|
||||
|
||||
private var hugeImageController: DraweeController? = null
|
||||
private var zoomableController: ZoomableController = createZoomableController()
|
||||
private var tapGestureDetector: GestureDetector? = null
|
||||
private var allowTouchInterceptionWhileZoomed = true
|
||||
private var isDialToneEnabled = false
|
||||
private var zoomingEnabled = true
|
||||
private var transformationListener: TransformationListener? = null
|
||||
|
||||
private val controllerListener = object : BaseControllerListener<Any>() {
|
||||
override fun onFinalImageSet(id: String, imageInfo: Any?, animatable: Animatable?) {
|
||||
this@ZoomableDraweeView.onFinalImageSet()
|
||||
}
|
||||
|
||||
override fun onRelease(id: String) {
|
||||
this@ZoomableDraweeView.onRelease()
|
||||
}
|
||||
}
|
||||
|
||||
private val zoomableListener = object : ZoomableController.Listener {
|
||||
override fun onTransformBegin(transform: Matrix) {}
|
||||
|
||||
override fun onTransformChanged(transform: Matrix) {
|
||||
this@ZoomableDraweeView.onTransformChanged(transform)
|
||||
}
|
||||
|
||||
override fun onTransformEnd(transform: Matrix) {
|
||||
transformationListener?.onTransformationEnd()
|
||||
}
|
||||
}
|
||||
|
||||
private val tapListenerWrapper = GestureListenerWrapper()
|
||||
|
||||
constructor(context: Context, hierarchy: GenericDraweeHierarchy) : super(context) {
|
||||
setHierarchy(hierarchy)
|
||||
init()
|
||||
}
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
inflateHierarchy(context, null)
|
||||
init()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
|
||||
inflateHierarchy(context, attrs)
|
||||
init()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyle: Int)
|
||||
: super(context, attrs, defStyle) {
|
||||
inflateHierarchy(context, attrs)
|
||||
init()
|
||||
}
|
||||
|
||||
fun setTransformationListener(transformationListener: TransformationListener) {
|
||||
this.transformationListener = transformationListener
|
||||
}
|
||||
|
||||
protected fun inflateHierarchy(context: Context, attrs: AttributeSet?) {
|
||||
val resources: Resources = context.resources
|
||||
val builder = GenericDraweeHierarchyBuilder(resources)
|
||||
.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
|
||||
GenericDraweeHierarchyInflater.updateBuilder(builder, context, attrs)
|
||||
aspectRatio = builder.desiredAspectRatio
|
||||
setHierarchy(builder.build())
|
||||
}
|
||||
|
||||
private fun init() {
|
||||
zoomableController.setListener(zoomableListener)
|
||||
tapGestureDetector = GestureDetector(context, tapListenerWrapper)
|
||||
}
|
||||
|
||||
fun setIsDialToneEnabled(isDialtoneEnabled: Boolean) {
|
||||
this.isDialToneEnabled = isDialtoneEnabled
|
||||
}
|
||||
|
||||
protected fun getImageBounds(outBounds: RectF) {
|
||||
hierarchy.getActualImageBounds(outBounds)
|
||||
}
|
||||
|
||||
protected fun getLimitBounds(outBounds: RectF) {
|
||||
outBounds.set(0f, 0f, width.toFloat(), height.toFloat())
|
||||
}
|
||||
|
||||
fun setZoomableController(zoomableController: ZoomableController) {
|
||||
Preconditions.checkNotNull(zoomableController)
|
||||
this.zoomableController.setListener(null)
|
||||
this.zoomableController = zoomableController
|
||||
this.zoomableController.setListener(zoomableListener)
|
||||
}
|
||||
|
||||
fun getZoomableController(): ZoomableController = zoomableController
|
||||
|
||||
fun allowsTouchInterceptionWhileZoomed(): Boolean = allowTouchInterceptionWhileZoomed
|
||||
|
||||
fun setAllowTouchInterceptionWhileZoomed(allow: Boolean) {
|
||||
allowTouchInterceptionWhileZoomed = allow
|
||||
}
|
||||
|
||||
fun setTapListener(tapListener: GestureDetector.SimpleOnGestureListener) {
|
||||
tapListenerWrapper.setListener(tapListener)
|
||||
}
|
||||
|
||||
fun setIsLongpressEnabled(enabled: Boolean) {
|
||||
tapGestureDetector?.setIsLongpressEnabled(enabled)
|
||||
}
|
||||
|
||||
fun setZoomingEnabled(zoomingEnabled: Boolean) {
|
||||
this.zoomingEnabled = zoomingEnabled
|
||||
zoomableController.setEnabled(false)
|
||||
}
|
||||
|
||||
override fun setController(controller: DraweeController?) {
|
||||
setControllers(controller, null)
|
||||
}
|
||||
|
||||
fun setControllers(controller: DraweeController?, hugeImageController: DraweeController?) {
|
||||
setControllersInternal(null, null)
|
||||
zoomableController.setEnabled(false)
|
||||
setControllersInternal(controller, hugeImageController)
|
||||
}
|
||||
|
||||
private fun setControllersInternal(
|
||||
controller: DraweeController?,
|
||||
hugeImageController: DraweeController?
|
||||
) {
|
||||
removeControllerListener(getController())
|
||||
addControllerListener(controller)
|
||||
this.hugeImageController = hugeImageController
|
||||
super.setController(controller)
|
||||
}
|
||||
|
||||
private fun maybeSetHugeImageController() {
|
||||
if (
|
||||
hugeImageController != null
|
||||
&&
|
||||
zoomableController.getScaleFactor() > HUGE_IMAGE_SCALE_FACTOR_THRESHOLD
|
||||
) {
|
||||
setControllersInternal(hugeImageController, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeControllerListener(controller: DraweeController?) {
|
||||
if (controller is AbstractDraweeController<*, *>) {
|
||||
controller.removeControllerListener(controllerListener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addControllerListener(controller: DraweeController?) {
|
||||
if (controller is AbstractDraweeController<*, *>) {
|
||||
controller.addControllerListener(controllerListener)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
val saveCount = canvas.save()
|
||||
canvas.concat(zoomableController.getTransform())
|
||||
try {
|
||||
super.onDraw(canvas)
|
||||
} catch (e: Exception) {
|
||||
val controller = controller
|
||||
if (controller is AbstractDraweeController<*, *>) {
|
||||
val callerContext = controller.callerContext
|
||||
if (callerContext != null) {
|
||||
throw RuntimeException("Exception in onDraw, callerContext=${callerContext}", e)
|
||||
}
|
||||
}
|
||||
throw e
|
||||
}
|
||||
canvas.restoreToCount(saveCount)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
var action = event.actionMasked
|
||||
FLog.v(getLogTag(), "onTouchEvent: $action, view ${hashCode()}, received")
|
||||
|
||||
if (!isDialToneEnabled && tapGestureDetector?.onTouchEvent(event) == true) {
|
||||
FLog.v(
|
||||
getLogTag(),
|
||||
"onTouchEvent: $action, view ${hashCode()}, handled by tap gesture detector"
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
if (!isDialToneEnabled && zoomableController.onTouchEvent(event)) {
|
||||
FLog.v(
|
||||
getLogTag(),
|
||||
"onTouchEvent: $action, view ${hashCode()}, handled by zoomable controller"
|
||||
)
|
||||
if (!allowTouchInterceptionWhileZoomed && !zoomableController.isIdentity()) {
|
||||
parent.requestDisallowInterceptTouchEvent(true)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if (super.onTouchEvent(event)) {
|
||||
FLog.v(
|
||||
getLogTag(),
|
||||
"onTouchEvent: $action, view ${hashCode()}, handled by the super"
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
// If none of our components handled the event, we send a cancel event to avoid unwanted actions.
|
||||
val cancelEvent = MotionEvent.obtain(event).apply { action = MotionEvent.ACTION_CANCEL }
|
||||
tapGestureDetector?.onTouchEvent(cancelEvent)
|
||||
zoomableController.onTouchEvent(cancelEvent)
|
||||
cancelEvent.recycle()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override fun computeHorizontalScrollRange(): Int =
|
||||
zoomableController.computeHorizontalScrollRange()
|
||||
|
||||
override fun computeHorizontalScrollOffset(): Int =
|
||||
zoomableController.computeHorizontalScrollOffset()
|
||||
|
||||
override fun computeHorizontalScrollExtent(): Int =
|
||||
zoomableController.computeHorizontalScrollExtent()
|
||||
|
||||
override fun computeVerticalScrollRange(): Int =
|
||||
zoomableController.computeVerticalScrollRange()
|
||||
|
||||
override fun computeVerticalScrollOffset(): Int =
|
||||
zoomableController.computeVerticalScrollOffset()
|
||||
|
||||
override fun computeVerticalScrollExtent(): Int =
|
||||
zoomableController.computeVerticalScrollExtent()
|
||||
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
FLog.v(getLogTag(), "onLayout: view ${hashCode()}")
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
updateZoomableControllerBounds()
|
||||
}
|
||||
|
||||
private fun onFinalImageSet() {
|
||||
FLog.v(getLogTag(), "onFinalImageSet: view ${hashCode()}")
|
||||
if (!zoomableController.isEnabled() && zoomingEnabled) {
|
||||
zoomableController.setEnabled(true)
|
||||
updateZoomableControllerBounds()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRelease() {
|
||||
FLog.v(getLogTag(), "onRelease: view ${hashCode()}")
|
||||
zoomableController.setEnabled(false)
|
||||
}
|
||||
|
||||
protected fun onTransformChanged(transform: Matrix) {
|
||||
FLog.v(getLogTag(), "onTransformChanged: view ${hashCode()}, transform: $transform")
|
||||
maybeSetHugeImageController()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
protected fun updateZoomableControllerBounds() {
|
||||
getImageBounds(imageBounds)
|
||||
getLimitBounds(viewBounds)
|
||||
zoomableController.setImageBounds(imageBounds)
|
||||
zoomableController.setViewBounds(viewBounds)
|
||||
|
||||
FLog.v(
|
||||
getLogTag(),
|
||||
"updateZoomableControllerBounds: view ${hashCode()}, " +
|
||||
"view bounds: $viewBounds, image bounds: $imageBounds"
|
||||
)
|
||||
}
|
||||
|
||||
protected fun getLogTag(): Class<*> = TAG
|
||||
|
||||
protected fun createZoomableController(): ZoomableController = AnimatedZoomableController.newInstance()
|
||||
|
||||
/**
|
||||
* Interface to listen for scale change events.
|
||||
*/
|
||||
interface TransformationListener {
|
||||
fun onTransformationEnd()
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.AdapterView.OnItemClickListener
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
|
|
@ -330,6 +331,9 @@ class UploadMediaDetailAdapter : RecyclerView.Adapter<UploadMediaDetailAdapter.V
|
|||
|
||||
listView.adapter = languagesAdapter
|
||||
|
||||
dialog.findViewById<Button>(R.id.cancel_button)
|
||||
.setOnClickListener { v: View? -> dialog.dismiss() }
|
||||
|
||||
editText.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) =
|
||||
hideRecentLanguagesSection()
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@
|
|||
<string name="no">Neen</string>
|
||||
<string name="media_detail_caption">Beschrëftung</string>
|
||||
<string name="media_detail_title">Titel</string>
|
||||
<string name="media_detail_depiction">Motiven</string>
|
||||
<string name="media_detail_description">Beschreiwung</string>
|
||||
<string name="media_detail_discussion">Diskussioun</string>
|
||||
<string name="media_detail_author">Auteur</string>
|
||||
|
|
|
|||
|
|
@ -406,10 +406,10 @@
|
|||
<string name="preference_author_name_toggle_summary">Gebruik een aangepaste auteursnaam in plaats van uw gebruikersnaam tijdens het uploaden van foto\'s</string>
|
||||
<string name="preference_author_name">Aangepaste auteursnaam</string>
|
||||
<string name="contributions_fragment">Bijdragen</string>
|
||||
<string name="nearby_fragment">Dichtbij</string>
|
||||
<string name="nearby_fragment">In de buurt</string>
|
||||
<string name="notifications">Meldingen</string>
|
||||
<string name="read_notifications">Meldingen (gelezen)</string>
|
||||
<string name="display_nearby_notification">Meldingen dichtbij weergeven</string>
|
||||
<string name="display_nearby_notification">Melding in de buurt weergeven</string>
|
||||
<string name="display_nearby_notification_summary">Toon in-app-melding voor de dichtstbijzijnde plaats die foto\'s nodig heeft</string>
|
||||
<string name="list_sheet">Lijst</string>
|
||||
<string name="storage_permission">Toestemming om op te slaan</string>
|
||||
|
|
@ -615,7 +615,7 @@
|
|||
<string name="recommend_high_accuracy_mode">Kies voor de beste resultaten de modus van hoge nauwkeurigheid.</string>
|
||||
<string name="ask_to_turn_location_on">Locatie inschakelen?</string>
|
||||
<string name="ask_to_turn_location_on_text">Schakel locatiediensten in zodat de app uw huidige locatie toont</string>
|
||||
<string name="nearby_needs_location">In de Buurt heeft locatie nodig om correct te werken</string>
|
||||
<string name="nearby_needs_location">‘In de buurt’ heeft locatietoegang nodig om correct te werken</string>
|
||||
<string name="explore_map_needs_location">Voor de verkenningskaart is locatietoestemming nodig om afbeeldingen in de buurt weer te geven</string>
|
||||
<string name="upload_map_location_access">U moet locatietoestemming geven om de locatie automatisch in te stellen.</string>
|
||||
<string name="use_location_from_similar_image">Heeft u deze twee foto\'s op dezelfde plek gemaakt? Wilt u de breedtegraad/lengtegraad van de afbeelding rechts gebruiken?</string>
|
||||
|
|
@ -657,7 +657,7 @@
|
|||
<string name="leaderboard_weekly">Wekelijks</string>
|
||||
<string name="leaderboard_all_time">Alle tijden</string>
|
||||
<string name="leaderboard_upload">Uploaden</string>
|
||||
<string name="leaderboard_nearby">In de Buurt</string>
|
||||
<string name="leaderboard_nearby">In de buurt</string>
|
||||
<string name="leaderboard_used">Gebruikt</string>
|
||||
<string name="leaderboard_my_rank_button_text">Mijn ranking</string>
|
||||
<string name="limited_connection_enabled">Beperkte verbindingsmodus ingeschakeld!</string>
|
||||
|
|
@ -724,7 +724,7 @@
|
|||
<string name="edit_depictions">Wijzig items</string>
|
||||
<string name="edit_categories">Categorieën bewerken</string>
|
||||
<string name="advanced_options">Geavanceerde opties</string>
|
||||
<string name="advanced_query_info_text">U kunt de zoekopdracht Dichtbij aanpassen. Als u fouten krijgt, kunt u opnieuw instellen en toepassen.</string>
|
||||
<string name="advanced_query_info_text">U kunt de zoekopdracht ‘In de buurt’ aanpassen. Als u fouten krijgt, kunt u opnieuw instellen en toepassen.</string>
|
||||
<string name="apply">Toepassen</string>
|
||||
<string name="reset">Opnieuw instellen</string>
|
||||
<string name="location_message">Locatiegegevens helpen wiki-bewerkers om uw foto te vinden, waardoor deze veel nuttiger wordt.\nUw recente uploads hebben geen locatie.\nWe raden u aan om de locatie in de instellingen van uw camera-app in te schakelen.\nBedankt voor het uploaden!</string>
|
||||
|
|
@ -835,4 +835,10 @@
|
|||
<string name="account">Account</string>
|
||||
<string name="vanish_account">Account laten verdwijnen</string>
|
||||
<string name="account_vanish_request_confirm_title">Waarschuwing verwijdering account</string>
|
||||
<string name="account_vanish_request_confirm">Verdwijnen is een <b>laatste redmiddel</b> en moet <b>alleen worden gebruikt als u voor altijd wilt stoppen met bewerken</b> en om zoveel mogelijk van uw voorgaande relaties te verbergen.<br/><br/>Accountverwijdering op Wikipedia gebeurt door uw accountnaam te wijzigen opdat anderen uw bijdragen niet meer kunnen herkennen. De procedure wordt accountverdwijning genoemd. <b>Door verdwijnen wordt geen volledige anonimiteit gegarandeerd en worden geen bijdragen aan de projecten verwijderd.</b></string>
|
||||
<string name="caption">Bijschrift</string>
|
||||
<string name="caption_copied_to_clipboard">Bijschrift gekopieerd naar klembord</string>
|
||||
<string name="congratulations_all_pictures_in_this_album_have_been_either_uploaded_or_marked_as_not_for_upload">Gefeliciteerd, alle foto’s in dit album zijn ofwel geüpload ofwel gemarkeerd als ‘niet om te uploaden’.</string>
|
||||
<string name="show_in_explore">Weergeven in Verkennen</string>
|
||||
<string name="show_in_nearby">Weergeven in ‘In de buurt’</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -192,6 +192,7 @@
|
|||
<string name="depictions_edit_helper_edit_message_else">{{Doc-commons-app-depicts}}</string>
|
||||
<string name="title_app_shortcut_bookmark">{{identical|Bookmark}}</string>
|
||||
<string name="theme_default_name">Option to make the app\'s theme follow the global system setting.</string>
|
||||
<string name="nearby_needs_location">‘Nearby’ should be translated as in {{msg-wm|Commons-android-strings-navigation item nearby}}</string>
|
||||
<string name="more">{{Identical|More}}</string>
|
||||
<string name="map_attribution">{{Optional}}\n<code>&amp;#169;</code> is the copyright symbol (©).</string>
|
||||
<string name="categories_tooltip">{{Doc-commons-app-depicts}}</string>
|
||||
|
|
@ -204,9 +205,12 @@
|
|||
<string name="done">{{identical|Done}}</string>
|
||||
<string name="edit_depictions">{{Doc-commons-app-depicts}}</string>
|
||||
<string name="advanced_options">{{Identical|Advanced options}}</string>
|
||||
<string name="advanced_query_info_text">‘Nearby’ should be translated as in {{msg-wm|Commons-android-strings-navigation item nearby}}</string>
|
||||
<string name="explore_map_details">{{Identical|Detail}}</string>
|
||||
<string name="set_up_avatar_toast_string">\"Set as avatar\" should be translated the same as {{msg-wm|Commons-android-strings-menu set avatar}}.</string>
|
||||
<string name="multiple_files_depiction">{{Doc-commons-app-depicts}}</string>
|
||||
<string name="custom_selector_delete">An answer to the question in {{msg-wm|Commons-android-strings-custom selector confirm deletion message}}.</string>
|
||||
<string name="bullet_point">{{optional}}</string>
|
||||
<string name="show_in_explore">“Explore” should be translated as in {{msg-wm|Commons-android-strings-navigation item explore}}</string>
|
||||
<string name="show_in_nearby">‘Nearby’ should be translated as in {{msg-wm|Commons-android-strings-navigation item nearby}}</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@
|
|||
<string name="upload_progress_notification_title_start">%s ಅಪ್ಲೋಡ್ ಸುರು ಆವೊಂದುಂಡು</string>
|
||||
<string name="upload_progress_notification_title_in_progress">%1$s ಅಪ್ಲೋಡ್ ಆವೊಂದುಂಡು</string>
|
||||
<string name="upload_progress_notification_title_finishing">%1$s ಅಪ್ಲೋಡ್ ಕೈದ್ ಆವೊಂದುಂಡು.</string>
|
||||
<string name="upload_failed_notification_title" fuzzy="true">%1$s ಅಪ್ಲೋಡ್ ಸರಿ ಆತಿಜಿ</string>
|
||||
<string name="upload_failed_notification_title">%1$s ಅಪ್ಲೋಡ್ ಆತಿಜಿ</string>
|
||||
<string name="upload_failed_notification_subtitle">ತುಯಾರ ಮೆಲ್ಲ ಒತ್ತುಲೆ</string>
|
||||
<string name="title_activity_contributions">ಎನ್ನ ದಿಂಜಯೀನಾ ವಿಚಾರೊಳು</string>
|
||||
<string name="contribution_state_queued">ದಿಂಜೊಂತುಂಡು</string>
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ class DeleteHelperTest {
|
|||
).thenReturn("Media successfully deleted: Test Media Title")
|
||||
|
||||
val creatorName = "Creator"
|
||||
whenever(media.author).thenReturn("$creatorName")
|
||||
whenever(media.getAuthorOrUser()).thenReturn("$creatorName")
|
||||
whenever(media.filename).thenReturn("Test file.jpg")
|
||||
val makeDeletion = deleteHelper.makeDeletion(
|
||||
context,
|
||||
|
|
@ -133,7 +133,7 @@ class DeleteHelperTest {
|
|||
|
||||
whenever(media.displayTitle).thenReturn("Test file")
|
||||
whenever(media.filename).thenReturn("Test file.jpg")
|
||||
whenever(media.author).thenReturn("Creator (page does not exist)")
|
||||
whenever(media.getAuthorOrUser()).thenReturn("Creator (page does not exist)")
|
||||
|
||||
deleteHelper.makeDeletion(context, media, "Test reason")?.blockingGet()
|
||||
}
|
||||
|
|
@ -148,7 +148,7 @@ class DeleteHelperTest {
|
|||
.thenReturn(Observable.just(false))
|
||||
whenever(media.displayTitle).thenReturn("Test file")
|
||||
whenever(media.filename).thenReturn("Test file.jpg")
|
||||
whenever(media.author).thenReturn("Creator (page does not exist)")
|
||||
whenever(media.getAuthorOrUser()).thenReturn("Creator (page does not exist)")
|
||||
|
||||
deleteHelper.makeDeletion(context, media, "Test reason")?.blockingGet()
|
||||
}
|
||||
|
|
@ -163,7 +163,7 @@ class DeleteHelperTest {
|
|||
.thenReturn(Observable.just(true))
|
||||
whenever(media.displayTitle).thenReturn("Test file")
|
||||
whenever(media.filename).thenReturn("Test file.jpg")
|
||||
whenever(media.author).thenReturn("Creator (page does not exist)")
|
||||
whenever(media.getAuthorOrUser()).thenReturn("Creator (page does not exist)")
|
||||
|
||||
deleteHelper.makeDeletion(context, media, "Test reason")?.blockingGet()
|
||||
}
|
||||
|
|
@ -221,7 +221,7 @@ class DeleteHelperTest {
|
|||
whenever(media.displayTitle).thenReturn("Test file")
|
||||
whenever(media.filename).thenReturn("Test file.jpg")
|
||||
|
||||
whenever(media.author).thenReturn(null)
|
||||
whenever(media.getAuthorOrUser()).thenReturn(null)
|
||||
|
||||
deleteHelper.makeDeletion(context, media, "Test reason")?.blockingGet()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,43 +107,43 @@ class MultiPointerGestureDetectorUnitTest {
|
|||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testIsGestureInProgress() {
|
||||
Assert.assertEquals(detector.isGestureInProgress, false)
|
||||
Assert.assertEquals(detector.isGestureInProgress(), false)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetNewPointerCount() {
|
||||
Assert.assertEquals(detector.newPointerCount, 0)
|
||||
Assert.assertEquals(detector.getNewPointerCount(), 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetPointerCount() {
|
||||
Assert.assertEquals(detector.pointerCount, 0)
|
||||
Assert.assertEquals(detector.getPointerCount(), 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetStartX() {
|
||||
Assert.assertEquals(detector.startX[0], 0.0f)
|
||||
Assert.assertEquals(detector.getStartX()[0], 0.0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetStartY() {
|
||||
Assert.assertEquals(detector.startY[0], 0.0f)
|
||||
Assert.assertEquals(detector.getStartY()[0], 0.0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetCurrentX() {
|
||||
Assert.assertEquals(detector.currentX[0], 0.0f)
|
||||
Assert.assertEquals(detector.getCurrentX()[0], 0.0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetCurrentY() {
|
||||
Assert.assertEquals(detector.currentY[0], 0.0f)
|
||||
Assert.assertEquals(detector.getCurrentY()[0], 0.0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -84,51 +84,51 @@ class TransformGestureDetectorUnitTest {
|
|||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testIsGestureInProgress() {
|
||||
Assert.assertEquals(detector.isGestureInProgress, false)
|
||||
Assert.assertEquals(detector.isGestureInProgress(), false)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetNewPointerCount() {
|
||||
Assert.assertEquals(detector.newPointerCount, 0)
|
||||
Assert.assertEquals(detector.getNewPointerCount(), 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetPointerCount() {
|
||||
Assert.assertEquals(detector.pointerCount, 0)
|
||||
Assert.assertEquals(detector.getPointerCount(), 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetPivotX() {
|
||||
Assert.assertEquals(detector.pivotX, 0.0f)
|
||||
Assert.assertEquals(detector.getPivotX(), 0.0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetPivotY() {
|
||||
Assert.assertEquals(detector.pivotY, 0.0f)
|
||||
Assert.assertEquals(detector.getPivotY(), 0.0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetTranslationX() {
|
||||
Assert.assertEquals(detector.translationX, 0.0f)
|
||||
Assert.assertEquals(detector.getTranslationX(), 0.0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetTranslationY() {
|
||||
Assert.assertEquals(detector.translationY, 0.0f)
|
||||
Assert.assertEquals(detector.getTranslationY(), 0.0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetScaleCaseLessThan2() {
|
||||
Whitebox.setInternalState(detector, "mDetector", mDetector)
|
||||
whenever(mDetector.pointerCount).thenReturn(1)
|
||||
Assert.assertEquals(detector.scale, 1f)
|
||||
whenever(mDetector.getPointerCount()).thenReturn(1)
|
||||
Assert.assertEquals(detector.getScale(), 1f)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -138,20 +138,20 @@ class TransformGestureDetectorUnitTest {
|
|||
array[0] = 0.0f
|
||||
array[1] = 1.0f
|
||||
Whitebox.setInternalState(detector, "mDetector", mDetector)
|
||||
whenever(mDetector.pointerCount).thenReturn(2)
|
||||
whenever(mDetector.startX).thenReturn(array)
|
||||
whenever(mDetector.startY).thenReturn(array)
|
||||
whenever(mDetector.currentX).thenReturn(array)
|
||||
whenever(mDetector.currentY).thenReturn(array)
|
||||
Assert.assertEquals(detector.scale, 1f)
|
||||
whenever(mDetector.getPointerCount()).thenReturn(2)
|
||||
whenever(mDetector.getStartX()).thenReturn(array)
|
||||
whenever(mDetector.getStartY()).thenReturn(array)
|
||||
whenever(mDetector.getCurrentX()).thenReturn(array)
|
||||
whenever(mDetector.getCurrentY()).thenReturn(array)
|
||||
Assert.assertEquals(detector.getScale(), 1f)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetRotationCaseLessThan2() {
|
||||
Whitebox.setInternalState(detector, "mDetector", mDetector)
|
||||
whenever(mDetector.pointerCount).thenReturn(1)
|
||||
Assert.assertEquals(detector.rotation, 0f)
|
||||
whenever(mDetector.getPointerCount()).thenReturn(1)
|
||||
Assert.assertEquals(detector.getRotation(), 0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -161,12 +161,12 @@ class TransformGestureDetectorUnitTest {
|
|||
array[0] = 0.0f
|
||||
array[1] = 1.0f
|
||||
Whitebox.setInternalState(detector, "mDetector", mDetector)
|
||||
whenever(mDetector.pointerCount).thenReturn(2)
|
||||
whenever(mDetector.startX).thenReturn(array)
|
||||
whenever(mDetector.startY).thenReturn(array)
|
||||
whenever(mDetector.currentX).thenReturn(array)
|
||||
whenever(mDetector.currentY).thenReturn(array)
|
||||
Assert.assertEquals(detector.rotation, 0f)
|
||||
whenever(mDetector.getPointerCount()).thenReturn(2)
|
||||
whenever(mDetector.getStartX()).thenReturn(array)
|
||||
whenever(mDetector.getStartY()).thenReturn(array)
|
||||
whenever(mDetector.getCurrentX()).thenReturn(array)
|
||||
whenever(mDetector.getCurrentY()).thenReturn(array)
|
||||
Assert.assertEquals(detector.getRotation(), 0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue