前言
应用开发展示图片的时候最担心的就是图片展示变形,这通常都是因为图片的真实大小和ImageView大小不相同导致的,这种问题只要后台和客户端规定好图片的宽高比固定就能解决,但是在布局中有些控件却却可能压缩ImageView占用控件,一般占用不大,普通图片看不出来有变形问题,有一些特殊的图片却能明显看出拉伸效果,这时候使用像素尺子工具直接测量ImageView在屏幕上展示的宽高,计算它的宽高比能够有效的避免这类问题。
实现效果
实现接口
像素尺子往往作为单独的工具出现,能够测量其他应用的界面尺寸,还需要能够跟随用户的手指移动定位要测量的控件。普通的Activity界面无法实现,PopupWindow也无法实现,因为前者是应用类型窗口类型,PopupWindow属于子窗口类型必须要有依赖的Activity存在才会展示,否则无法展示。查看WindowManager里的Window类型,一共有三种类型:
- 应用Window(1~99):通常Activity窗口就属于这种类型
- 子Window(1000~1999):不能单独存在,它需要附属在特定的父Window之中,比如前面提到的PopupWindow就是这种情况,必须要在展示它的时候提供附属的Activity父窗口
- 系统Window(2000~2999):是需要声明权限才能创建的Window
为了能够测量所有的应用界面,像素尺子界面需要设置为系统Window类型,比较常用的就是SYSTEM_ALERT_WINDOW类型,它需要请求权限,否则无法展示。Android系统为用户直接添加系统界面提供了接口,WindowManager的三个方法如下,分别对应添加、更新和删除界面操作。
// 添加View
public void addView(View view, ViewGroup.LayoutParams params);
// 更新View的展示
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
// 从系统中移除展示的View
public void removeView(View view);
上面三个方法的View参数就是要添加的界面布局,后面的LayoutParams要求必须是WindowManager.LayoutParams,查看一下它的内部属性:
属性 | 说明 |
---|---|
x | View在屏幕的X轴展示位置 |
y | View在屏幕的y轴展示的位置 |
width | View在屏幕上展示的宽度 |
height | View在屏幕上展示的高度 |
gravity | View在屏幕上展示的重力设置,一般为了方便都会把自定义View放到左上位置 |
type | View所在Window的类型,主要分前面提到的三种类型 |
flags | 标识当前Window的交互效果和展示模式 |
flags标识有很多,这里只列举几个常用的标识:
- FLAG_NOT_FOCUSABLE:表示Window不需要获取焦点,也不需要接收各种输入事件,此标记会同时启用FLAG_NOT_TOUCH_MODAL,最终事件会直接传递给下层的具有焦点的Window
- FLAG_NOT_TOUCH_MODAL:在此模式下,系统会将当前Window区域以外的单击事件传递给底层的Window,当前Window区域以内的单击事件则自己处理 一般来说都需要开启此标记,否则其他Window将无法收到单击事件
- FLAG_SHOW_WHEN_LOCKED:此模式可以让Window显示在锁屏的界面上
- FLAG_DIM_BEHIND:在此模式下Window后面的其他Window都会变暗
- FLAG_NOT_TOUCHABLE:当前Window永远都不会接收Touch时间
- FLAG_LAYOUT_NO_LIMITS:允许Window里的内容扩展到屏幕之外
- FLAG_LAYOUT_IN_SCREEN:只允许Window里的内容展示在屏幕里面
实现过程
首先是尺子的实现,需要用到自定义控件来实现,尺子分为横向和竖向两种展示方式,尺子方向可以使用自定义属性来实现,在attrs.xml文件中定义尺子的方向属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RulerView">
<attr name="orientation" format="enum">
<enum value="0" name="horizontal" />
<enum value="1" name="vertical" />
</attr>
</declare-styleable>
</resources>
接下来在RulerView的构造函数中解析这个属性,同时用户可以通过接口修改属性值。
public RulerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (attrs != null) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RulerView);
int value = array.getInt(R.styleable.RulerView_orientation, 0);
orientation = value == 0;
array.recycle();
}
setBackgroundColor(getResources().getColor(R.color.transparent));
// 初始化画笔工具
paint = new Paint();
paint.setColor(getResources().getColor(R.color.colorAccent));
paint.setDither(true);
paint.setAntiAlias(true);
paint.setTextSize(30);
}
// 用户可以根据需要旋转当前的尺子方向
public void rotate() {
this.orientation = !orientation;
requestLayout();
}
接下来就是确定自定义尺子的宽高,横向展示的长为屏幕宽度,高度是80dp,竖向尺子宽度80dp,高度屏幕高度。宽高确认之后就开始根据长宽画尺子的刻度与刻度值,由于横向竖向原理类似,这里只展示横向画尺子代码。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int height = 0, width = 0;
if (orientation) { // 如果是横向尺子
width = CommonUtils.getScreenWidth();
height = CommonUtils.dp2px(80);
} else {
width = CommonUtils.dp2px(80);
height = CommonUtils.getScreenHeight();
}
setMeasuredDimension(width, height);
mTmpRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (orientation) {
drawHorizontalRuler(canvas);
} else {
drawVerticalRuler(canvas);
}
}
private void drawHorizontalRuler(Canvas canvas) {
// 画尺子外框
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(mTmpRect, paint);
// 每5个像素为一小格,每5个小格为一个小组,每两个小组也就是10个小格为一个大组
for (int i = 0; i < getMeasuredWidth(); i++) {
if (i % 100 == 0) {
canvas.drawText(String.valueOf(i), i - 20, getMeasuredHeight() / 2, paint);
}
if (i % 50 == 0) { // 画大组分隔
// 画左边的刻度
canvas.drawLine(i, 0, i, LARGE_GROUP_DIVIDER_HEIGHT, paint);
// 画右边的刻度
canvas.drawLine(i, getMeasuredHeight(), i,getMeasuredHeight() - LARGE_GROUP_DIVIDER_HEIGHT, paint);
} else if (i % 25 == 0) { // 画小组分隔
// 画左边的刻度
canvas.drawLine(i, 0, i, SMALL_GROUP_DIVIDER_HEIGHT, paint);
// 画右边的刻度
canvas.drawLine(i, getMeasuredHeight(), i,getMeasuredHeight() - SMALL_GROUP_DIVIDER_HEIGHT, paint);
} else if (i % 5 == 0) { // 画小格分隔
// 画左边的刻度
canvas.drawLine(i, 0, i, UNIT_DIVIDER_HEIGHT, paint);
// 画右边的刻度
canvas.drawLine(i, getMeasuredHeight(), i,getMeasuredHeight() - UNIT_DIVIDER_HEIGHT, paint);
}
}
}
尺子的样式定义完成之后就需要把尺子放到屏幕上去,首先在AndroidManifest文件中添加android.permission.SYSTEM_ALERT_WINDOW系统窗口权限申请,同时还需要在代码中做6.0+版本的动态权限申请。接下来在RulerWindow中封装WindowManager对RulerView的操作,首先是初始化展示的布局参数。
public class RulerWindow {
private WindowManager windowManager;
private WindowManager.LayoutParams layoutParams;
private RulerView ruler;
public RulerWindow() {
this.windowManager = (WindowManager) RulerApplication.getContext().getSystemService(Context.WINDOW_SERVICE);
this.ruler = new RulerView(RulerApplication.getContext());
layoutParams = new WindowManager.LayoutParams();
ruler.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
layoutParams.x = CommonUtils.dp2px(50);
layoutParams.y = CommonUtils.dp2px(50);
layoutParams.width = ruler.getMeasuredWidth();
layoutParams.height = ruler.getMeasuredHeight();
layoutParams.gravity = Gravity.TOP | Gravity.LEFT;
layoutParams.format = PixelFormat.RGBA_8888;
layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
initRuler();
}
....
}
里面的参数在前面的接口说明中都已经解释的很清楚了,这里不再赘述。最后的initRuler方法主要是设置用户触摸操作,原理是窗口的x,y坐标值变化和用户手指的坐标值变化做同步,然后在调用WindowManger.updateView更新窗口的最新布局参数。
private void initRuler() {
ruler.setOnTouchListener(new View.OnTouchListener() {
private int lastX;
private int lastY;
@Override
public boolean onTouch(View v, MotionEvent event) {
int x = (int) event.getRawX(), y = (int) event.getRawY();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - lastX;
int deltaY = y - lastY;
layoutParams.x += deltaX;
layoutParams.y += deltaY;
// 修改窗口坐标值
windowManager.updateViewLayout(ruler, layoutParams);
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_UP:
break;
}
return false;
}
});
}
接着封装尺子的展示隐藏操作,其实还是调用WindowManger的addView和removeView操作实现。尺子的横竖样式切换和前面的移动类似只不过修改的是尺子的宽高属性。
public void show() {
try {
windowManager.addView(ruler, layoutParams);
} catch (Exception e) {
e.printStackTrace();
}
}
public void hide() {
try {
windowManager.removeView(ruler);
} catch (Exception e) {
e.printStackTrace();
}
}
public void rotate() {
ruler.rotate();
ruler.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
layoutParams.x = CommonUtils.dp2px(50);
layoutParams.y = CommonUtils.dp2px(50);
layoutParams.width = ruler.getMeasuredWidth();
layoutParams.height = ruler.getMeasuredHeight();
windowManager.updateViewLayout(ruler, layoutParams);
}
接下来需要在Service中启动尺子工具,自定义的Service如下,第一次启动的时候调用展示功能,以后每次用户再启动就做转向操作。
public class RulerService extends Service {
private boolean first = true;
RulerWindow rulerWindow;
public RulerService() {
rulerWindow = new RulerWindow();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (first) {
rulerWindow.show();
first = false;
} else {
rulerWindow.rotate();
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
rulerWindow.destroy();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
像素尺子的实现整体代码请查看源码