Android 实现 按钮从两边移到中间动画效果

这篇博客介绍了如何在Android中实现一个动画效果,让按钮从屏幕的两边逐渐移动到中间。通过布局和线程控制,创建出平滑的动画过渡。

Android 实现 按钮从两边移到中间动画效果

http://blog.youkuaiyun.com/aminfo/article/details/7589149

package org.shuxiang.test;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;

import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Gravity;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;

public class MainActivity extends Activity
{
	private DisplayMetrics dm;
	private LinearLayout layout, layoutLeft, layoutCenter, layoutRight;
	private static Handler messageHandler;
	
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)  
    {
        super.onCreate(savedInstanceState);
        
        dm = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(dm);
        
        layout = new LinearLayout(this);
        layout.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));
        layout.setOrientation(0); //水平
        layout.setGravity(Gravity.CENTER);
        
        layoutLeft = new LinearLayout(this);
        layoutLeft.setLayoutParams(new LayoutParams(0, LayoutParams.WRAP_CONTENT));
        layoutLeft.setGravity(Gravity.RIGHT);
        
        layoutCenter = new LinearLayout(this);
        layoutCenter.setLayoutParams(new LayoutParams(dm.widthPixels, LayoutParams.WRAP_CONTENT));
        
        layoutRight = new LinearLayout(this);
        layoutRight.setLayoutParams(new LayoutParams(0, LayoutParams.WRAP_CONTENT));
        layoutRight.setGravity(Gravity.LEFT);
        
        Button btA = new Button(this);
        btA.setText("按钮A");
        btA.setSingleLine(true);
        
        Button btB = new Button(this);
        btB.setText("按钮B");
        btB.setSingleLine(true);
        
        layoutLeft.addView(btA);
        layoutRight.addView(btB);
        
        layout.addView(layoutLeft);
        layout.addView(layoutCenter);
        layout.addView(layoutRight);
        
        setContentView(layout);
        
        Looper looper = Looper.myLooper();
        messageHandler = new MessageHandler(looper);
        
        new Thread(new Runnable()
        {
			@Override
			public void run()
			{
				int speed = 5;		//每次移动间隔毫秒,数字越大越慢
				int speedPx = 1;	//每次移动间隔像素,数字越大越快
				int loopCount = dm.widthPixels/2;
				int i=1;
				while(i < loopCount)
				{
					Message message = Message.obtain();
					message.what = 1;
					message.arg1 = i;
	                messageHandler.sendMessage(message);
	                
	                i = i + speedPx;
	                
	                synchronized(this)
					{
					    try
					    {
							wait(speed);
						}
					    catch (InterruptedException e)
					    {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
				}
				
				i=0;
				while(i < 10)
				{
					Message message = Message.obtain();
					message.what = 1;
					message.arg1 = loopCount - i;
	                messageHandler.sendMessage(message);
	                
	                i = i + speedPx;
	                
	                synchronized(this)
					{
					    try
					    {
							wait(speed);
						}
					    catch (InterruptedException e)
					    {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
				}
			}
        }).start();

    }
    
    class MessageHandler extends Handler
    {
    	public MessageHandler(Looper looper)
    	{
    		super(looper);
        }
        @Override
        public void handleMessage(Message msg)
        {
        	switch (msg.what)
        	{        	
        	case 1:
        		layoutLeft.setLayoutParams(new LayoutParams(msg.arg1, LayoutParams.WRAP_CONTENT));
        		layoutRight.setLayoutParams(new LayoutParams(msg.arg1, LayoutParams.WRAP_CONTENT));
        		layoutCenter.setLayoutParams(new LayoutParams(dm.widthPixels - msg.arg1 * 2, LayoutParams.WRAP_CONTENT));
        		break;
        	}        	
        }
    }
}


转载请标明:http://blog.youkuaiyun.com/aminfo/article/details/7589149

<think>我们将在MapCanvasView中添加两个方法: 1. focusOnWsc():将视角中心定位到WSC坐标点 2. focusOnArea():将视角定位到标记的区域(即所有记录点的包围盒中心,并调整缩放使得整个区域可见) 同时,我们需要在Activity中为两个新按钮设置点击事件,调用这两个方法。 具体实现步骤: ### 1. 在MapCanvasView中添加方法 #### 定位到WSC点: ```java /** * 将视图中心移动到WSC点,并保持当前缩放比例 */ public void focusOnWsc() { // 计算当前视图中心(逻辑坐标)与WSC点的偏移 float[] center = {viewCenterX, viewCenterY}; inverseMatrix.mapPoints(center); // 将视图中心转换为当前的逻辑坐标 float dx = wscPoint.x - center[0]; float dy = wscPoint.y - center[1]; // 平移视图 transformMatrix.postTranslate(-dx * getMatrixScale(transformMatrix), -dy * getMatrixScale(transformMatrix)); transformMatrix.invert(inverseMatrix); // 标记网格需要更新 gridDirty = true; invalidate(); } ``` 注意:上面的平移计算需要考虑到缩放,因为矩阵的平移是物理像素偏移。但实际上,我们这里采用另一种方法:直接重置视图变换,将WSC点移动到视图中心。 更简单的方法(避免复杂计算): ```java public void focusOnWsc() { // 将变换矩阵重置,然后平移到当前中心,再调整使得WSC点位于视图中心 resetTransformation(); // 重置变换(此时逻辑原点在视图中心) // 由于WSC点坐标是逻辑坐标,我们需要将视图平移使其位于视图中心 // 当前WSC点坐标是(wscPoint.x, wscPoint.y),而视图中心在(0,0)(逻辑坐标) // 所以需要将视图平移 (-wscPoint.x, -wscPoint.y) 使得WSC点移动到(0,0)(即视图中心) transformMatrix.preTranslate(-wscPoint.x, -wscPoint.y); inverseMatrix.reset(); transformMatrix.invert(inverseMatrix); gridDirty = true; invalidate(); } ``` 但是注意:resetTransformation()会将视图重置为初始状态(即逻辑原点在视图中心,无缩放)。如果我们希望保留当前缩放比例,只改变中心位置,则需要复杂计算。 #### 保留缩放比例的定位: ```java public void focusOnWsc() { // 获取当前缩放比例 float currentScale = getMatrixScale(transformMatrix); // 重置变换:先平移将WSC点移到视图中心,再缩放 transformMatrix.reset(); // 先缩放 transformMatrix.postScale(currentScale, currentScale, viewCenterX, viewCenterY); // 再平移:将WSC点移到视图中心(注意:缩放后的平移计算) transformMatrix.postTranslate(viewCenterX - wscPoint.x * currentScale, viewCenterY - wscPoint.y * currentScale); // 更新逆矩阵 transformMatrix.invert(inverseMatrix); gridDirty = true; invalidate(); } ``` #### 定位到区域(所有记录点的包围盒): ```java /** * 调整视图使得所有记录点可见,并居中显示 */ public void focusOnArea() { if (recordedPoints.isEmpty()) return; // 计算记录点的包围盒 float left = recordedPoints.get(0).x; float top = recordedPoints.get(0).y; float right = recordedPoints.get(0).x; float bottom = recordedPoints.get(0).y; for (PointF p : recordedPoints) { if (p.x < left) left = p.x; if (p.y < top) top = p.y; if (p.x > right) right = p.x; if (p.y > bottom) bottom = p.y; } // 包围盒中心 float centerX = (left + right) / 2; float centerY = (top + bottom) / 2; // 包围盒大小 float width = right - left; float height = bottom - top; // 计算视图可用大小(物理像素) int viewWidth = getWidth(); int viewHeight = getHeight(); // 计算需要的缩放比例(两边各留10%边距) float scaleX = (viewWidth * 0.9f) / width; float scaleY = (viewHeight * 0.9f) / height; float targetScale = Math.min(scaleX, scaleY); // 限制缩放范围 float currentScale = getMatrixScale(transformMatrix); if (targetScale > 5f) targetScale = 5f; if (targetScale < 0.5f) targetScale = 0.5f; // 重置变换矩阵 transformMatrix.reset(); // 先缩放 transformMatrix.postScale(targetScale, targetScale, viewCenterX, viewCenterY); // 再平移:将区域中心点移到视图中心 transformMatrix.postTranslate(viewCenterX - centerX * targetScale, viewCenterY - centerY * targetScale); // 更新逆矩阵 transformMatrix.invert(inverseMatrix); gridDirty = true; invalidate(); } ``` ### 2. 在Activity中添加按钮并设置点击事件 在布局文件中添加两个新按钮,例如: ```xml <Button android:id="@+id/btnFocusWsc" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="聚焦WSC"/> <Button android:id="@+id/btnFocusArea" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="聚焦区域"/> ``` 在Activity中设置点击事件: ```java private void setupButtons() { // ... 其他按钮设置 // 聚焦WSC按钮 findViewById(R.id.btnFocusWsc).setOnClickListener(v -> mapCanvas.focusOnWsc()); // 聚焦区域按钮 findViewById(R.id.btnFocusArea).setOnClickListener(v -> mapCanvas.focusOnArea()); } ``` ### 完整代码(MapCanvasView更新部分) 以下是MapCanvasView中新增的两个方法: ```java /** * 将视图中心定位到WSC点,保持当前缩放比例 */ public void focusOnWsc() { // 获取当前缩放比例 float currentScale = getMatrixScale(transformMatrix); // 计算当前视图中心在逻辑坐标的位置 float[] center = {viewCenterX, viewCenterY}; inverseMatrix.mapPoints(center); // 转换到逻辑坐标 // 计算需要平移的逻辑坐标偏移量(WSC点与当前视图中心的偏移) float dx = wscPoint.x - center[0]; float dy = wscPoint.y - center[1]; // 将逻辑偏移转换为视图偏移(乘以当前缩放比例,因为矩阵平移的单位是物理像素) // 注意:矩阵的平移是物理像素,所以我们需要将逻辑坐标的偏移量乘以缩放比例 float pixelDx = dx * currentScale; float pixelDy = dy * currentScale; // 平移视图 transformMatrix.postTranslate(-pixelDx, -pixelDy); transformMatrix.invert(inverseMatrix); gridDirty = true; invalidate(); } /** * 调整视图使得所有记录点可见,并居中显示 */ public void focusOnArea() { if (recordedPoints.isEmpty()) return; // 计算包围盒 float left = recordedPoints.get(0).x; float top = recordedPoints.get(0).y; float right = recordedPoints.get(0).x; float bottom = recordedPoints.get(0).y; for (int i = 1; i < recordedPoints.size(); i++) { PointF p = recordedPoints.get(i); left = Math.min(left, p.x); top = Math.min(top, p.y); right = Math.max(right, p.x); bottom = Math.max(bottom, p.y); } // 包围盒中心 float centerX = (left + right) / 2; float centerY = (top + bottom) / 2; // 包围盒宽高 float width = right - left; float height = bottom - top; // 如果区域没有大小,则设定一个默认大小(避免除以0) if (width == 0) width = 100; if (height == 0) height = 100; // 获取视图尺寸 int viewWidth = getWidth(); int viewHeight = getHeight(); // 计算缩放比例:使得区域在视图中可见,并留10%的边距 float scaleX = (viewWidth * 0.9f) / width; float scaleY = (viewHeight * 0.9f) / height; float targetScale = Math.min(scaleX, scaleY); // 限制缩放范围 targetScale = Math.max(0.5f, Math.min(targetScale, 5f)); // 重置变换 transformMatrix.reset(); // 缩放(以视图中心为缩放中心) transformMatrix.postScale(targetScale, targetScale, viewCenterX, viewCenterY); // 平移:将包围盒中心移到视图中心 transformMatrix.postTranslate(viewCenterX - centerX * targetScale, viewCenterY - centerY * targetScale); // 更新逆矩阵 transformMatrix.invert(inverseMatrix); gridDirty = true; invalidate(); } ``` 注意:`focusOnArea`方法中,如果记录点为空则直接返回。另外,我们计算包围盒时考虑了边界情况(宽高为0)。 ### 性能说明 - 这两个操作都是用户触发,频率低,计算开销可接受。 - 包围盒计算是线性复杂度,与记录点数量成正比,但通常记录点数量不会太大。 ### 注意事项 1. 在调用`focusOnArea`时,要确保记录点非空。 2. 缩放比例被限制在0.5到5之间,避免过大或过小。 ### 整合到之前的代码 将这两个方法加入到之前的`MapCanvasView`类中,然后在Activity中绑定按钮事件即可。 §§相关问题§§: 1. 在Android中如何实现视图的平移和缩放操作? 2. 如何计算一组点的最小包围盒? 3. 在自定义视图中,如何根据内容动态调整视图的缩放和平移? 4. 如何处理矩阵变换中的缩放中心和平移量? </think># Android 遥控器应用视图定位功能实现 我将为您实现两个新功能:定位到WSC坐标和定位到标记区域。以下是完整的实现方案: ## MapCanvasView.java 更新代码 ```java import android.content.Context; import android.graphics.*; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; import java.util.ArrayList; public class MapCanvasView extends View { // 新增功能相关变量 private boolean animateToWsc = false; private boolean animateToArea = false; private float animFraction = 0f; private static final int ANIM_DURATION = 300; // 动画持续时间(ms) private PointF targetCenter = new PointF(); // 动画目标中心点 private float targetScale = 1f; // 动画目标缩放值 private Matrix startMatrix = new Matrix(); // 动画开始矩阵 private long animStartTime; // 动画开始时间 // ... 其余变量保持不变 ... // 新增方法:定位到WSC坐标 public void focusOnWsc() { if (animateToArea) return; // 如果正在动画到区域,则忽略 // 设置动画目标 animateToWsc = true; animateToArea = false; animFraction = 0f; // WSC点作为目标中心 targetCenter.set(wscPoint); targetScale = getMatrixScale(transformMatrix); // 保持当前缩放 // 保存当前变换矩阵作为动画起点 startMatrix.set(transformMatrix); animStartTime = System.currentTimeMillis(); // 触发重绘 invalidate(); } // 新增方法:定位到标记区域 public void focusOnArea() { if (recordedPoints.isEmpty()) return; // 没有点时忽略 if (animateToWsc) return; // 如果正在动画到WSC,则忽略 // 设置动画目标 animateToWsc = false; animateToArea = true; animFraction = 0f; // 计算包围盒 RectF boundingBox = calculateBoundingBox(); // 设置目标中心为包围盒中心 targetCenter.set(boundingBox.centerX(), boundingBox.centerY()); // 计算合适的缩放级别 float viewWidth = getWidth(); float viewHeight = getHeight(); float areaWidth = boundingBox.width(); float areaHeight = boundingBox.height(); // 计算缩放比例(两边留10%边距) float scaleX = (viewWidth * 0.9f) / areaWidth; float scaleY = (viewHeight * 0.9f) / areaHeight; // 取较小的缩放比例,确保整个区域可见 targetScale = Math.min(scaleX, scaleY); // 限制缩放范围 targetScale = Math.max(0.5f, Math.min(targetScale, 5f)); // 保存当前变换矩阵作为动画起点 startMatrix.set(transformMatrix); animStartTime = System.currentTimeMillis(); // 标记网格需要更新 gridDirty = true; invalidate(); } // 计算记录点的包围盒 private RectF calculateBoundingBox() { if (recordedPoints.isEmpty()) return new RectF(); float minX = Float.MAX_VALUE; float minY = Float.MAX_VALUE; float maxX = Float.MIN_VALUE; float maxY = Float.MIN_VALUE; for (PointF point : recordedPoints) { if (point.x < minX) minX = point.x; if (point.y < minY) minY = point.y; if (point.x > maxX) maxX = point.x; if (point.y > maxY) maxY = point.y; } // 添加10%的边距 float marginX = (maxX - minX) * 0.1f; float marginY = (maxY - minY) * 0.1f; return new RectF( minX - marginX, minY - marginY, maxX + marginX, maxY + marginY ); } @Override protected void onDraw(Canvas canvas) { // 处理视图动画 handleViewAnimation(); // ... 其余绘制代码保持不变 ... } // 处理视图动画 private void handleViewAnimation() { if (!animateToWsc && !animateToArea) return; // 计算动画进度 (0.0 - 1.0) long elapsed = System.currentTimeMillis() - animStartTime; animFraction = Math.min(1.0f, (float) elapsed / ANIM_DURATION); // 应用缓动函数 (二次缓入缓出) float easedFraction = easeInOutQuad(animFraction); // 计算中间变换矩阵 Matrix currentMatrix = new Matrix(); if (animateToWsc) { // 动画到WSC点:保持缩放,改变中心点 float[] startCenter = getCurrentCenter(); float currentScale = getMatrixScale(startMatrix); // 计算当前中心位置 float centerX = startCenter[0] + (targetCenter.x - startCenter[0]) * easedFraction; float centerY = startCenter[1] + (targetCenter.y - startCenter[1]) * easedFraction; // 构建新矩阵 currentMatrix.reset(); currentMatrix.postScale(currentScale, currentScale, viewCenterX, viewCenterY); currentMatrix.postTranslate( viewCenterX - centerX * currentScale, viewCenterY - centerY * currentScale ); } else if (animateToArea) { // 动画到区域:同时改变缩放和中心点 float[] startCenter = getCurrentCenter(); float startScale = getMatrixScale(startMatrix); // 计算当前缩放 float currentScale = startScale + (targetScale - startScale) * easedFraction; // 计算当前中心位置 float centerX = startCenter[0] + (targetCenter.x - startCenter[0]) * easedFraction; float centerY = startCenter[1] + (targetCenter.y - startCenter[1]) * easedFraction; // 构建新矩阵 currentMatrix.reset(); currentMatrix.postScale(currentScale, currentScale, viewCenterX, viewCenterY); currentMatrix.postTranslate( viewCenterX - centerX * currentScale, viewCenterY - centerY * currentScale ); } // 应用变换矩阵 transformMatrix.set(currentMatrix); transformMatrix.invert(inverseMatrix); // 标记网格需要更新 gridDirty = true; // 如果动画未完成,继续重绘 if (animFraction < 1.0f) { postInvalidateOnAnimation(); } else { // 动画完成,重置标志 animateToWsc = false; animateToArea = false; } } // 获取当前视图中心(逻辑坐标) private float[] getCurrentCenter() { // 获取视图中心(屏幕坐标) float[] center = {viewCenterX, viewCenterY}; // 转换为逻辑坐标 inverseMatrix.mapPoints(center); return center; } // 缓动函数:二次缓入缓出 private float easeInOutQuad(float t) { return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t; } // ... 其余方法保持不变 ... } ``` ## MainActivity.java 更新代码 ```java import android.os.Bundle; import android.view.View; import android.widget.Button; import androidx.appcompat.app.AppCompatActivity; public class MainActivity extends AppCompatActivity { private MapCanvasView mapCanvas; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mapCanvas = findViewById(R.id.mapCanvas); setupButtons(); } private void setupButtons() { // ... 原有的按钮设置代码 ... // 新增按钮:定位到WSC Button btnFocusWsc = findViewById(R.id.btnFocusWsc); btnFocusWsc.setOnClickListener(v -> mapCanvas.focusOnWsc()); // 新增按钮:定位到区域 Button btnFocusArea = findViewById(R.id.btnFocusArea); btnFocusArea.setOnClickListener(v -> mapCanvas.focusOnArea()); } } ``` ## 布局文件更新示例 (activity_main.xml) ```xml <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- 地图画布 --> <com.yourpackage.MapCanvasView android:id="@+id/mapCanvas" android:layout_width="match_parent" android:layout_height="match_parent"/> <!-- 控制按钮容器 --> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:orientation="horizontal" android:padding="16dp"> <!-- 方向控制按钮 --> <Button android:id="@+id/btnUp" android:layout_width="60dp" android:layout_height="60dp" android:text="↑"/> <Button android:id="@+id/btnLeft" android:layout_width="60dp" android:layout_height="60dp" android:text="←"/> <Button android:id="@+id/btnRight" android:layout_width="60dp" android:layout_height="60dp" android:text="→"/> <Button android:id="@+id/btnDown" android:layout_width="60dp" android:layout_height="60dp" android:text="↓"/> <!-- 新增定位按钮 --> <Button android:id="@+id/btnFocusWsc" android:layout_width="100dp" android:layout_height="60dp" android:text="定位WSC"/> <Button android:id="@+id/btnFocusArea" android:layout_width="100dp" android:layout_height="60dp" android:text="定位区域"/> <!-- 记录按钮 --> <Button android:id="@+id/btnRecord" android:layout_width="100dp" android:layout_height="60dp" android:text="记录点"/> <!-- 闭合按钮 --> <Button android:id="@+id/btnCloseArea" android:layout_width="100dp" android:layout_height="60dp" android:text="闭合区域"/> </LinearLayout> </RelativeLayout> ``` ## 实现详解 ### 1. 定位到WSC坐标 (`focusOnWsc()`) - **功能**:将当前视图中心移动到WSC点位置,保持当前缩放比例 - **实现**: 1. 设置动画标志和初始状态 2. 目标中心设为WSC点的逻辑坐标 3. 保持当前缩放比例 4. 启动平滑过渡动画 ### 2. 定位到标记区域 (`focusOnArea()`) - **功能**:自动调整视图,使所有记录点可见并居中显示 - **实现**: 1. 计算所有记录点的包围盒 (bounding box) 2. 计算合适的缩放比例,确保整个区域可见(包含10%边距) 3. 设置目标中心为包围盒中心 4. 启动平滑过渡动画(同时改变缩放和位置) ### 3. 平滑动画实现 - **动画原理**:使用插值器计算中间状态 - **缓动函数**:二次缓入缓出(easeInOutQuad),提供更自然的动画效果 - **动画流程**: 1. 记录起始变换矩阵 2. 每帧计算当前进度 3. 根据进度插值计算中间矩阵 4. 应用中间矩阵并重绘视图 5. 动画结束重置状态 ### 4. 性能优化 - **矩阵操作**:避免创建新对象,重用现有Matrix - **增量更新**:仅当需要时更新网格 - **平滑动画**:使用postInvalidateOnAnimation()保证60fps流畅度 - **包围盒计算**:高效计算最小包围矩形 ## 使用说明 1. 将更新的MapCanvasView类集成到项目中 2. 在布局文件中添加两个新按钮 3. 在Activity中绑定按钮点击事件 4. 点击"定位WSC"按钮将视图中心移动到当前WSC点 5. 点击"定位区域"按钮将自动调整视图显示所有记录点 这种方法提供了平滑的过渡动画,确保用户能清晰地看到视图变化过程,同时保持界面响应流畅。
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值