一、Button背景问题
这是一张普通的.png图片(非点9图片)。
我想要将它作为button的背景图片,但是button或高或宽,或大或小,背景图片就会相应的缩放。
可以看到上图中的几张图片的四个角都已经有了明显的锯齿,而四条边(直线部分)都很光滑,图片中间部分也看不到模糊的痕迹。那有没有办法让图片缩放时四个角不变模糊,也就是四个角不随图片整体做放缩,这样的话不论button或高或宽,或大或小,背景图片都不会变模糊?
二、如何解决这个问题可以将图片划分为九个区域,四个角、四条边和中间区域。
四个角不做放缩,当图片要水平拉长时,只让上下绿块以及中间紫块水平拉长;当图片要垂直长高时,只让左右绿块以及中间紫块垂直长高;这样四角都不会变化,图片就不会出现锯齿。
来看看Android中NinePatch类的定义:
NinePatch类可以绘制一个九宫格式的图片。它的四个角不会缩放;四个边会沿着一个轴的方向缩放。中间的区域会在两个轴上都缩放。本质上,就是允许你创建按照你定义的方式缩放的图片。
三、NinePatch图片的工作原理和使用如何将一个普通的PNG图片划分为九宫格并变成NinePatch图呢?
可以在普通的PNG图片周围额外增加1像素宽的边界,在上边界和左边界画上黑色的线段,根据这两条线段就可以将图片划分为九个区域。
看看Android中的定义:
Android中NinePatchDrawable是一个包含额外1像素宽边界的标准PNG图像,它以.9.png为后缀,保存在工程的res/drawable/目录下。
这个边界是用来确定图像的可伸缩区域和内容区域。你可以在左边和上边的线上画一个或多个黑色的1个像素指出可伸缩的部分(你可能需要很多可伸缩区域)。
还可以在图像的右边和下边画一条可选的drawable区域。如果View设置NinePath图片为背景并且含有Text,它将自行伸缩以使所有的Text在右线与底部线确定的的区域中(如果有的话)。
总之,左边跟顶部的线来确定哪些区域的像素允许在伸缩时被复制。底部与右边的线用来定义一个相对的区域,View的内容就放入其中。
.9图片的使用与普通png图片的使用方法一样。
四、使用draw9patch.bat制作NinePatch图draw9patch.bat可以很容易的通过一个所见即所得(WYS|WYG)的图片编辑器来制作NinePatch图。
下面是一个便捷指南。
a) sdk\tools\draw9patch.bat双击打开。
b) 将PNG图片拖放到这个工具的窗口中(或者通过File->Open 9-patch... 来选择文件)。
工作台的左边窗格是绘制区域,在里面可以通过绘制边界上的黑色线段确定可延伸的宫格和内容区域。右边窗格是预览区域,从中你可以预览图形的拉伸。
c) 在1个像素宽的边界里点击,绘制线条来定义可延伸宫格以及(可选的)内容区域。点击右键(或者按住Shift并点击)取消之前画的线。
d) 完成后,选择File > Save 9-patch...,图片将以.9.png 文件名保存。
注意: 打开一个普通的PNG文件(*.png) ,会额外添加一个1像素宽的边界。打开一个九宫格文件(*.9.png)将不会添加边界,因为已经存在。
其他的一些功能:
- l 缩放Zoom: 调整图片大小;
- l 宫格比例Patch scale: 调整预览视图中图像的比例;
- l 显示锁定区域Show lock: 使不可画区域在鼠标移动到该区域上时显示出来;
- l 显示宫格Show patches: 预览这个绘图区中的可延伸宫格(粉红色代表一个可延伸宫格);
- l 显示内容Show content: 预览视图中的高亮内容区域(紫色部分);
- l 显示坏宫格Show bad patches: 在宫格区域四周增加一个红色边界,这可能会在图像被延伸时产生人工痕迹。如果你消除所有的坏宫格,延伸视图的视觉一致性将得到维护(不太明白)。
首先想要说明一下,这个Demo例子是从eoeAndroid上面Download下来的,本文里只是解析,学习一下实现原理。从昨天开始就想分析下,一直拖到今天,不到5点,睡不着了(当然不是因为这个技术问题),就趁着早晨把他写下来吧,多有不足,请多多原谅。
现在【csdn2012博客之星】评选活动正在火热进行中,有幸成为候选人,在这里也给自己拉拉票,我的投票地址:http://vote.blog.csdn.net/item/blogstar/aomandeshangxiao。为什么那么关注这个投票呢,因为我想,如果能够有幸得奖的话,把奖品送给一个人,我想这几天的心绪不宁多少可能跟她有些关系,所以,如果你看到这篇文章,希望能得到你的祝福!当然,我也不会进行道德绑架,你不投给我票我也不会画圈圈诅咒你,不投给我,你也是可以参加这次投票活动的,为你欣赏的博主投上一票,有可能还会获得活动奖品。谨记:一个人只能投10票(好吧,这段文字我会保留到投票活动结束,谢谢大家,请投一票给我)。
下面开始正题 ,先看下程序运行是图片:
然后你可以拖动圆形菜单外面项到圆形菜单中:
开始正式的代码解析:
主Activity的onCreate:
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); mNewView = new NewView(getApplicationContext(), 300, 200, 150); setContentView(mNewView); new Thread(new myThread()).start(); }
里面自定义了一个NewView类,继承自View,就是我们第一张图显示的内容,然后又起了一个线程:
class myThread implements Runnable { public void run() { while (!Thread.currentThread().isInterrupted()) { Message message = new Message(); switch (mNewView.getReturn()) { case 1: message.what = 0x101; break; case 2: message.what = 0x102; break; case 3: message.what = 0x103; break; case 4: message.what = 0x104; break; case 5: message.what = 0x105; break; case 6: message.what = 0x106; break; case 7: message.what = 0x107; break; case 8: message.what = 0x108; break; case 9: message.what = 0x109; break; } QSA.this.myHandler.sendMessage(message); try { Thread.sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }
这个线程就是监听图片有滑动的时候,通过Handler发送一个消息到UI线程,进行相对应的操作:
Handler myHandler = new Handler() { public void handleMessage(Message msg) { // newView.getReturn = -1; switch (msg.what) { case 0x101: break; case 0x102: break; case 0x103: mIntent = new Intent(getApplicationContext(), ShowPic.class); startActivity(mIntent); break; case 0x104: break; case 0x105: break; case 0x106: break; case 0x107: mIntent = new Intent(getApplicationContext(), RunLed.class); startActivity(mIntent); break; case 0x108: break; case 0x109: break; } super.handleMessage(msg); } };
其主要内容在与NewView类,下面我们看下这个类:
public class NewView extends View { //返回值,用于调用者获取newView状态 protected static int mGetReturn = -1; private Paint mPaint = new Paint(); // stone列表 private BigStone[] mStones; int mode = NONE; static final int NONE = 0; //拖动 static final int DRAG = 1; //用于双指视图缩放 static final int ZOOM = 2; //存放数据的数组 private BigStone[] mMenus; private BigStone[] mAddMenus = new BigStone[MENUS]; // 数目 private static int STONE_COUNT = 5; private static int MENUS = 4; // 圆心坐标 private float mPointX = 0, mPointY = 0; private int flagwai = 0; private int flag = 0; // 半径 private int mRadius = 0; // 每两个点间隔的角度 private int mDegreeDelta; private float maxX, maxY, minX, minY;
上面是一些全局变量,都加上了注释,比较好理解。
看它的构造方法:
public NewView(Context context, int px, int py, int radius) { super(context); //为圆心和半径赋值 mPointX = px; mPointY = py; mRadius = radius; setBackgroundResource(R.drawable.menubkground); //设置菜单项(为两个菜单项赋值) setupStones(); //计算坐标 computeCoordinates(); }
先看一下setupStone方法:
private void setupStones() { mStones = new BigStone[STONE_COUNT]; mMenus = new BigStone[MENUS]; BigStone stone; BigStone menus; //初始角度 int angle = 0; //每项之间相隔角度 mDegreeDelta = 360 / STONE_COUNT; //初始圆形菜单外的项 if (flagwai == 0) { for (int i = 0; i < MENUS; i++) { menus = new BigStone(); menus.bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.menu6 + i); menus.text = String.valueOf(1 + i); mMenus[i] = menus; } } //初始化圆形菜单项 for (int index = 0; index < STONE_COUNT; index++) { stone = new BigStone(); stone.angle = angle; stone.bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.menu1 + index); stone.text = String.valueOf(1 + index); //每一项的角度 angle += mDegreeDelta; mStones[index] = stone; } }
里面该加注释的地方,我都加了注释,比较简单,不多说了,里面用到了一个BigStone的实体类:
class BigStone { // 图片 Bitmap bitmap; // 角度 int angle; // x坐标 float x; // y坐标 float y; String text; // 是否可见 boolean isVisible = true; }
用于存储每一项的各个数据。
computeCoordinates,计算每一项的坐标:
private void computeCoordinates() { BigStone stone; BigStone menus; for (int index = 0; index < STONE_COUNT; index++) { stone = mStones[index]; stone.x = mPointX + (float) (mRadius * Math.cos(stone.angle * Math.PI / 180)); stone.y = mPointY + (float) (mRadius * Math.sin(stone.angle * Math.PI / 180)); } if (flag == 0) { for (int i = 0; i < MENUS; i++) { menus = mMenus[i]; switch (i) { case 0: menus.x = 300 * 1.8f; menus.y = 50; break; case 1: menus.x = 300 * 1.8f + 100; menus.y = 50; break; case 2: menus.x = 300 * 1.8f + 200; menus.y = 50; break; case 3: menus.x = 300 * 1.8f; menus.y = 150; break; //MENUS设置为4,下面这个应该是多余的 case 4: menus.x = 300 * 1.8f + 10 + 100; menus.y = 250; break; } } } }
设置好各项参数,就应该绘制:
@Override public void onDraw(Canvas canvas) { Paint paint = new Paint(); paint.setAntiAlias(true); paint.setColor(Color.WHITE); paint.setStyle(Paint.Style.FILL); paint.setAlpha(0x30); if (change == 0) { //绘制中心小圆 canvas.drawCircle(mPointX, mPointY, mRadius - 80, paint); } paint.setStyle(Paint.Style.FILL); paint.setAntiAlias(true); paint.setColor(Color.BLUE); paint.setAlpha(0x30); if (change == 0) { canvas.drawCircle(mPointX, mPointY, mRadius + 41, paint); // 大圆 } if (change == 1) { //满足一个条件不绘制圆盘菜单,绘制矩形菜单。 canvas.drawRect(800, 240, 0, 140, paint); } //绘制每一个外部菜单项 for (int i = 0; i < MENUS; i++) { if (!mMenus[i].isVisible) continue; drawMenus(canvas, mMenus[i].bitmap, mMenus[i].x, mMenus[i].y); } //绘制每一个园内菜单 for (int index = 0; index < STONE_COUNT; index++) { if (!mStones[index].isVisible) continue; drawInCenter(canvas, mStones[index].bitmap, mStones[index].x, mStones[index].y, mStones[index].text); } }
先绘制小圆,然后是大圆:
绘制菜单项时,调用了drawMenus和drawInCenter方法:
void drawMenus(Canvas canvas, Bitmap b, float x, float y) { canvas.drawBitmap(b, x - b.getWidth() / 2, y - b.getHeight() / 2, null); // 图标 } void drawInCenter(Canvas canvas, Bitmap bitmap, float left, float top, String text) { canvas.drawText(text, left, top, mPaint); canvas.drawBitmap(bitmap, left - bitmap.getWidth() / 2, top - bitmap.getHeight() / 2, null); }
下面就是触发事件的处理:
@Override public boolean dispatchTouchEvent(MotionEvent e) { dumpEvent(e); switch (e.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: //判断选中的是哪个点 for (int i = 0; i < STONE_COUNT; i++) { if (e.getX() >= mStones[i].x - 20 && e.getX() <= mStones[i].x + 40 && e.getY() >= mStones[i].y && e.getY() <= mStones[i].y + 40) { if (i < 5) { mGetReturn = Integer.valueOf(mStones[i].text); } else { mGetReturn = Integer.valueOf(mStones[i].text) + 5; } Toast.makeText(getContext(), String.valueOf(mGetReturn), Toast.LENGTH_SHORT).show(); } } //把mode设为拖动 mode = DRAG; return true; case MotionEvent.ACTION_POINTER_DOWN: //获取两点间的距离 oldDist = spacing(e); if (oldDist > 100f) { mode = ZOOM; } return true; case MotionEvent.ACTION_MOVE: //获取最大最小坐标 getMaxMin(e); if (mode == DRAG) { } else if (mode == ZOOM) { //获取两点间的距离 // float newDist = spacing(e); // if (newDist > 100f) { // /* // * if (change == 0) { change = 1; // * // * resetStonesAngle(e.getX(), e.getY()); // * computeCoordinates(); invalidate(); } // */ // } } int a = 0; for (int i = 0; i < MENUS; i++) { if (e.getX() > mMenus[i].x - 40 && e.getX() < mMenus[i].x + 40 && e.getY() > mMenus[i].y - 40 && e.getY() < mMenus[i].y + 40) { mMenus[i].x = e.getX(); mMenus[i].y = e.getY(); flag = 1; computeCoordinates(); //重新绘制 postInvalidate(); //从外面添加到圆形菜单中 if (e.getX() < maxX && e.getX() > minX && e.getY() < maxY && e.getY() > minY) { if (mMenus[i].isVisible) { for (int j = 0; j < MENUS; j++) { if (mAddMenus[j] == null && a == 0) { mAddMenus[j] = mMenus[i]; a = 1; } } STONE_COUNT++; mDegreeDelta = 360 / STONE_COUNT; mStones = new BigStone[STONE_COUNT]; flagwai = 1; int angle = 0; BigStone stone; for (int index = 0; index < STONE_COUNT; index++) { stone = new BigStone(); if (index < 5) { stone.bitmap = BitmapFactory .decodeResource(getResources(), R.drawable.menu1 + index); stone.text = String.valueOf(1 + index); } else { stone.bitmap = mAddMenus[index - 5].bitmap; stone.text = mAddMenus[index - 5].text; } stone.angle = angle; angle += mDegreeDelta; mStones[index] = stone; } //把添加到圆内菜单项中的外部Item设为不可见,是不是可以考虑移除? mMenus[i].isVisible = false; } } break; } } if (e.getX() < maxX && e.getX() > minX && e.getY() < maxY && e.getY() > minY) { if (e.getX() < maxX - 80 && e.getX() > minX + 81 && e.getY() < maxY - 80 && e.getY() > minY + 81) { mPointX = e.getX(); mPointY = e.getY(); } //重新设置每个item的角度 resetStonesAngle(e.getX(), e.getY()); //重新设置每一个item的坐标 computeCoordinates(); //重绘 invalidate(); } break; case MotionEvent.ACTION_UP: break; case MotionEvent.ACTION_POINTER_UP: mode = NONE; break; } return super.dispatchTouchEvent(e); }
先看以下事件:
- MotionEvent.ACTION_DOWN:在第一个点被按下时触发
- MotionEvent.ACTION_UP:当屏幕上唯一的点被放开时触发
- MotionEvent.ACTION_POINTER_DOWN:当屏幕上已经有一个点被按住,此时再按下其他点时触发。
- MotionEvent.ACTION_POINTER_UP:当屏幕上有多个点被按住,松开其中一个点时触发(即非最后一个点被放开时)。
- MotionEvent.ACTION_MOVE:当有点在屏幕上移动时触发。值得注意的是,由于它的灵敏度很高,而我们的手指又不可能完全静止(即使我们感觉不到移动,但其实我们的手指也在不停地抖动),所以实际的情况是,基本上只要有点在屏幕上,此事件就会一直不停地被触发。
举例子来说:当我们放一个食指到屏幕上时,触发ACTION_DOWN事件;再放一个中指到屏幕上,触发ACTION_POINTER_DOWN事件;此时再把食指或中指放开,都会触发ACTION_POINTER_UP事件;再放开最后一个手指,触发ACTION_UP事件;而同时在整个过程中,ACTION_MOVE事件会一直不停地被触发。
逐个事件分析一下:case MotionEvent.ACTION_DOWN: //判断选中的是哪个点 for (int i = 0; i < STONE_COUNT; i++) { if (e.getX() >= mStones[i].x - 20 && e.getX() <= mStones[i].x + 40 && e.getY() >= mStones[i].y && e.getY() <= mStones[i].y + 40) { if (i < 5) { mGetReturn = Integer.valueOf(mStones[i].text); } else { mGetReturn = Integer.valueOf(mStones[i].text) + 5; } Toast.makeText(getContext(), String.valueOf(mGetReturn), Toast.LENGTH_SHORT).show(); } } //把mode设为拖动 mode = DRAG; return true;
判断点击的是哪一项,然后弹出一个Toast提示。
case MotionEvent.ACTION_POINTER_DOWN: //获取两点间的距离 oldDist = spacing(e); if (oldDist > 100f) { mode = ZOOM; } return true;
多点触发,这里只是算了一下距离,没做什么处理,如有需要,可以添加一些功能代码。里面用到了spacing方法:
private float spacing(MotionEvent event) { float x = event.getX(0) - event.getX(1); float y = event.getY(0) - event.getY(1); return FloatMath.sqrt(x * x + y * y); }
该方法用于获取两点间的距离。
move:
case MotionEvent.ACTION_MOVE: //获取最大最小坐标 getMaxMin(e); if (mode == DRAG) { } else if (mode == ZOOM) { //获取两点间的距离 // float newDist = spacing(e); // if (newDist > 100f) { // /* // * if (change == 0) { change = 1; // * // * resetStonesAngle(e.getX(), e.getY()); // * computeCoordinates(); invalidate(); } // */ // } } int a = 0; for (int i = 0; i < MENUS; i++) { if (e.getX() > mMenus[i].x - 40 && e.getX() < mMenus[i].x + 40 && e.getY() > mMenus[i].y - 40 && e.getY() < mMenus[i].y + 40) { mMenus[i].x = e.getX(); mMenus[i].y = e.getY(); flag = 1; computeCoordinates(); //重新绘制 postInvalidate(); //从外面添加到圆形菜单中 if (e.getX() < maxX && e.getX() > minX && e.getY() < maxY && e.getY() > minY) { if (mMenus[i].isVisible) { for (int j = 0; j < MENUS; j++) { if (mAddMenus[j] == null && a == 0) { mAddMenus[j] = mMenus[i]; a = 1; } } STONE_COUNT++; mDegreeDelta = 360 / STONE_COUNT; mStones = new BigStone[STONE_COUNT]; flagwai = 1; int angle = 0; BigStone stone; for (int index = 0; index < STONE_COUNT; index++) { stone = new BigStone(); if (index < 5) { stone.bitmap = BitmapFactory .decodeResource(getResources(), R.drawable.menu1 + index); stone.text = String.valueOf(1 + index); } else { stone.bitmap = mAddMenus[index - 5].bitmap; stone.text = mAddMenus[index - 5].text; } stone.angle = angle; angle += mDegreeDelta; mStones[index] = stone; } //把添加到圆内菜单项中的外部Item设为不可见,是不是可以考虑移除? mMenus[i].isVisible = false; } } break; } } if (e.getX() < maxX && e.getX() > minX && e.getY() < maxY && e.getY() > minY) { if (e.getX() < maxX - 80 && e.getX() > minX + 81 && e.getY() < maxY - 80 && e.getY() > minY + 81) { mPointX = e.getX(); mPointY = e.getY(); } //重新设置每个item的角度 resetStonesAngle(e.getX(), e.getY()); //重新设置每一个item的坐标 computeCoordinates(); //重绘 invalidate(); } break;
里面就是各种判断,然后重新计算角度、坐标等,最后重新绘制。
里面有getMaxMin方法:
private void getMaxMin(MotionEvent e) { float tempx; float tempy; for (int i = 0; i < STONE_COUNT; i++) { for (int j = 0; j < STONE_COUNT; j++) { if (mStones[i].x < mStones[j].x) { tempx = mStones[i].x; mStones[i].x = mStones[j].x; mStones[j].x = tempx; } if (mStones[i].y < mStones[j].y) { tempy = mStones[i].y; mStones[i].y = mStones[j].y; mStones[j].y = tempy; } } } maxX = mStones[STONE_COUNT - 1].x; minX = mStones[0].x; maxY = mStones[STONE_COUNT - 1].y; minY = mStones[0].y; }
通过重新排序,获取最大最小坐标点。
里面的计算角度方法:
private void resetStonesAngle(float x, float y) { int angle = computeCurrentAngle(x, y); for (int index = 0; index < STONE_COUNT; index++) { mStones[index].angle = angle; angle += mDegreeDelta; } } private int computeCurrentAngle(float x, float y) { float distance = (float) Math .sqrt(((x - mPointX) * (x - mPointX) + (y - mPointY) * (y - mPointY))); int degree = (int) (Math.acos((x - mPointX) / distance) * 180 / Math.PI); if (y < mPointY) { degree = -degree; } return degree; }
好吧,就先写到这吧,饿了,先吃点东西。一会把代码上传上去,先吃点东西,最后,再给自己拉个票:
http://vote.blog.csdn.net/item/blogstar/aomandeshangxiao,写那么多博客,总有一个对你有用吧,没有功劳也有苦劳,没有苦劳也有疲劳啊,给投一票吧!
最后源代码下载地址:http://download.csdn.net/detail/aomandeshangxiao/4857216
因为CSDN的图片需要各种上传,比较麻烦,所以做成了PDF,大家直接下看即可:
阅读器链接
http://www.docin.com/p-421265393.html