Espresso with Custom KeyboardView button press - android

I am implementing a custom KeyboardView in my app and it's all working at the moment, however, when I attempt to press a key on the keyboard using Espresso ViewAction, I am getting an exception saying:
android.support.test.espresso.PerformException:
Error performing 'single click - At Coordinates: 1070, 2809 and
precision: 16, 16' on view 'with id:
com.example.app.mvpdemo:id/keyboardLayout'.
The code throwing the exception is:
#Test
fun enter100AsPriceShouldDisplay120ForA20PercentTip(){
onView(withId(R.id.editTextCheckAmount))
.perform(typeText("100"), closeSoftKeyboard())
val appContext = InstrumentationRegistry.getTargetContext()
val displayMetrics = appContext.resources.displayMetrics
onView(withId(R.id.keyboardLayout)).perform(clickXY(displayMetrics.widthPixels - 10, displayMetrics.heightPixels - 10))
onView(withText("$120.00")).check(matches(isDisplayed()))
}
and the click XY function which came from this post
private fun clickXY(x: Int, y: Int): ViewAction {
return GeneralClickAction(
Tap.SINGLE,
CoordinatesProvider { view ->
val screenPos = IntArray(2)
view.getLocationOnScreen(screenPos)
val screenX = (screenPos[0] + x).toFloat()
val screenY = (screenPos[1] + y).toFloat()
floatArrayOf(screenX, screenY)
},
Press.FINGER, 0, 0)
}
Here is my keyboard layout (pinned to the bottom of the screen inside a ConstraintLayout):
Does anyone know why? Any help is appreciated.

Answering my own question after determining a flexible solution:
First attempt - get DisplayMetrics of the root View and subtract an arbitrary number to attempt to hit the Keyboard.Key
this didn't work because clickXY function uses the position of the view
this ended up being the reason for the exception since the view is smaller than the DisplayMetrics values and adding to the Views on screen position would give a very high number for the x and y.
So I tried again,
Second attempt - use check method on the ViewMatcher to check the KeyBoardView.
by doing so I was able to get access to the KeyboardView's position x
then I was able to get the KeyboardView's width and height
by performing some math, I was able to figure out target index for x & y
the math:
take the widthPercent for the Keyboard.Key (in my case 33.3%)
take the rowCount of the keyboard.xml (in my case 3)
use (viewWidth * widthPercent) / 4 to get relativeButtonX
use (viewHeight / rowCount) / 2 to get relativeButtonY
then for targetY, I took viewHeight - relativeButtonY
finally, for targetX, I took (viewPosX + viewWidth) - relativeButtonX
So enough explanation, here is the code:
#Test
fun enter100AsPriceShouldDisplay120ForA20PercentTip() {
onView(withId(R.id.editTextCheckAmount))
.perform(typeText("100"), closeSoftKeyboard())
// call the function to get the targets
val (viewTargetY, viewTargetX) = getTargetXAndY()
// perform the action
onView(withId(R.id.keyboardLayout)).perform(clickXY(viewTargetX.toInt(), viewTargetY))
onView(withText("Tip: $20.00")).check(matches(isDisplayed()))
onView(withText("Total: $120.00")).check(matches(isDisplayed()))
}
and the helper method with all the math:
private fun getTargetXAndY(): Pair<Int, Double> {
var viewHeight = 0
var viewWidth = 0
var viewPosX = 0F
val viewMatcher = onView(withId(R.id.keyboardLayout))
viewMatcher.check { view, _ ->
viewWidth = view.width
viewHeight = view.height
viewPosX = view.x
}
val keyboardKeyWidthPercent = 0.333
val keyboardRowsCount = 3
val keyboardButtonWidthQuarter = (viewWidth * keyboardKeyWidthPercent) / 4
val keyboardButtonHeightHalf = (viewHeight / keyboardRowsCount) / 2
val viewTargetY = viewHeight - keyboardButtonHeightHalf
val viewTargetX = (viewPosX + viewWidth) - keyboardButtonWidthQuarter
return Pair(viewTargetY, viewTargetX)
}
Now, the click is not perfectly centered but it clicks the button pretty close to the center.

Related

RecyclerView Fit 5 items with different height and width using custom layout manager

First of all let me show you an image what i am trying achieve exactly :
Now as of above gif i need to build it with recyclerview.
Fit only 5 items at a time on screen.
Center and other 4 items will be scaled as shown in image.
I have tried with custom layout manager like below:
private val shrinkAmount = 0.3f
private val shrinkDistance = 1f
override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler?, state: RecyclerView.State?): Int {
val orientation = orientation
if (orientation == VERTICAL) {
val scrolled = super.scrollVerticallyBy(dy, recycler, state)
if (isScaleView()) {
val midpoint = height / 2f
val d0 = 0f
val d1 = shrinkDistance * midpoint
val s0 = 1f
val s1 = 1f - shrinkAmount
for (i in 0 until childCount) {
val child = getChildAt(i)
val childMidpoint = (getDecoratedBottom(child!!) + getDecoratedTop(child)) / 2f
val d = Math.min(d1, Math.abs(midpoint - childMidpoint))
val scale = s0 + (s1 - s0) * (d - d0) / (d1 - d0)
child.scaleX = scale
child.scaleY = scale
}
}
return scrolled
} else {
return 0
}
}
But i am getting output as follows:
How can i achive exactly like above gif ?

Implement animation without breaking layout constraints

I need to build my own animation library/helper, that could move a view from a point A to point B just by updating the animation's progression percentage. This has to be written in Kotlin, but if needed I can translate it in java.
What I've done
class Animator(
private val animations: List<Animation>
) {
constructor(view: View,
startPoint: Point2D = Point2D(view.x, view.y),
endPoint: Point2D) : this(listOf(Animation(view, startPoint, endPoint)))
private val transitionState: TransitionState = TransitionState(0)
private val animationQueue: Handler = Handler()
/**
* Apply a progression in the animation from [startPoint] to [endPoint] or conversely depending
* on the transition state.
* When the [TransitionState.progress] reach 0 the position of the [view] to animate is at [startPoint]. When it
* reach 100 the view will be at the [endPoint]
*
* #param percent an Integer that must be between 0 and 100 because it's percentage. If the
* given percent is below 0 it
* will override it to 0. Same process when it's over 100.
*/
fun progressTo(percent: Int) {
for (anim in animations) {
val finalPercent = if (percent > 100) 100 else if (percent < 0) 0 else percent
if (Math.abs(transitionState.progress - finalPercent) < 10) {
animationQueue.post {
anim.view.x = (Vector2D(anim.startPoint, anim.endPoint) % finalPercent).endPoint.x
anim.view.y = (Vector2D(anim.startPoint, anim.endPoint) % finalPercent).endPoint.y
}
} else {
anim.view.animate().x((Vector2D(anim.startPoint, anim.endPoint) % finalPercent).endPoint.x)
anim.view.animate().y((Vector2D(anim.startPoint, anim.endPoint) % finalPercent).endPoint.y)
}
transitionState.progress = finalPercent
}
}
/**
* Finish the animation to the endPoint or startPoint depending on the [TransitionState.progress]
*/
fun finishAnimation() {
if (transitionState.progress < 50) {
progressTo(0)
} else if (transitionState.progress >= 50) {
progressTo(100)
}
}
}
data class TransitionState(
var progress: Int,
var isOnTransaction: Boolean = progress != 0 && progress != 100
)
data class Vector2D(
val startPoint: Point2D,
val endPoint: Point2D
) {
operator fun rem(percent: Int): Vector2D {
val finalPercent = if (percent > 100) 100 else if (percent < 0) 0 else percent
return Vector2D(startPoint, Point2D(
startPoint.x + ((endPoint.x - startPoint.x) * finalPercent / 100),
startPoint.y + ((endPoint.y - startPoint.y) * finalPercent / 100)
))
}
}
data class Animation(
val view: View,
val startPoint: Point2D = Point2D(view.x, view.y),
val endPoint: Point2D
)
data class Point2D(
val x: Float,
val y: Float
)
As you can see i'm using the property View.setX and View.setY to move the view. It works great it move the view properly with the correct animation and movement.
The problem
When I use the X and Y values, the constraints (LinearLayout, RelativeLayout or ConstraintLayout) of the view's layout parent are not updated or respected. For instance there is a view (the red one in the first capture) in a constraint layout which is constrained to parent left, right and bottom as shown in this layout editor capture.
When I change the y value of the view the bottom constraint is not updated (it does not fit anymore the parent bottom). Here is the captures of the real application before animation and after animation.
Using the View.layout() or View.invalidate() does not change anything, and if there is a method that could redraw and restore the whole layout, I'm not sure that the application will be performant anymore ....
EDIT
I corrected a piece of code (in the for loop) that enables the anmiation to be more fluent.
The question
Is it possible to change the position of a view without breaking its constraints ?
I'm pretty sure I'm misunderstanding something in the android ui positioning/drawing, so feel free to post your understanding of this even you can't answer to my question. Thank you by advance.

How to fit content of GIF animation in View and in live wallpaper?

Background
I have a small live wallpaper app, that I want to add support for it to show GIF animations.
For this, I've found various solutions. There is the solution of showing a GIF animation in a view (here), and there is even a solution for showing it in a live wallpaper (here).
However, for both of them, I can't find how to fit the content of the GIF animation nicely in the space it has, meaning any of the following:
center-crop - fits to 100% of the container (the screen in this case), cropping on sides (top&bottom or left&right) when needed. Doesn't stretch anything. This means the content seems fine, but not all of it might be shown.
fit-center - stretch to fit width/height
center-inside - set as original size, centered, and stretch to fit width/height only if too large.
The problem
None of those is actually about ImageView, so I can't just use the scaleType attribute.
What I've found
There is a solution that gives you a GifDrawable (here), which you can use in ImageView, but it seems it's pretty slow in some cases, and I can't figure out how to use it in LiveWallpaper and then fit it.
The main code of the LiveWallpaper GIF handling is as such (here) :
class GIFWallpaperService : WallpaperService() {
override fun onCreateEngine(): WallpaperService.Engine {
val movie = Movie.decodeStream(resources.openRawResource(R.raw.cinemagraphs))
return GIFWallpaperEngine(movie)
}
private inner class GIFWallpaperEngine(private val movie: Movie) : WallpaperService.Engine() {
private val frameDuration = 20
private var holder: SurfaceHolder? = null
private var visible: Boolean = false
private val handler: Handler = Handler()
private val drawGIF = Runnable { draw() }
private fun draw() {
if (visible) {
val canvas = holder!!.lockCanvas()
canvas.save()
movie.draw(canvas, 0f, 0f)
canvas.restore()
holder!!.unlockCanvasAndPost(canvas)
movie.setTime((System.currentTimeMillis() % movie.duration()).toInt())
handler.removeCallbacks(drawGIF)
handler.postDelayed(drawGIF, frameDuration.toLong())
}
}
override fun onVisibilityChanged(visible: Boolean) {
this.visible = visible
if (visible)
handler.post(drawGIF)
else
handler.removeCallbacks(drawGIF)
}
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacks(drawGIF)
}
override fun onCreate(surfaceHolder: SurfaceHolder) {
super.onCreate(surfaceHolder)
this.holder = surfaceHolder
}
}
}
The main code for handling GIF animation in a view is as such:
class CustomGifView #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
private var gifMovie: Movie? = null
var movieWidth: Int = 0
var movieHeight: Int = 0
var movieDuration: Long = 0
var mMovieStart: Long = 0
init {
isFocusable = true
val gifInputStream = context.resources.openRawResource(R.raw.test)
gifMovie = Movie.decodeStream(gifInputStream)
movieWidth = gifMovie!!.width()
movieHeight = gifMovie!!.height()
movieDuration = gifMovie!!.duration().toLong()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(movieWidth, movieHeight)
}
override fun onDraw(canvas: Canvas) {
val now = android.os.SystemClock.uptimeMillis()
if (mMovieStart == 0L) { // first time
mMovieStart = now
}
if (gifMovie != null) {
var dur = gifMovie!!.duration()
if (dur == 0) {
dur = 1000
}
val relTime = ((now - mMovieStart) % dur).toInt()
gifMovie!!.setTime(relTime)
gifMovie!!.draw(canvas, 0f, 0f)
invalidate()
}
}
}
The questions
Given a GIF animation, how can I scale it in each of the above ways?
Is it possible to have a single solution for both cases?
Is it possible to use GifDrawable library (or any other drawable for the matter) for the live wallpaper, instead of the Movie class? If so, how?
EDIT: after finding how to scale for 2 kinds, I still need to know how to scale according to the third type, and also want to know why it keeps crashing after orientation changes, and why it doesn't always show the preview right away.
I'd also like to know what's the best way to show the GIF animation here, because currently I just refresh the canvas ~60fps (1000/60 waiting between each 2 frames), without consideration of what's in the file.
Project is available here.
If you have Glide in your project, You can easily load Gifs, as it provides drawing GIFs to your ImageViews and does support many scaling options (like center or a given width and ...).
Glide.with(context)
.load(imageUrl or resourceId)
.asGif()
.fitCenter() //or other scaling options as you like
.into(imageView);
OK I think I got how to scale the content. Not sure though why the app still crashes upon orientation change sometimes, and why the app doesn't show the preview right away sometimes.
Project is available here.
For center-inside, the code is:
private fun draw() {
if (!isVisible)
return
val canvas = holder!!.lockCanvas() ?: return
canvas.save()
//center-inside
val scale = Math.min(canvas.width.toFloat() / movie.width().toFloat(), canvas.height.toFloat() / movie.height().toFloat());
val x = (canvas.width.toFloat() / 2f) - (movie.width().toFloat() / 2f) * scale;
val y = (canvas.height.toFloat() / 2f) - (movie.height().toFloat() / 2f) * scale;
canvas.translate(x, y)
canvas.scale(scale, scale)
movie.draw(canvas, 0f, 0f)
canvas.restore()
holder!!.unlockCanvasAndPost(canvas)
movie.setTime((System.currentTimeMillis() % movie.duration()).toInt())
handler.removeCallbacks(drawGIF)
handler.postDelayed(drawGIF, frameDuration.toLong())
}
For center-crop, the code is:
private fun draw() {
if (!isVisible)
return
val canvas = holder!!.lockCanvas() ?: return
canvas.save()
//center crop
val scale = Math.max(canvas.width.toFloat() / movie.width().toFloat(), canvas.height.toFloat() / movie.height().toFloat());
val x = (canvas.width.toFloat() / 2f) - (movie.width().toFloat() / 2f) * scale;
val y = (canvas.height.toFloat() / 2f) - (movie.height().toFloat() / 2f) * scale;
canvas.translate(x, y)
canvas.scale(scale, scale)
movie.draw(canvas, 0f, 0f)
canvas.restore()
holder!!.unlockCanvasAndPost(canvas)
movie.setTime((System.currentTimeMillis() % movie.duration()).toInt())
handler.removeCallbacks(drawGIF)
handler.postDelayed(drawGIF, frameDuration.toLong())
}
for fit-center, I can use this:
val canvasWidth = canvas.width.toFloat()
val canvasHeight = canvas.height.toFloat()
val bitmapWidth = curBitmap.width.toFloat()
val bitmapHeight = curBitmap.height.toFloat()
val scaleX = canvasWidth / bitmapWidth
val scaleY = canvasHeight / bitmapHeight
scale = if (scaleX * curBitmap.height > canvas.height) scaleY else scaleX
x = (canvasWidth / 2f) - (bitmapWidth / 2f) * scale
y = (canvasHeight / 2f) - (bitmapHeight / 2f) * scale
...
Change the width and the height of the movie:
Add this code in onDraw method before movie.draw
canvas.scale((float)this.getWidth() / (float)movie.width(),(float)this.getHeight() / (float)movie.height());
or
canvas.scale(1.9f, 1.21f); //this changes according to screen size
Scale to fill and scale to fit:
There's already a good answer on that:
https://stackoverflow.com/a/38898699/5675325

Custom layout manager scroll/animation

What I'm trying to do.
Create a simple carousel with RecyclerView.
Problem
Initially the view is not snap to center and the view is not getting the style I intended to.(i.e, the item which is fully visible should be bigger than other, when scroll by finger it works fine)
When scroll programmatically the view is not getting snap effect like it does when scroll with finger.
See the attached gif below for example.
Question
How to have the style as intended (i.e the fully visible item is bigger) when started.
How to get the style when scroll to button is click. (It scrolls to correct position the only problem is not getting the style as intended and its not snap to center)
Full code here on github
Here's the code for custom LayoutManager
open class CarouselLayoutManager(
context: Context,
orientation: Int,
reverseLayout: Boolean
) : LinearLayoutManager(context, orientation, reverseLayout) {
private val mShrinkAmount = 0.15f
private val mShrinkDistance = 0.9f
override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {
scrollVerticallyBy(0, recycler, state)
super.onLayoutChildren(recycler, state)
}
override fun scrollHorizontallyBy(dx: Int, recycler: RecyclerView.Recycler?, state: RecyclerView.State?): Int {
val orientation = orientation
if (orientation == LinearLayoutManager.HORIZONTAL) {
val scrolled = super.scrollHorizontallyBy(dx, recycler, state)
val midpoint = width / 2f
val d0 = 0f
val d1 = mShrinkDistance * midpoint
val s0 = 1f
val s1 = 1f - mShrinkAmount
for (i in 0 until childCount) {
val child = getChildAt(i)
val childMidpoint = (getDecoratedRight(child) + getDecoratedLeft(child)) / 2f
val d = Math.min(d1, Math.abs(midpoint - childMidpoint))
val scale = s0 + (s1 - s0) * (d - d0) / (d1 - d0)
child.scaleX = scale
child.scaleY = scale
}
return scrolled
} else {
return 0
}
}
override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler?, state: RecyclerView.State?): Int {
val orientation = orientation
if (orientation == LinearLayoutManager.VERTICAL) {
val scrolled = super.scrollVerticallyBy(dy, recycler, state)
val midpoint = height / 2f
val d0 = 0f
val d1 = mShrinkDistance * midpoint
val s0 = 1f
val s1 = 1f - mShrinkAmount
for (i in 0 until childCount) {
val child = getChildAt(i)
val childMidpoint = (getDecoratedBottom(child) + getDecoratedTop(child)) / 2f
val d = Math.min(d1, Math.abs(midpoint - childMidpoint))
val scale = s0 + (s1 - s0) * (d - d0) / (d1 - d0)
child.scaleX = scale
child.scaleY = scale
}
return scrolled
} else {
return 0
}
}
}
Finally I have solved the problem by using this libraries/examples
DiscreteScrollView
android-viewpager-transformers
Here is the final result.
For full code see Carousel Demo
call scrollVerticallyBy(0, recycler, state) in onLayoutCompleted() method

Call getMeasuredWidth() or getWidth() for RecyclerView return 0 on data binding

I'm using data binding to setup a RecyclerView. Here is the binding adapter:
fun setRecyclerDevices(recyclerView: RecyclerView, items: List<Device>, itemBinder: MultipleTypeItemBinder,
listener: BindableListAdapter.OnClickListener<Device>?) {
var adapter = recyclerView.adapter as? DevicesBindableAdapter
if (adapter == null) {
val spannedGridLayoutManager = SpannedGridLayoutManager(orientation = SpannedGridLayoutManager.Orientation.VERTICAL,
spans = getSpanSizeFromScreenWidth(recyclerView.context, recyclerView))
recyclerView.layoutManager = spannedGridLayoutManager
recyclerView.addItemDecoration(SpaceItemDecorator(left = 15, top = 15, right = 15, bottom = 15))
adapter = DevicesBindableAdapter(items, itemBinder)
adapter.setOnClickListener(listener)
recyclerView.adapter = adapter
} else {
adapter.setOnClickListener(listener)
adapter.setItemBinder(itemBinder)
adapter.setItems(items)
}
}
getSpanSizeFromScreenWidth needs the recycler's width to do some calculation. But it always returns 0.
I tried to apply a ViewTreeObserver like this:
recyclerView.viewTreeObserver.addOnGlobalLayoutListener(object: ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
recyclerView.viewTreeObserver.removeOnGlobalLayoutListener(this)
val spannedGridLayoutManager = SpannedGridLayoutManager(orientation = SpannedGridLayoutManager.Orientation.VERTICAL,
spans = getSpanSizeFromScreenWidth(recyclerView.context, recyclerView))
recyclerView.layoutManager = spannedGridLayoutManager
}
})
Or use post like this:
recyclerView.post({
val spannedGridLayoutManager = SpannedGridLayoutManager(orientation = SpannedGridLayoutManager.Orientation.VERTICAL,
spans = getSpanSizeFromScreenWidth(recyclerView.context, recyclerView))
recyclerView.layoutManager = spannedGridLayoutManager
})
Code of getSpanSizeFormScreenWidth:
private fun getSpanSizeFromScreenWidth(context: Context, recyclerView: RecyclerView): Int {
val availableWidth = recyclerView.width.toFloat()
val px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 300f, context.resources.displayMetrics)
val margin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 15f, context.resources.displayMetrics)
return Math.max(1, Math.floor((availableWidth / (px + margin)).toDouble()).toInt()) * DevicesBindableAdapter.WIDTH_UNIT_VALUE
}
But it still returns 0 despite my RecyclerView being displayed on the screen (not 0).
Any ideas?
In inspecting the code, it appears that your RecyclerView may actually be fine, but your logic in getSpanSizeFromScreenWidth may not be.
It looks like this: Math.floor((availableWidth / (px + margin)).toDouble()).toInt() will always be 0 when availableWidth is less than (px + margin). This will then cause getSpanSizeFromScreenWidth to return 0.
Breaking it down:
Math.floor - rounds a double down to a whole number
availableWidth / (px + margin) - will be a low number (a fraction of availableWidth)
Therefore, you're going to get 0 at times especially on smaller screens and/or smaller density screens.
Does that make sense? May not be this issue, but I'd start there. It's hard to tell you exactly the issue without knowing the whole context, but that's likely your issue.
If that is not your issue, could you say what your value is for availableWidth, px, and margin during execution?

Categories

Resources