I implemented ACTION_MOVE for ImageView which is bigger then its parent (FrameLayout). Now I'm trying to add some code to stop movement (both drag and pinch zooming) when at least one of Image edges is inside its parent (so it won’t show background of a parent).
I used the code like in this question.
case MotionEvent.ACTION_MOVE:
if (mode == DRAG) { //movement of first finger
matrix.set(savedMatrix);
float newX = event.getX() - start.x;
float newY = event.getY() - start.y;
if (view.getLeft() >= -392){
matrix.postTranslate(newX, newY);
}
}
else if (mode == ZOOM) { //pinch zooming
float[] f = new float[9];
float newDist = spacing(event);
if (newDist > 5f) {
matrix.set(savedMatrix);
scale = newDist / oldDist;
matrix.postScale(scale, scale, mid.x, mid.y);
}
matrix.getValues(f);
float scaleX = f[Matrix.MSCALE_X];
float scaleY = f[Matrix.MSCALE_Y];
if(scaleX <= MIN_ZOOM) {
matrix.postScale((MIN_ZOOM)/scaleX,
(MIN_ZOOM)/scaleY, mid.x, mid.y);
}
else if(scaleX >= MAX_ZOOM) {
matrix.postScale((MAX_ZOOM)/scaleX,
(MAX_ZOOM)/scaleY, mid.x, mid.y);
}
}
break;
I tried many things, and I know similar questions had been asked already but non of them fit my need so far.
My best guess was to add code below but it looks like mImageView.getLeft(), mImageView.getRight() etc returns parents edges not the images.
In onCreate():
DisplayMetrics displayMetrics =
getApplicationContext().getResources().getDisplayMetrics();
float screenWidth = displayMetrics.widthPixels;
float screenHeight = displayMetrics.heightPixels;
and in case MotionEvent.ACTION_MOVE
if (mImageView.getLeft() > 0 ||
mImageView.getRight() < screenWidth) {
matrix.postTranslate(0, newY);
break;
}
else if (mImageView.getTop() > 0 ||
mImageView.getBottom() < screenHeight) {
matrix.postTranslate(newX, 0);
break;
}
else {
matrix.postTranslate(newX, newY);
}
And this is my layout:
<RelativeLayout
android:id="#+id/imageViewContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/mag_background">
<TextView/>
<ImageView/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="#+id/imageViewHeadingLabel">
<ImageView
android:id="#+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/image"
android:scaleType="matrix"
android:layout_gravity="center_vertical|center_horizontal"
android:contentDescription="image to zoom"/>
</FrameLayout>
</RelativeLayout>
Thanks in advance for your help!
I found solution hire.
I reverse the checks so instead of not letting image to move outside layout I not letting image bounds to be move inside layout like this:
if (mode == DRAG) { //movement of first finger
matrix.set(savedMatrix);
matrix.getValues(matrixValues);
matrixX = matrixValues[2];
matrixY = matrixValues[5];
width = matrixValues[0] * (view.getDrawable().getIntrinsicWidth());
height = matrixValues[4] * (view.getDrawable().getIntrinsicHeight());
dx = event.getX() - start.x;
dy = event.getY() - start.y;
//if image will go inside left bound
if (matrixX + dx > 0){
dx = -matrixX;
}
//if image will go inside right bound
if(matrixX + dx + width < view.getWidth()){
dx = view.getWidth() - matrixX - width;
}
//if image will go inside top bound
if (matrixY + dy > 0){
dy = -matrixY;
}
//if image will go inside bottom bound
if(matrixY + dy + height < view.getHeight()){
dy = view.getHeight() - matrixY - height;
}
matrix.postTranslate(dx, dy);
}
However if you move zoomed image to the corner and then zoom out, it will show parent background until next touch event so problem is not fully solved.
Related
Java and C# (Xamarin.Android) accepted
I have a MediaRecorder that streams the cameraframes onto a TextureView (basic camera2 approach)
the TextureView fills my screen.
I want the video to fill the screen but keep the aspect ratio.
I know I have to eventually cut of either left/right or top/bottom parts of the video, to keep the aspect ratio.
How can I make the video fit the TextureView and "zoom in" until the whole screen is filled? I don't want "black bars" that compromise for the ratio to be kept.
I guess this has to be accomplished through overriding the OnMeasure of the TextureView
C# extended TextureView
//Called by my MediaRecorder Implementation, when I set the video dimensions (mostly width = 1280, height = 720)
public void SetAspectRatio(int width, int height)
{
if (width == 0 || height == 0)
throw new ArgumentException("Size cannot be negative.");
mRatioWidth = width;
mRatioHeight = height;
RequestLayout();
}
protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
base.OnMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.GetSize(widthMeasureSpec);
int height = MeasureSpec.GetSize(heightMeasureSpec);
if (0 == mRatioWidth || 0 == mRatioHeight)
{
SetMeasuredDimension(width, height);
}
else
{
if (width > (float)height * mRatioWidth / (float)mRatioHeight)
{
SetMeasuredDimension(width, width * mRatioHeight / mRatioWidth);
}
else
{
SetMeasuredDimension(height * mRatioWidth / mRatioHeight, height);
}
}
}
I have found the answer, thanks to this comment from this post.
C# Xamarin.Android:
I call the following method when my surface texture size changes (OnSurfaceSizeTextureChanged callback in SurfaceTextureListener)
public void ConfigureTransform(int viewWidth, int viewHeight)
{
int rotation = (int)ThisActivity.WindowManager.DefaultDisplay.Rotation;
var matrix = new Matrix();
var viewRect = new RectF(0, 0, viewWidth, viewHeight);
var bufferRect = new RectF(0, 0, previewSize.Height, previewSize.Width);
float centerX = viewRect.CenterX();
float centerY = viewRect.CenterY();
//Set the scale accordingly, to have the preview texture view fill the whole screen with cutting of the least amount of the preview as possible
//(but this way we keep the aspect ratio -> no preview distortion)
bool isLandscape = ((int)SurfaceOrientation.Rotation90 == rotation || (int)SurfaceOrientation.Rotation270 == rotation);
float scale;
if (isLandscape)
{
scale = System.Math.Max(
(float)viewWidth / previewSize.Height,
(float)viewWidth / previewSize.Width);
}
else
{
scale = System.Math.Max(
(float)viewHeight / previewSize.Height,
(float)viewHeight / previewSize.Width);
}
if ((int)SurfaceOrientation.Rotation90 == rotation || (int)SurfaceOrientation.Rotation270 == rotation || (int)SurfaceOrientation.Rotation180 == rotation)
{
bufferRect.Offset((centerX - bufferRect.CenterX()), (centerY - bufferRect.CenterY()));
matrix.SetRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.Fill);
matrix.PostScale(scale, scale, centerX, centerY);
//Small check if orientation is Reverse Portrait -> dont substract 2 from rotation, otherwise the preview is flipped by 180°
if ((int)SurfaceOrientation.Rotation180 == rotation)
matrix.PostRotate(90 * rotation, centerX, centerY);
else
matrix.PostRotate(90 * (rotation - 2), centerX, centerY);
}
TextureView.SetTransform(matrix);
}
I am a newbie and I want to use this example (example number 3) in my app.
My app will do some calculations at background and the result will be 0, 1, 2, 3, or 4. How should I make the needle move accordingly (needle should move left to right)?
Based on the output, you could use the code from this video: https://www.youtube.com/watch?v=tWCHDoO14aE to rotate the needle image a certain number of degrees to get to the value on the gauge
I didnt try but it must work. change stroke_colors attr as below to get proper coloring as in your image above. There are seven color segments so 1 segment means max_val/7
<FrameLayout
android:layout_width="260dp"
android:layout_height="162dp"
android:background="#354051">
<com.sccomponents.widgets.ScArcGauge
android:id="#+id/gauge"
xmlns:sc="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:padding="30dp"
sc:scc_angle_start="-180"
sc:scc_angle_sweep="180"
sc:scc_stroke_colors="#F7AD36|#8BBE28|#8BBE28|#F7AD36|#F7AD36|#EC4949|#EC4949"
sc:scc_stroke_colors_mode="solid"
sc:scc_stroke_size="30dp"/>
<ImageView
android:id="#+id/indicator"
android:layout_width="64dp"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="29dp"
android:layout_marginLeft="18dp"
android:src="#drawable/indicator"/>
</FrameLayout>
float angle0 = 0;
float angle1 = gauge.percentageToAngle(max_val/7);
float angle2 = gauge.percentageToAngle(max_val/7*3);
float angle3 = gauge.percentageToAngle(max_val/7*5);
float angle4 = gauge.percentageToAngle(max_val);
float angle;
if(cur_val >= 0 && cur_val < max_val/7)
angle = angle0;
else if(cur_val >= max_val/7 && cur_val < max_val/7*3)
angle = angle1;
else if(cur_val >= max_val/7*3 && cur_val < max_val/7*5)
angle = angle2;
else if(cur_val >= max_val/7*5 && cur_val < max_val)
angle = angle3;
else
angle = angle4;
indicator.setRotation(angle);
I have custom view control, which looks like this:
Inside my activity I want to be able to move this view around the screen by dragging it on green Arcs (left or right does not matter).
Also want to be able to detect if yellow arc at top is tapped, middle circle or bottom arc.
I'm having trouble to detect where the tap is in which area. This is the code that I'm using inside of my activity:
float dX, dY;
final MyCustomView myCustomView = (MyCustomView)findViewById(R.id.test);
final Boolean[] movable = {false};
myCustomView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
movable[0] = false;
dX = view.getX() - event.getRawX();
dY = view.getY() - event.getRawY();
int x = (int) event.getX();
int y = (int) event.getY();
if (myCustomView.leftArcRegion.contains(x,y) || myCustomView.rightArcRegion.contains(x,y)){
movable[0] = true;
} else if (myCustomView.topArcRegion.contains(x,y)){
//todo: do something if top arc area is selected
} else if (myCustomView.midRoundedBitmapRegion.contains(x,y)){
//todo: do something if mid bitmap area is selected
} else if (myCustomView.bottomArcRegion.contains(x,y)){
//todo: do something if bottom arc area is selected
}
break;
case MotionEvent.ACTION_MOVE:
if (movable[0]) {
view.animate()
.x(event.getRawX() + dX)
.y(event.getRawY() + dY)
.setDuration(0)
.start();
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
break;
default:
return false;
}
return true;
}
});
And these are public fields from my custom view control:
public Region topArcRegion;
private Path topArc;
//topArc is my top arc path
RectF rectFTop = new RectF();
topArc.computeBounds(rectFTop, true);
topArcRegion = new Region();
topArcRegion.setPath(topArc, new Region((int) rectFTop.left, (int) rectFTop.top,
(int) rectFTop.right, (int) rectFTop.bottom));
But it looks like it uses rectangular shapes for these regions, not arcs when checking with this "contains" method. And because of that I'm not getting expected results.
So, how can I detect where is initial tap (top arc, bottom arc, side arcs or middle bitmap) in order to apply my app logic?
Since you're only looking to detect touches inside an arc segment, it should't be too complicated.
Each of your arc segments is defined as the space between two concentric circles and between start and end angles. So all you really want to do is do a little trig to determine the distance from the center of the circles to your touch point and the angle from the center to your touch point.
float x = touchevent.getX();
float y = touchevent.getY();
// Transform relative to arc centers
x -= circle1.x;
y -= circle1.y;
double dist = Math.sqrt(x*x + y*y);
double angle = Math.atan2(y,x) * 180 / Math.PI;
// Given an arc segment defined by circle1, circle2, angle1, angle2:
boolean touch = dist > circle1.radius && dist < circle2.radius &&
angle > angle1 && angle < angle2;
You'll probably have to play around a bit depending on whether angle1 > angle2 or vice versa. If there's any chance that any of the angles cross the zero-degree angle, it gets a little trickier.
Meta: For clarity, I used sqrt() to compute distance, but you can optimize this code by skipping the sqrt() and compare distance² instead:
double dist2 = x*x + y*y;
if (dist2 > circle1.radius * circle1.radius &&
dist2 < circle2.radius * circle2.radius &&
...
One more edit: computing trig functions can be expensive; certainly a lot more expensive than computing distance².
In the interest of optimization, you should check the distance against the circle radii before bothering with the trig:
boolean touch = dist > circle1.radius && dist < circle2.radius;
if (touch) {
// This is only a *possible* touch, check the angles now
double angle = Math.atan2(y,x) * 180 / Math.PI;
touch = angle > angle1 && angle < angle2;
}
I am creating a game, where there will be a lot of clickable ImageView(s), and one unclickable imageView, when the clickable ImageView is clicked, it should swap its position with the unclickable ImageView (in this example, its called imageView2).
Everything works fine when I am using px unit for my imageView(s). However, as you know using px units will resulted in different display on different devices. However, the property animations does not work when I use dp units. How do I get it working with dp units ??
MainActivity.java
public class MainActivity extends Activity implements View.OnClickListener {
ImageView imageView1, imageView2;
float x, x_9, y, y_9;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView1 = (ImageView) findViewById(R.id.imageView1);
imageView2 = (ImageView) findViewById(R.id.imageView2);
imageView1.setOnClickListener(this);
}
#Override
public void onClick(View v) {
// Get x position of clicked imageView
x = v.getX();
// Get x position of imageView2
x_9 = imageView2.getX();
// Get y position of clicked imageView
y = v.getY();
// Get y position of imageView2
y_9 = imageView2.getY();
// Check if imageViews are align with each other either horizontally or vertically
if((x == x_9 && y + 100 == y_9) || (x == x_9 && y - 100 == y_9) || (x + 100 == x_9 && y == y_9) || (x - 100 == x_9 && y == y_9)) {
// If they are aligned, swap their position with property animation
PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("translationX", x_9);
PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("translationY", y_9);
PropertyValuesHolder pvhX9 = PropertyValuesHolder.ofFloat("translationX", x);
PropertyValuesHolder pvhY9 = PropertyValuesHolder.ofFloat("translationY", y);
ObjectAnimator blockAnim = ObjectAnimator.ofPropertyValuesHolder(v, pvhX, pvhY);
blockAnim.setDuration(500);
blockAnim.setRepeatCount(0);
blockAnim.start();
ObjectAnimator blockAnim2 = ObjectAnimator.ofPropertyValuesHolder(imageView2, pvhX9, pvhY9);
blockAnim2.setDuration(500);
blockAnim2.setRepeatCount(0);
blockAnim2.start();
}
}
}
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
android:paddingBottom="#dimen/activity_vertical_margin" tools:context=".MainActivity">
<RelativeLayout
android:id="#+id/relative"
android:background="#color/grey"
android:layout_width="400dp"
android:layout_height="400dp"
>
<ImageView
android:id="#+id/imageView1"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#drawable/smiley1"/>
<ImageView
android:id="#+id/imageView2"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#drawable/smiley2"
android:translationX="100dp"/>
</RelativeLayout>
</RelativeLayout>
How do I work around this problem ? All suggestions are greatly welcomed. Thank you.
As suggested by Nikola Milutinovic, I added GlobalLayoutListener like below
this.findViewById(R.id.relative).getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
x = v.getX();
x_9 = imageView2.getX();
y = v.getY();
y_9 = imageView2.getY();
}
});
I think the way to do it is to change your hard coded referece to the width and height by the actual value contained by the imageViews. In your if condition change 100 to getHeight() or getWidth();
int height = imageView1.getHeight();
int width = imageView1.getWidth();
if((x == x_9 && y + height == y_9) || (x == x_9 && y - height == y_9) ||
(x + width == x_9 && y == y_9) || (x - width == x_9 && y == y_9)){
}
This way you do not have a reference to px. The "100" value that your were using meant 100px. The real width or height for an hdpi device would of been something like 1.5*100 = 150px. By using getWidth() and getHeight(), you are using the actual value of px in your screen. I hope it is clear. You will need to modify your if condition for more than 2 imageViews, but the principle is the same.
Finally it is better to use AnimatorSet if you want your animations to be synchronized.
ArrayList <Animator> animations = new ArrayList<Animator>();
animations.add(ObjectAnimator.ofPropertyValuesHolder(v, pvhX, pvhY));
animations.add(ObjectAnimator.ofPropertyValuesHolder(imageView2, pvhX9, pvhY9));
AnimatorSet animatoreSet = new AnimatorSet();
animatorSet.setDuration(500);
animatorSet.setRepeatCount(0);
animatorSet.playTogether(animations);
animatorSet.start();
By doing this if you need to add listeners you just add them once.
See documentation for these two methods below and find which one suits you the most. container would be your root layout (this RelativeLayout at the top or this with id relative)
container.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
}
});
container.getViewTreeObserver().addOnDrawListener(new ViewTreeObserver.OnDrawListener() {
#Override
public void onDraw() {
}
});
Here is the example of my implementation of this listener.
I do all sorts of stuff inside. Please take a look.
public class CustomOnGlobalLayoutListener implements ViewTreeObserver.OnGlobalLayoutListener {
private Context context;
private View fragment;
private float x1;
private float y1;
private float x2;
private float y2;
public CustomOnGlobalLayoutListener(Context context, View view, float x1, float y1, float x2, float y2) {
this.context = context;
this.fragment = view;
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
#Override
public void onGlobalLayout() {
fragment.setX(x1);
fragment.setY(y1);
removeOnGlobalLayoutListener();
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) fragment.getLayoutParams();
fragment.setLayoutParams(layoutParams);
fragment.setPivotX(0);
fragment.setPivotY(0);
fragment.setScaleX((x2 - x1) / fragment.getMeasuredWidth());
fragment.setScaleY((y2 - y1) / fragment.getMeasuredHeight());
fragment.setAlpha(0f);
PropertyValuesHolder pA = PropertyValuesHolder.ofFloat(View.ALPHA, 1f);
PropertyValuesHolder pX = PropertyValuesHolder.ofFloat(View.X, 0f);
PropertyValuesHolder pY = PropertyValuesHolder.ofFloat(View.Y, 0f);
PropertyValuesHolder pSx = PropertyValuesHolder.ofFloat(View.SCALE_X, 1);
PropertyValuesHolder pSy = PropertyValuesHolder.ofFloat(View.SCALE_Y, 1);
final ObjectAnimator animator1 = ObjectAnimator.ofPropertyValuesHolder(fragment, pA, pX, pY, pSx, pSy);
animator1.setInterpolator(new AccelerateDecelerateInterpolator());
animator1.setDuration(200);
animator1.start();
}
private void removeOnGlobalLayoutListener() {
if (Build.VERSION.SDK_INT < 16) {
//noinspection deprecation
fragment.getViewTreeObserver().removeGlobalOnLayoutListener(this);
} else {
fragment.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
}
DO NOT FORGET TO REMOVE GLOBAL LAYOUT LISTENER;
If you have any problem implementing this, write and we will think something out :)
You can also check my github repo where I am actually using this to achieve opening fragment with viewpager from point of touch.
https://github.com/mrnmilutinovic/HumanityTestProject
Check ImagePagerFragment class to see it's usage.
This code is compilable, so you can also run and see that it works.
The bitmap A has a position which is its X/Y position is static, bitmap B is moving bitmap which moves from the bottom of the screen to where ever the user touches on screen. When the moving bitmap reaches the static bitmap however it is not recognized as a collision, the moving bitmap uses float which i round off and the static bitmap uses int. Below is a snippet of code, I want to figure out what is needed to be changed for my bitmaps crossing paths to be considered a collision
canvas.drawBitmap(rock2, 70, 32, null);
//this is where it checks to see if the moving bitmaps y coordinate is matched to the Y coordinate of the static bitmap
if(canvas.getHeight() - Math.round(animY*-1) == (canvas.getHeight() - (canvas.getHeight()-32))){
Log.d("ALERT", "COLLIDE");
}
//the Y value is *-1 because the moving bitmap is starting from the bottom of the screen so this converts the value to positive value
I wonder if my calculations are off, or I am going about this collision the wrong way. Below is my movement code snippet
//UP Y ANIMATION
if(animY*-1 < canvas.getHeight() && !bumpY){
animY += speedY;
//IF ROCK IS NOT AT EXTREME LEFT OR EXTREME RIGHT KEEP ON TRACK
if(animX < (canvas.getWidth()/2) || animX*-1 < (canvas.getWidth()/2) && !bumpX){
animX += speedX;
//IF ROCK HITS EDGE LEFT/RIGHT RETURN TO SENDER (INDICATE BUMP)
if(animX*-1 > (canvas.getWidth()/2) - rock.getWidth()/2 || animX > (canvas.getWidth()/2) - rock.getWidth()/2){
bumpX = true;
bumpY = true;
}
}
//IF Y HITS TOP OF SCREEN
if(animY*-1 > canvas.getHeight()){
bumpY = true;
}
}
//DOWN Y ANIMATION
if(animY < 0 && bumpY){
//REVERSE DIRECTION OF Y
animY -= speedY;
//IF ROCK HITS TOP OR SIDE REVERSE X DIRECTION
if(bumpX || bumpY)
animX -= speedX;
//IF AT STARTING POINT
if(animY > 0){
bumpY = false;
bumpX = false;
}
}
//in an ontouch method where the X and Y values are calculated
case MotionEvent.ACTION_UP:
finalX = event.getX() - (cross.getWidth() / 2);
finalY = event.getY() - (cross.getHeight() / 2);
moveToX = finalX - startX;
moveToY = finalY - startY;
speedX = moveToX / 50;
speedY = moveToY / 50;
break;
Any tips would be appreciated, thank you.
sorry if this is a dumb answer, but should your check be:
if(canvas.getHeight() - Math.round(animY*-1) >= (canvas.getHeight() - (canvas.getHeight()-32))){
instead? (change the "==" to ">=" ) otherwise you are only checking for when the rock lands EXACTLY on (canvas.getheight -32), rather than checking past it