I have a circle as ImageView and I want to be able to smoothly rotate it when swiping with a finger. If I swipe slowly, I want the circle to rotate slowly until the swiping action stops. If I swipe faster, I want the image to rotate more.
I've implemented the code that I've found here. This longer version allows to change swiping direction without lifting the finger, so that the circle would start rotating to the other direction.
This is my OnTouchListener class:
open class OnSwipeTouchListenerV2() : View.OnTouchListener {
private val SWIPE_THRESHOLD = 0f
private var initialX = 0f
private var initialY = 0f
private var previousX = 0f
private var previousY = 0f
private var currentX = 0f
private var currentY = 0f
private var diffX = 0f
private var diffY = 0f
private var swipeH = "0" // Horizontal swipe direction (LEFT or RIGHT)
override fun onTouch(v: View, event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
initialX = event.x
initialY = event.x
return true
}
MotionEvent.ACTION_MOVE -> {
currentX = event.x
currentY = event.x
// These where original diff calculations
// The issue with this is that diffs get bigger when swiping further
// It is an issue because I'm trying to use the diff as an angle to rotate by
// diffX = currentX - initialX // Original
// diffY = currentY - initialY // Original
diffX = currentX - previousX // My Implementation
diffY = currentY - previousY // My Implementation
when (swipeH) {
"LEFT" -> {
if (currentX > previousX) {
swipeH = "RIGHT"
initialX = previousX
diffX = currentX - initialX
} else {
// Intentionally kept empty
}
}
"RIGHT" -> {
if (currentX < previousX) {
swipeH = "LEFT"
initialX = previousX
diffX = currentX - initialX
} else {
// Intentionally kept empty
}
}
else -> {
if (currentX < initialX) {
swipeH = "LEFT"
} else if (currentX > initialX) {
swipeH = "RIGHT"
} else {
// Intentionally kept empty
}
}
}
previousX = currentX
previousY = currentY
if (abs(diffX) > abs(diffY)) {
if (abs(diffX) > SWIPE_THRESHOLD) {
if (diffX > 0) {
onSwipeRight(diffX)
} else {
onSwipeLeft(diffX)
}
}
} else {
if (abs(diffY) > SWIPE_THRESHOLD) {
if (diffY > 0) {
onSwipeBottom(diffY)
} else {
onSwipeTop(diffY)
}
}
}
return true
}
MotionEvent.ACTION_UP -> {
swipeH = "0"
initialX = 0f
initialY = 0f
previousX = 0f
previousY = 0f
diffX = 0f
diffY = 0f
return true
}
else -> {
return false
}
}
}
open fun onSwipeRight(diffX: Float) {}
open fun onSwipeLeft(diffX: Float) {}
open fun onSwipeTop(diffY: Float) {}
open fun onSwipeBottom(diffY: Float) {}
}
This is my MainActivity file:
class MainActivityBackup : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val wheel = findViewById<ImageView>(R.id.imageView1) as ImageView
window.decorView.setOnTouchListener(object: OnSwipeTouchListenerV2() {
override fun onSwipeLeft(diffX: Float) {
rotateImage(wheel, diffX)
}
override fun onSwipeRight(diffX: Float) {
rotateImage(wheel, diffX)
}
})
}
fun rotateImage(imageView: ImageView, angle: Float) {
imageView.animate().rotation(angle).start()
}
}
It kind of works, but not really... On emulator, if I swipe just a bit, then it works fine, but if I do a continuous swipe slowly, then the circle moves very little during the swipe and makes a normal rotation once the swipe is finished. Same happens during a fast longer swipe: the circle does not move during the swipe but rotates (more than during the slow swipe because I'm using diffX as an angle of how much to rotate) once the swipe is finished.
I've tried a different function for the actual rotation found here. I've tried different interpolators, that did not help.
Maybe I should not be using diffX for rotation? But from Log messages (to get the value of diffX during each ACTION_MOVE) I saw that during a continuous swipe the circle definitely does not rotate as much as it is supposed to.
My guess is that this happens because during a continuous swipe the are a lot of MotionEvent.ACTION_MOVE actions and each of them makes a call to onSwipeLeft or onSwipeRight functions, but they do not finish for some reason because a new MotionEvent.ACTION_MOVE action arrives, right?
So, any ideas how to smoothly rotate a circle?
Solved
Figured it out myself... I guess it was because I was using animation and when there are many ACTION_MOVE events happening, then animation just cannot keep up. This is the change that I've made (not exactly this, but it's the main idea why it was not working properly):
fun rotateImage(imageView: ImageView, angle: Float) {
// old code
imageView.animate().rotation(angle).start()
// new code
imageView.rotation = angle
}
I want to rotate my view by dragging a helper button on the edge of the view.
Here Image Here is video
In this case, when the view is standing still, only the change on event.Y should be affected. As the view becomes horizontal, the event.X change should increase and only the event.X change should affect the view when the view is fully horizontal
Can anyone know how to do this?
current code is, but this worked incorrectly:
emojiBinding.btnRotate.setOnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
boolArray[0] = false
boolArray[2] = false
startPositionOfRotation.x = event.x
startPositionOfRotation.y = event.y
}
MotionEvent.ACTION_MOVE -> {
val degree = getRotationDegree(emojiBinding, event.x, event.y)
val rotState = emojiBinding.root.rotation
var mult = 0f
if (abs(rotState)%90 < 45 ){
mult = if (event.x >=0)1f else -1f
}
else{
mult = if (event.y >=0)1f else -1f
}
emojiBinding.root.rotation += mult * degree
Timber.d("Degree $degree Rotation ${emojiBinding.root.rotation}")
}
MotionEvent.ACTION_UP -> {
startPositionOfRotation.x = 0f
startPositionOfRotation.y = 0f
boolArray[0] = true
boolArray[2] = true
}
}
boolArray[1]
}
I am using "Draw over other aps" to show a chat head like view.
I need to position it relatively and show animation while dismissing it.
Please see first video/gif of google:
Please note:
a) In first video: the view can be position relative to screen blocks designed by system.
b) The movement is smooth.
c) While dismissing/closing view an animation cross is displayed.
Please see second video/gif of my app:
The movement is not that smooth, its plain and no animation.
Below is my code:
floatingView.setOnTouchListener(object : View.OnTouchListener {
private var initialX = 0
private var initialY = 0
private var initialTouchX = 0f
private var initialTouchY = 0f
override fun onTouch(v: View, event: MotionEvent): Boolean {
when (event.action) {
ACTION_UP -> {
overlayView.visibility = INVISIBLE
if (v.isOverlap(overlayView)) {
stopSelf()
}
return false
}
ACTION_MOVE -> {
overlayView.visibility = View.VISIBLE
layoutParams.x = (initialX + (event.rawX - initialTouchX).toInt())
layoutParams.y = (initialY + (event.rawY - initialTouchY).toInt())
windowManager.updateViewLayout(floatingView, layoutParams)
return false
}
ACTION_DOWN -> {
initialX = layoutParams.x
initialY = layoutParams.y
initialTouchX = event.rawX
initialTouchY = event.rawY
return false
}
}
return false
}
}
How should I tweak it to achieve the first video ?
I am using WindowManager.LayoutParams
var floatingView = LayoutInflater.from(this).inflate(R.layout.chat, null)
var windowManager: WindowManager getSystemService(WINDOW_SERVICE) as WindowManager
var layoutParams: WindowManager.LayoutParams = WindowManagerLayoutParams(WRAP_CONTENT, WRAP_CONTENT, LAYOUT_FLAG, FLAG_NOT_FOCUSABLE, TRANSLUCENT).apply {
gravity = TOP or START
x = 0
y = 100
}
windowManager.addView(floatingView, layoutParams)
Please try below code and check if it's not working then tell me.
#SuppressLint("ClickableViewAccessibility")
private fun onTouchListener()
: View.OnTouchListener {
return View.OnTouchListener { view, event ->
val x = event.rawX.toInt()
val y = event.rawY.toInt()
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
val lParams = view.layoutParams as RelativeLayout.LayoutParams
xDelta = x - lParams.leftMargin
yDelta = y - lParams.topMargin
}
MotionEvent.ACTION_UP -> {
}
MotionEvent.ACTION_MOVE -> {
val layoutParams = view
.layoutParams as RelativeLayout.LayoutParams
layoutParams.leftMargin = x - xDelta
layoutParams.topMargin = y - yDelta
layoutParams.rightMargin = 0
layoutParams.bottomMargin = 0
view.layoutParams = layoutParams
}
}
your_view.invalidate()
true
}
}
What I am trying to do is the following:
I have a fragment that has scrollview with a video on top. What I am a trying to achieve is having the video to float when I scroll.
Similar behavior to this link: https://www.independentarabia.com/jsonfeed/api/v2/node/34291
I looked into picture in picture mode but with no luck
Can anyone give me an idea of how this behavior can be achieved?
In case someone was looking to do something like this:
I referred to the jw player library:
scroll.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { v, _, scrollY, _, oldScrollY ->
x = 0
if (scrollY > 200) {
if (!mPlayerContainer.isMovable) {
movable = false
x = 2
} else {
x = 0
val momentView = v.getChildAt(v.childCount - 1)
val diff = (momentView.bottom - (scroll.height + scroll
.scrollY))
if (diff < 50) {
val layoutParamsNew =
RelativeLayout.LayoutParams(mPlayerContainer.width, mPlayerContainer.height)
layoutParamsNew.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
layoutParamsNew.addRule(RelativeLayout.ALIGN_PARENT_RIGHT)
val displayMetrics = resources.displayMetrics
layoutParamsNew.setMargins(
0,
0,
(displayMetrics.density * 16).roundToInt(),
(displayMetrics.density * 16).roundToInt()
)
mPlayerContainer.layoutParams = layoutParamsNew
} else {
val layoutParamsNew =
RelativeLayout.LayoutParams(mPlayerContainer.width, mPlayerContainer.height)
layoutParamsNew.addRule(RelativeLayout.CENTER_VERTICAL)
layoutParamsNew.addRule(RelativeLayout.ALIGN_PARENT_RIGHT)
val displayMetrics = resources.displayMetrics
layoutParamsNew.setMargins(
0,
0,
(displayMetrics.density * 16).roundToInt(),
(displayMetrics.density * 16).roundToInt()
)
mPlayerContainer.layoutParams = layoutParamsNew
}
}
} else if (scrollY < 200 && mPlayerContainer.isMovable) {
movable = true
x = 1
}
if (x != 0) {
toggleMovablePlayer()
}
})
private fun toggleMovablePlayer() {
if (!movable) {
// Set the player container to movable, in order to intercept touch events.
mPlayerContainer.isMovable = true
// Disable fullscreen rotation handling on the JW Player.
mPlayerView!!.setFullscreen(mPlayerView!!.fullscreen, false)
// Disable controls.
mPlayerView!!.controls = false
if (mPlayerState != PlayerState.PLAYING && mPlayerState != PlayerState.BUFFERING) {
// Start playback in case the user hasn't done this yet, since we don't want to have
// a movable player that does not play anything...
mPlayerView!!.play()
}
// Scale the player.
mInitialLayoutParams = mPlayerContainer.layoutParams
val newWidth = (mPlayerContainer.width / SCALING_FACTOR)
val newHeight = (mPlayerContainer.height / SCALING_FACTOR)
val layoutParams = RelativeLayout.LayoutParams(newWidth.toInt(), newHeight.toInt())
// Position the player in the right bottom corner.
mPlayerContainer.layoutParams = getInitialMovablePlayerLayoutParams(layoutParams)
// Set an onTouchListener on the player which handles MotionEvents.
mPlayerContainer.setOnTouchListener(View.OnTouchListener { v, event ->
if (v.id == R.id.player_container) {
val layoutParams = v.layoutParams as RelativeLayout.LayoutParams
when (event.action) {
MotionEvent.ACTION_DOWN ->
// Notify the MovablePlayerLayout that we started consuming
// events in order to receive ACTION_MOVE events.
return#OnTouchListener true
MotionEvent.ACTION_MOVE -> {
var topMargin = event.rawY.toInt() - v.height
var leftMargin = event.rawX.toInt() - v.width / 2
// Make sure that the view can not go "out of bounds"
if (topMargin < 0) {
// Out of bounds: TOP
topMargin = 0
}
if (topMargin > mContentContainer.height - mPlayerContainer.height) {
// Out of bounds: BOTTOM
topMargin = mContentContainer.height - mPlayerContainer.height
}
if (leftMargin < 0) {
// Out of bounds: LEFT
leftMargin = 0
}
if (leftMargin > mContentContainer.width - mPlayerContainer.width) {
// Out of bounds: RIGHT
leftMargin = mContentContainer.width - mPlayerContainer.width
}
layoutParams.topMargin = topMargin
layoutParams.leftMargin = leftMargin
// Make sure the align rules have been removed.
layoutParams.removeRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
layoutParams.removeRule(RelativeLayout.CENTER_VERTICAL)
layoutParams.removeRule(RelativeLayout.ALIGN_PARENT_RIGHT)
layoutParams.rightMargin = 0
layoutParams.bottomMargin = 0
// Set the new layout parameters
v.layoutParams = layoutParams
return#OnTouchListener true
}
}
}
false
})
} else {
// Disable the movable property of the MovableViewLayout.
mPlayerContainer.isMovable = false
// Restore the initial layout parameters.
mPlayerContainer.layoutParams = mInitialLayoutParams
// Remove the onTouchListener.
mPlayerContainer.setOnTouchListener(null)
// Re-enable the controls.
mPlayerView!!.controls = true
// Re-enable fullscreen rotation handling, and go to fullscreen if we're in landscape mode.
mPlayerView!!.setFullscreen(
resources.configuration.orientation === Configuration.ORIENTATION_LANDSCAPE,
true
)
}
}
private fun setInitialLayoutParams() {
val displayMetrics = resources.displayMetrics
if (resources.configuration.orientation === Configuration.ORIENTATION_PORTRAIT) {
/*mPlayerContainer.layoutParams = RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, displayMetrics.widthPixels / 16 * 9
) // 16:9*/
mPlayerContainer.layoutParams = RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
200f,
resources.displayMetrics
).toInt()
)
} else {
// We need to use height to calculate a 16:9 ratio since we're in landscape mode.
mPlayerContainer.layoutParams = RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, displayMetrics.heightPixels / 16 * 9
) // 16:9
// Toggle fullscreen, since we're in landscape mode.
mPlayerView!!.setFullscreen(true, true)
}
}
/**
* Positions the movable player to the right bottom corner.
*
* #param layoutParams
* #return
*/
private fun getInitialMovablePlayerLayoutParams(layoutParams: RelativeLayout.LayoutParams): RelativeLayout.LayoutParams {
layoutParams.addRule(RelativeLayout.CENTER_VERTICAL)
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT)
val displayMetrics = resources.displayMetrics
layoutParams.setMargins(0, 0, Math.round(displayMetrics.density * 16), Math.round(displayMetrics.density * 16))
return layoutParams
}
I want my app to simulate a swipe touch event (to up/down/left/right) when I click a button, then a TextView will scroll down/up.
I have tried to use Motion Event, but nothing happen after I dispatch 3 Motion Event of ACTION_DOWN, ACTION_MOVE and ACTION_UP respectively.
Is that possible to simulate a swipe event?
public void simulation(View view){
swipe(Direction.Bot);
}
public enum Direction {
Top, Bot, Left, Right;
}
protected void swipe(Direction direction) {
Point size = new Point();
this.getWindowManager().getDefaultDisplay().getSize(size);
int height = size.y; // height will be at top of the screen
int width = size.x; // width will be rightmost location of the screen
float xStart = size.x-50;
float xEnd = size.x-50;
float yStart = size.y-50;
float yEnd = size.y-50;
long downTime = SystemClock.uptimeMillis();
if(direction == Direction.Top || direction == Direction.Bot){
yStart = ((direction == Direction.Top) ? 50 : (height - 50));
yEnd = ((direction == Direction.Top) ? (height - 50) : 50);
}else {
xStart = ((direction == Direction.Left) ? (width - 50) : 50); // true: xStart = w-10; else: = 10
xEnd = ((direction == Direction.Left) ? 50 : (width - 50)); // true: xEnd = 10; else: = w-10
}
findViewById(R.id.my_id).dispatchTouchEvent(MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),MotionEvent.ACTION_DOWN, xStart/2, yStart/2, 0));
System.out.println("ACTION_DOWN");
findViewById(R.id.my_id).dispatchTouchEvent(MotionEvent.obtain(downTime, SystemClock.uptimeMillis() + 500, MotionEvent.ACTION_MOVE, xEnd / 2, yEnd / 2, 0));
System.out.println("ACTION_MOVE");
findViewById(R.id.my_id).dispatchTouchEvent(MotionEvent.obtain(downTime, SystemClock.uptimeMillis() + 1000, MotionEvent.ACTION_UP, xEnd / 2, yEnd / 2, 0));
System.out.println("ACTION_UP");
}
I was able to programmatically emulate fling event in scrolling activity demo.
This is an example of an emulating fling event I was trying and it worked.
Blue dotted line is the fling event I have emulated:
class ScrollingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scrolling)
setSupportActionBar(toolbar)
fab.setOnClickListener { view ->
Thread(Runnable {
try {
fling(500f ,900f ,530f ,20f, 5);
// emulateMptionEvent()
} catch (e: Exception) {
}
}).start()
}
}
/** * Simulate touching a specific location and dragging to a new location.
*
* #param fromX X coordinate of the initial touch, in screen coordinates
* #param toX Xcoordinate of the drag destination, in screen coordinates
* #param fromY X coordinate of the initial touch, in screen coordinates
* #param toY Y coordinate of the drag destination, in screen coordinates
* #param stepCount How many move steps to include in the drag
*/
fun fling(
fromX: Float, toX: Float, fromY: Float,
toY: Float, stepCount: Int
) {
val inst = Instrumentation()
val downTime = SystemClock.uptimeMillis()
var eventTime = SystemClock.uptimeMillis()
var y = fromY
var x = fromX
val yStep = (toY - fromY) / stepCount
val xStep = (toX - fromX) / stepCount
var event = MotionEvent.obtain(
downTime, eventTime,
MotionEvent.ACTION_DOWN, fromX, fromY, 0
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
event.source = InputDevice.SOURCE_TOUCHSCREEN
}
inst.sendPointerSync(event)
for (i in 0 until stepCount) {
y += yStep
x += xStep
eventTime = SystemClock.uptimeMillis()
event = MotionEvent.obtain(
downTime, eventTime + stepCount,
MotionEvent.ACTION_MOVE, x, y, 0
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
event.source = InputDevice.SOURCE_TOUCHSCREEN
}
inst.sendPointerSync(event)
}
eventTime = SystemClock.uptimeMillis() + stepCount.toLong() + 2
event = MotionEvent.obtain(
downTime, eventTime,
MotionEvent.ACTION_UP, toX, toY, 0
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
event.source = InputDevice.SOURCE_TOUCHSCREEN
}
inst.sendPointerSync(event)
}
}
Hope it help somebody
I have wrote extension functions that doesn't require Instrumentation, so they could be used not only in androidTest but also in robolectric tests and even in release build:
fun ViewGroup.performSwipeToLeft(target: View) {
this.performSwipe(target, distanceX = -this.width * .5f, distanceY = 0f)
}
fun ViewGroup.performSwipeToRight(target: View) {
this.performSwipe(target, distanceX = +this.width * .5f, distanceY = 0f)
}
fun ViewGroup.performSwipeToTop(target: View) {
this.performSwipe(target, distanceX = 0f, distanceY = -this.height * .5f)
}
fun ViewGroup.performSwipeToBottom(target: View) {
this.performSwipe(target, distanceX = 0f, distanceY = +this.width * .5f)
}
fun ViewGroup.performSwipe(target: View, distanceX: Float, distanceY: Float) {
val parentCoords = intArrayOf(0, 0)
this.getLocationInWindow(parentCoords)
val childCoords = intArrayOf(0, 0)
target.getLocationInWindow(childCoords)
val initGlobalX = childCoords[0].toFloat() + 1f
val initGlobalY = childCoords[1].toFloat() + 1f
val initLocalX = (childCoords[0] - parentCoords[0]).toFloat() + 1f
val initLocalY = (childCoords[1] - parentCoords[1]).toFloat() + 1f
val downTime = SystemClock.uptimeMillis()
var eventTime = SystemClock.uptimeMillis()
this.dispatchTouchEvent(
MotionEvent.obtain(
downTime,
eventTime,
MotionEvent.ACTION_DOWN,
initGlobalX,
initGlobalY,
0
).apply {
setLocation(initLocalX, initLocalY)
source = InputDevice.SOURCE_TOUCHSCREEN
}
)
val steps = 20
var i = 0
while (i in 0..steps) {
val globalX = initGlobalX + i * distanceX / steps
val globalY = initGlobalY + i * distanceY / steps
val localX = initLocalX + i * distanceX / steps
val localY = initLocalY + i * distanceY / steps
if (globalX <= 10f || globalY <= 10f) {
break
}
this.dispatchTouchEvent(
MotionEvent.obtain(
downTime,
++eventTime,
MotionEvent.ACTION_MOVE,
globalX,
globalY,
0
).apply {
setLocation(localX, localY)
source = InputDevice.SOURCE_TOUCHSCREEN
}
)
i++
}
this.dispatchTouchEvent(
MotionEvent.obtain(
downTime,
++eventTime,
MotionEvent.ACTION_UP,
initGlobalX + i * distanceX,
initGlobalY + i * distanceY,
0
).apply {
setLocation(initLocalX + i * distanceX, initLocalY + i * distanceY)
source = InputDevice.SOURCE_TOUCHSCREEN
}
)
}
To use it you need pass your textView as an argument, and parent of textView should be receiver of this functions:
val textView = ...
(textView.parent as ViewGroup).performSwipeToTop(textView)
With RecyclerView you also wanna take a look at the smoothScollBy function where you can pass an Interpolator like the following.
recyclerView.smoothScrollBy(0, 500, AccelerateDecelerateInterpolator())
Comes close enough to a "swipe scoll"