英文:
Issue with Custom View Position Jump when Changing Pivot Point
问题
I'm encountering an unexpected behavior in my Android app involving a custom view named MyView
. I'm attempting to achieve both translation and rotation effects on this view. When the pivot point is set to the center of the view, everything work as expected. However, as soon as I change the pivot point (for instance, to "Point 2" selected from a spinner), the view unexpectedly jumps to a different position, as illustrated in this GIF:
Edit:
Observations:
- The issue primarily arises when the view is rotated. If the view has no rotation (rotation = 0), changing the pivot point doesn't cause any problems.
- Also if before changing the pivot point, I restore the view to its initial rotation (rotation = 0) this prevent also the issue from occurring.
To illustrate this point further, here's an example:
Could anyone kindly advise on what might be causing the view displacement when changing the pivot point and suggest potential fixes? Your help is much appreciated.
Below is the complete code:
// MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FixedPosLayout fixedPosLayout = findViewById(R.id.fixedPosLayout);
MyView myView = new MyView(this);
FixedPosLayout.LayoutParams params =
new FixedPosLayout.LayoutParams(200, 400, 300, 80);
fixedPosLayout.addView(myView, params);
ImageButton moveLeftButton = findViewById(R.id.moveLeftButton);
ImageButton moveRightButton = findViewById(R.id.moveRightButton);
ImageButton moveUpButton = findViewById(R.id.moveUpButton);
ImageButton moveBottomButton = findViewById(R.id.moveBottomButton);
ImageButton rotateLeftButton = findViewById(R.id.rotateLeftButton);
ImageButton rotateRightButton = findViewById(R.id.rotateRightButton);
AppCompatButton resetButton = findViewById(R.id.resetButton);
Spinner spinner = findViewById(R.id.pivotPointSpinner);
float d = 10;
float r = 10;
moveLeftButton.setOnClickListener(v -> myView.moveLeftBy(d));
moveRightButton.setOnClickListener(v -> myView.moveRightBy(d));
moveUpButton.setOnClickListener(v -> myView.moveUpBy(d));
moveBottomButton.setOnClickListener(v -> myView.moveDownBy(d));
rotateRightButton.setOnClickListener(v -> myView.rotateClockwiseBy(r));
rotateLeftButton.setOnClickListener(v -> myView.rotateConterClockwiseBy(r));
resetButton.setOnClickListener(v -> {
myView.reset();
spinner.setSelection(0); // Select first item
});
// Spinner
String[] items = {"Center Point", "Point 1", "Point 2"};
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, items);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
// Handle spinner click
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
myView.setPivotPoint(position);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
myView.setPivotPoint(0); // Set center point as default pivot point
}
});
}
}
// MyView.java
public class MyView extends View {
public static final int RADIUS = 24; // Anchor radius
public static final int OFFSET = 1;
private final Paint paint;
public RectF rectPoint1;
public RectF rectPoint2;
public MyView(Context context) {
super(context);
paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
rectPoint1 = new RectF();
rectPoint2 = new RectF();
}
public void moveRightBy(float dRight) {
setX(getX() + dRight);
}
public void moveLeftBy(float dLeft) {
moveRightBy(-dLeft);
}
public void moveDownBy(float dDown) {
setY(getY() + dDown);
}
public void moveUpBy(float dUp) {
moveDownBy(-dUp);
}
public void rotateClockwiseBy(float dDegree) {
setRotation(getRotation() + dDegree);
}
public void rotateConterClockwiseBy(float dDegree){
rotateClockwiseBy(-dDegree);
}
public void setPivotPoint(int position) {
switch (position) {
// Set center point as pivot
case 0: {
float width = getWidth();
float height = getHeight();
setPivotX(width/2);
setPivotY(height/2);
break;
}
// Set Point 1 as pivot
case 1: {
setPivotX(rectPoint1.centerX());
setPivotY(rectPoint1.centerY());
break;
}
// Set Point 2 as pivot
case 2: {
setPivotX(rectPoint2.centerX());
setPivotY(rectPoint2.centerY());
break;
}
}
}
@Override
protected void onDraw(Canvas canvas) {
float width = getWidth();
float height = getHeight();
//Draw line between anchor points
canvas.drawLine(
rectPoint1.centerX(), rectPoint1.centerY(),
rectPoint2.centerX(), rectPoint2.centerY(), paint);
//Draw anchor circle
canvas.drawCircle(rectPoint1.centerX(), rectPoint1.centerY(), RADIUS, paint);
canvas.drawCircle(rectPoint2.centerX(), rectPoint2.centerY(), RADIUS, paint);
//Draw bounding rect
canvas.drawRect(OFFSET, OFFSET, width - OFFSET, height - OFFSET, paint);
// Draw anchors label
canvas.drawText("1", rectPoint1.centerX(), rectPoint1.centerY(), paint);
canvas.drawText("2", rectPoint2.centerX(), rectPoint2.centerY(), paint);
// Draw small circle at the center
canvas.drawCircle(width/2f, height/2f, 4, paint);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
//Recompute anchors rect
rectPoint1.set(
OFFSET, h /2f - RADIUS + OFFSET,
2*RADIUS, h /2f + RADIUS - OFFSET
);
rectPoint2.set(
w - 2*RADIUS, h /2f - RADIUS + OFFSET,
w - OFFSET, h /2f + RADIUS - OFFSET
);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Measure exactly
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}
英文:
I'm encountering an unexpected behavior in my Android app involving a custom view named MyView
. I'm attempting to achieve both translation and rotation effects on this view. When the pivot point is set to the center of the view, everything work as expected. However, as soon as I change the pivot point (for instance, to "Point 2" selected from a spinner), the view unexpectedly jumps to a different position, as illustrated in this GIF:
Edit:
Observations:
- The issue primarily arises when the view is rotated. If the view has no rotation (rotation = 0), changing the pivot point doesn't cause any problems.
- Also if before changing the pivot point, I restore the view to its initial rotation (rotation = 0) this prevent also the issue from occurring.
To illustrate this point further, here's an example:
Could anyone kindly advise on what might be causing the view displacement when changing the pivot point and suggest potential fixes? Your help is much appreciated.
Below is the complete code:
// MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FixedPosLayout fixedPosLayout = findViewById(R.id.fixedPosLayout);
MyView myView = new MyView(this);
FixedPosLayout.LayoutParams params =
new FixedPosLayout.LayoutParams(200, 400, 300, 80);
fixedPosLayout.addView(myView, params);
ImageButton moveLeftButton = findViewById(R.id.moveLeftButton);
ImageButton moveRightButton = findViewById(R.id.moveRightButton);
ImageButton moveUpButton = findViewById(R.id.moveUpButton);
ImageButton moveBottomButton = findViewById(R.id.moveBottomButton);
ImageButton rotateLeftButton = findViewById(R.id.rotateLeftButton);
ImageButton rotateRightButton = findViewById(R.id.rotateRightButton);
AppCompatButton resetButton = findViewById(R.id.resetButton);
Spinner spinner = findViewById(R.id.pivotPointSpinner);
float d = 10;
float r = 10;
moveLeftButton.setOnClickListener(v -> myView.moveLeftBy(d));
moveRightButton.setOnClickListener(v -> myView.moveRightBy(d));
moveUpButton.setOnClickListener(v -> myView.moveUpBy(d));
moveBottomButton.setOnClickListener(v -> myView.moveDownBy(d));
rotateRightButton.setOnClickListener(v -> myView.rotateClockwiseBy(r));
rotateLeftButton.setOnClickListener(v -> myView.rotateConterClockwiseBy(r));
resetButton.setOnClickListener(v -> {
myView.reset();
spinner.setSelection(0); // Select first item
});
// Spinner
String[] items = {"Center Point", "Point 1", "Point 2"};
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, items);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
// Handle spinner click
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
myView.setPivotPoint(position);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
myView.setPivotPoint(0); // Set center point as default pivot point
}
});
}
}
// MyView.java
public class MyView extends View {
public static final int RADIUS = 24; // Anchor radius
public static final int OFFSET = 1;
private final Paint paint;
public RectF rectPoint1;
public RectF rectPoint2;
public MyView(Context context) {
super(context);
paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
rectPoint1 = new RectF();
rectPoint2 = new RectF();
}
public void moveRightBy(float dRight) {
setX(getX() + dRight);
}
public void moveLeftBy(float dLeft) {
moveRightBy(-dLeft);
}
public void moveDownBy(float dDown) {
setY(getY() + dDown);
}
public void moveUpBy(float dUp) {
moveDownBy(-dUp);
}
public void rotateClockwiseBy(float dDegree) {
setRotation(getRotation() + dDegree);
}
public void rotateConterClockwiseBy(float dDegree){
rotateClockwiseBy(-dDegree);
}
public void setPivotPoint(int position) {
switch (position) {
// Set center point as pivot
case 0: {
float width = getWidth();
float height = getHeight();
setPivotX(width/2);
setPivotY(height/2);
break;
}
// Set Point 1 as pivot
case 1: {
setPivotX(rectPoint1.centerX());
setPivotY(rectPoint1.centerY());
break;
}
// Set Point 2 as pivot
case 2: {
setPivotX(rectPoint2.centerX());
setPivotY(rectPoint2.centerY());
break;
}
}
}
@Override
protected void onDraw(Canvas canvas) {
float width = getWidth();
float height = getHeight();
//Draw line between anchor points
canvas.drawLine(
rectPoint1.centerX(), rectPoint1.centerY(),
rectPoint2.centerX(), rectPoint2.centerY(), paint);
//Draw anchor circle
canvas.drawCircle(rectPoint1.centerX(), rectPoint1.centerY(), RADIUS, paint);
canvas.drawCircle(rectPoint2.centerX(), rectPoint2.centerY(), RADIUS, paint);
//Draw bounding rect
canvas.drawRect(OFFSET, OFFSET, width - OFFSET, height - OFFSET, paint);
// Draw anchors label
canvas.drawText("1", rectPoint1.centerX(), rectPoint1.centerY(), paint);
canvas.drawText("2", rectPoint2.centerX(), rectPoint2.centerY(), paint);
// Draw small circle at the center
canvas.drawCircle(width/2f, height/2f, 4, paint);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
//Recompute anchors rect
rectPoint1.set(
OFFSET, h /2f - RADIUS + OFFSET,
2*RADIUS, h /2f + RADIUS - OFFSET
);
rectPoint2.set(
w - 2*RADIUS, h /2f - RADIUS + OFFSET,
w - OFFSET, h /2f + RADIUS - OFFSET
);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Measure exactly
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}
public void reset() {
setPivotPoint(0);
setRotation(0);
setX(200);
setY(400);
}
}
// FixedPosLayout.java
public class FixedPosLayout extends ViewGroup {
public FixedPosLayout(Context context) {
super(context);
}
public FixedPosLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FixedPosLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
for (int i = 0; i<count; ++i) {
View child = getChildAt(i);
FixedPosLayout.LayoutParams params =
(FixedPosLayout.LayoutParams) child.getLayoutParams();
int left = params.x;
int top = params.y;
child.layout(left, top,
left + child.getMeasuredWidth(),
top + child.getMeasuredHeight());
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Measure this view
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// Measure children
int count = getChildCount();
for (int i = 0; i<count; ++i) {
View child = getChildAt(i);
FixedPosLayout.LayoutParams lp =
(FixedPosLayout.LayoutParams) child.getLayoutParams();
int childWidthMeasureSpec =
MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
int childHeightMeasureSpec =
MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
public static class LayoutParams extends ViewGroup.LayoutParams {
public int x;
public int y;
public LayoutParams(int x, int y, int width, int height) {
super(width, height);
this.x = x;
this.y = y;
}
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity"
android:layout_height="match_parent"
android:layout_width="match_parent">
<com.abdo.rotateviewquestion.FixedPosLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:id="@+id/fixedPosLayout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/barrier_horizontal_top"/>
<ImageButton
android:id="@+id/moveRightButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/move_right"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/barrier_vertical_right"/>
<ImageButton
android:id="@+id/moveLeftButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/move_left"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/barrier_vertical_left"/>
<ImageButton
android:id="@+id/moveUpButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/move_up"
app:layout_constraintBottom_toTopOf="@+id/moveBottomButton"
app:layout_constraintEnd_toEndOf="@id/moveBottomButton"
app:layout_constraintStart_toStartOf="@id/moveBottomButton" />
<ImageButton
android:id="@+id/moveBottomButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/move_down"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/moveLeftButton"/>
<ImageButton
android:id="@+id/rotateLeftButton"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:src="@drawable/rotate_left"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageButton
android:id="@+id/rotateRightButton"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:src="@drawable/rotate_right"
app:layout_constraintEnd_toStartOf="@id/rotateLeftButton"
app:layout_constraintBottom_toBottomOf="parent" />
<Spinner
android:id="@+id/pivotPointSpinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_horizontal_top"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/resetButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Reset"
app:layout_constraintTop_toBottomOf="@id/barrier_horizontal_top"
app:layout_constraintEnd_toStartOf="@id/pivotPointSpinner"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_vertical_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="right"
app:constraint_referenced_ids="moveUpButton, moveBottomButton"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_vertical_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="left"
app:constraint_referenced_ids="moveUpButton, moveBottomButton"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_horizontal_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
app:constraint_referenced_ids="moveUpButton, rotateLeftButton, rotateRightButton"/>
</androidx.constraintlayout.widget.ConstraintLayout>
答案1
得分: 2
原因:
> setRotation
> 设置视图围绕中心点旋转的度数。增加的值会导致顺时针旋转。
文档 表示旋转围绕中心点发生变化;所以当你在修改中心点之前改变旋转时,旋转会考虑原始中心点;然后在你修改中心点之后,旋转仍然考虑原始中心点;因此你会看到视图跳动。
此外,如果屏幕坐标与中心点不同,改变视图的 pivotX
/pivotY
值会改变视图在屏幕上的位置。这个回答有一个很好的 解释。所以在初始旋转的情况下,你不会注意到变化,因为此时中心点与屏幕坐标相同。但是,当视图被旋转时情况就不同了。
解决方案:
因此,一个简单的解决方案是获取修改中心点之前和之后的视图位置的差异;然后用这个差异重新定位视图的 x/y 位置:
String TAG = "LOG_TAG";
public void setPivotPoint(int position) {
// 获取视图在屏幕上的位置
int[] locationBefore = new int[2];
getLocationOnScreen(locationBefore);
Log.d(TAG, "\ngetLocationOnScreen: x = " + locationBefore[0] + " y = " + locationBefore[1]);
switch (position) {
// 将中心点设置为中心点
case 0: {
float width = getWidth();
float height = getHeight();
setPivotX(width / 2);
setPivotY(height / 2);
break;
}
// 将点1设置为中心点
case 1: {
setPivotX(rectPoint1.centerX());
setPivotY(rectPoint1.centerY());
break;
}
// 将点2设置为中心点
case 2: {
setPivotX(rectPoint2.centerX());
setPivotY(rectPoint2.centerY());
break;
}
}
// 修改了 pivotX 和 pivotY 后获取视图在屏幕上的新位置
int[] locationAfter = new int[2];
getLocationOnScreen(locationAfter);
Log.d(TAG, "getLocationOnScreen: x = " + locationAfter[0] + " y = " + locationAfter[1]);
// 将视图的当前位置还原到原始位置
int xDiff = locationAfter[0] - locationBefore[0];
setX(getX() - xDiff);
int yDiff = locationAfter[1] - locationBefore[1];
setY(getY() - yDiff);
// 检查新位置
getLocationOnScreen(locationAfter);
Log.d(TAG, "getLocationOnScreen: x = " + locationAfter[0] + " y = " + locationAfter[1]);
}
预览:
英文:
Cause:
> setRotation
> Sets the degrees that the view is rotated around the pivot point. Increasing values result in clockwise rotation.
The documentation says that the rotation changes around the pivot point; so when you change the rotation before modifying the pivot point, the rotation considered the original pivot point; then after you modify the pivot, still the rotation considers the original pivot; and therefore you'd see the view jumps.
In addition, changing the pivotX
/pivotY
values of a view would change the view position on the screen in case that the screen coordinates are different than the pivot point. This answer has a nice breakdown for that. So, you'd not notice a change for the initial rotation because in that case the pivot point are the same as the screen coordinates. But, that will not be the case when the view is rotated.
Solution:
So, one simple solution is to get the difference between the view location before and after modifying the pivot; and relocate the view position x/y with that difference:
String TAG = "LOG_TAG";
public void setPivotPoint(int position) {
// Get the location of the View on the screen
int[] locationBefore = new int[2];
getLocationOnScreen(locationBefore);
Log.d(TAG, "\ngetLocationOnScreen: x = " + locationBefore[0] + " y = " + locationBefore[1]);
switch (position) {
// Set center point as pivot
case 0: {
float width = getWidth();
float height = getHeight();
setPivotX(width / 2);
setPivotY(height / 2);
break;
}
// Set Point 1 as pivot
case 1: {
setPivotX(rectPoint1.centerX());
setPivotY(rectPoint1.centerY());
break;
}
// Set Point 2 as pivot
case 2: {
setPivotX(rectPoint2.centerX());
setPivotY(rectPoint2.centerY());
break;
}
}
// Get the new location of the View on the screen after changing the pivotX & pivotY
int[] locationAfter = new int[2];
getLocationOnScreen(locationAfter);
Log.d(TAG, "getLocationOnScreen: x = " + locationAfter[0] + " y = " + locationAfter[1]);
// Change the current location of the View back to the original location
int xDiff = locationAfter[0] - locationBefore[0];
setX(getX() - xDiff);
int yDiff = locationAfter[1] - locationBefore[1];
setY(getY() - yDiff);
// Checking the new location
getLocationOnScreen(locationAfter);
Log.d(TAG, "getLocationOnScreen: x = " + locationAfter[0] + " y = " + locationAfter[1]);
}
Preview:
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论