继续上次的DEMO。不知道有没有人留意到那个扭曲的菜单呢?这就是Web OS中很有趣的小彩虹菜单。不知道有没有人想过这样的菜单如何实现呢?其实,菜单主要分两部分,一个是每个菜单item的位置布局变化和背景图片变化。首先说说菜单背景吧!背景有两个特点,第一是图片场景模糊,第二是图片扭曲。先说,图片模糊如何处理吧。
private Bitmap getBitmap(int width, int height){//获取背景图片Bitmap
Bitmap bitmap = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bitmap);
Paint paint = new Paint();
paint.setColor(Color.parseColor("#86000000"));
paint.setStyle(Paint.Style.FILL);
BlurMaskFilter filter = new BlurMaskFilter(30f,BlurMaskFilter.Blur.NORMAL);
paint.setMaskFilter(filter);
Rect rect = new Rect(-30,0,width+30,height);
c.drawRect(rect,paint);
return bitmap;
}
其中,设置画笔属性Paint.SetMaskFilter就可以设置模糊滤镜。一张带有模糊效果的图片就诞生了。在这里我还另外一种模糊特效,就是毛玻璃效果:
if (VERSION.SDK_INT > 16) {
Bitmap bitmap = sentBitmap.copy(sentBitmap.getConfig(), true);
final RenderScript rs = RenderScript.create(context);
final Allocation input = Allocation.createFromBitmap(rs, sentBitmap, Allocation.MipmapControl.MIPMAP_NONE,
Allocation.USAGE_SCRIPT);
final Allocation output = Allocation.createTyped(rs, input.getType());
final ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
script.setRadius(radius /* e.g. 3.f */);
script.setInput(input);
script.forEach(output);
output.copyTo(bitmap);
return bitmap;
}
对于这种图片特效还有非常之多,我就不展开分析了。
说完图片模糊效果就该说说图片扭曲效果了。图形扭曲很常见,比如哈哈镜,就是一种扭曲成像。汽车的后视镜,也是一种图片扭曲效果。再比如我们用的全景照片的在浏览也是。全景照片怎么说都是一张线性图片,如何把它立体化呈现在我们眼中呢?其实就是应用了一个凸透镜效果,把原有的线性图片组织成一个圆展示出来的。是一个图片扭曲的经典案例。废话不多说,图片扭曲主要引用了一个方法:
Canvas.drawBitmapMash(Bitmap:bit,Width:int,Height:int,MeshVerts:int[],VertOffset:int,Colors:int[],ColorOffset:int,paint:Paint);这个方法不仅可以实现图像位置扭曲还可以实现颜色扭曲。现在说一个图片扭曲的原理吧。
如上两图,现在正常的Bitmap进行网格编号,标出每个点的实际坐标数据,用一个数组保存起来,然后对所有的点的坐标进行扭曲运算,比如图二。扭曲后的坐标点发生了变化,绘制出来的bitmap也随之发生变化。实现视图扭曲的效果。至于颜色扭曲同理可得。
不知道有没有人留意到,“小彩虹菜单”并非单一的扭曲,它是中间肥厚,两边细的一个扭曲,而且随波浪形。没错,这是一个复合的余弦函数波合成图。假设余弦函数f(X) = a * Cos(b* (x+c)+d)+e;
设最高峰函数为f1(x),最低峰函数为f2(x)。因为f1(x),f2(x)
图像的最高峰在同一个坐标上可以得出,(b1*(x+c1)+d1) = (b2*(x+c2)+d2);假设b1 = b2,c1 = c2,d1 = d2
可得,最大值max = (a1+e1)-(a2+e2);最小值min = (-a1+e1)-(-a2+e2)
最后方程组解方程,我就不详细解答了。
说了这么多,上代码:
public class MeshBitmapDrawable extends Drawable{
private MeshHelper helper;
private Bitmap bitmap;
private Paint paint;
public MeshBitmapDrawable(@NonNull Bitmap bitmap, @NonNull MeshHelper helper){
this.bitmap = bitmap;
this.helper = helper;
paint = new Paint();
paint.setAntiAlias(true);
}
public MeshHelper getMeshHelper(){
return helper;
}
@Override
public void draw(Canvas canvas) {
canvas.save();
canvas.translate(helper.getTanslationX(),helper.gettTanslationY());
canvas.drawBitmapMesh(bitmap,helper.getMeshWidth(),helper.getMeshHeight(),helper.getMeshVerts(),helper.getVertOffset(),helper.getColors(),helper.getColorOffset(),paint);
canvas.restore();
}
@Override
public void setAlpha(int alpha) {
paint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
paint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return paint.getAlpha();
}
public interface MeshHelper{
public int getMeshWidth();
public int getMeshHeight();
public float[] getMeshVerts();
public void warp(float ...values);
public int getVertOffset();
public void restore();
public int[] getColors();
public int getColorOffset();
public float getTanslationX();
public float gettTanslationY();
public void setTanslation(float tanslationX,float tanslationY);
public float getTackPositionY(float x,float positionX,int row,float smalltall);
}
}
public class SimpleMeshHelper implements MeshBitmapDrawable.MeshHelper{ private final int MESHWIDTH = 50; private final int MESHHEIGHT = 10; private float minGap = 40; private float maxGap = 80; private final int COUNT = (MESHWIDTH+1) * (MESHHEIGHT+1); private float[] verts = new float[COUNT * 2]; private float[] orig = new float[COUNT * 2]; private float tanslationX; private float tanslationY; private int bitmapWidth = 1; public SimpleMeshHelper(int bitmapWidth,int bitmapHeight){ if(bitmapHeight > 0) this.bitmapWidth = bitmapWidth; int index = 0; for(int i = 0;i <= MESHHEIGHT;i++){//初始化坐标数组 float fy = bitmapHeight * i / MESHHEIGHT; for(int j = 0;j <= MESHWIDTH;j++){ float fx = bitmapWidth * j / MESHWIDTH; orig[index * 2 + 0] = verts[index * 2 + 0] = fx; orig[index * 2 + 1] = verts[index * 2 + 1] = fy; index ++; } } } public void setGap(float maxGap,float minGap){//设置两条曲线的最大和最小差 this.maxGap = maxGap; this.minGap = minGap; } @Override public int getColorOffset() { return 0; } @Override public int getMeshWidth() { return MESHWIDTH; } @Override public int getMeshHeight() { return MESHHEIGHT; } @Override public float[] getMeshVerts() { return verts; } @Override public void restore() { System.arraycopy(orig,0,verts,0,verts.length); } @Override public void warp(float ...values) {//变形 if(values == null || values.length <= 0) return; float x = values[0];//最高点X坐标 float t = values.length > 1 ? values[1] : minGap;//下面线条的最高值 int index = 0; for(int i = 0;i <= MESHHEIGHT;i++){//运算变化后的坐标 for(int j = 0;j <= MESHWIDTH;j++){ verts[index * 2 + 1] = calculate(x,orig[index * 2 + 0],i,t); index ++; } } } /* *正弦波运算 * @params offsetX X轴偏移量 * @params x 实时最高点的X坐标 * @params row 网格层数 * @params t 最下面曲线的高度最大值 * @return 返回对应网格Y坐标 */ private float calculate(float offsetX,float x,int row,float t){ row = row >= MESHHEIGHT ? MESHHEIGHT : row; float minT = t >= minGap ? minGap : t; float syncCap = (minGap-minT)/minGap*(maxGap-minGap)+minGap; float a = ((maxGap-syncCap+2*minT)/2-minT)/MESHHEIGHT*(MESHHEIGHT-row)+minT; float b = ((maxGap+syncCap)/2) /MESHHEIGHT*(MESHHEIGHT-row); float result = (maxGap+minT) - ((a * (float)Math.cos((Math.PI/bitmapWidth)*(x-offsetX)))+b); return result; } @Override public float getTackPositionY(float x, float positionX, int row, float smalltall) {//获取轨道数据 return calculate(x,positionX,row,smalltall); } @Override public int getVertOffset() { return 0; } @Override public int[] getColors() { return null; } @Override public void setTanslation(float x, float y) {//设置绘画偏移量 tanslationX = x; tanslationY = y; } @Override public float gettTanslationY() { return tanslationY; } @Override public float getTanslationX() { return tanslationX; } }
在这里,我并没有在Drawable中写死逻辑,承接上文所述,拔插效果。给扩展留下更大的空间,也便于维护和重用。
其实,“小彩虹菜单”的主要东西就到这里了。至于item菜单的布局排列,可以实现的方法可以很多种,但一定都走不出轨迹的运算。轨迹运算弄明白了,这也不是问题了。
在这里强调一下绘画优化问题,在“小彩虹”菜单中,绘制并不复杂。如果遇到类似复杂的动态UI的绘制,优化的必要性就非常强了。关于绘制优化主要有几点
一,绘制运算能用int类型的就不用float类型,能用float类型的就不用double类型
二,运算函数能用API中的方法就不要自己写,因为api中的方法的运算是通过底层C实现的,效率会比java高,内存 占用也小,特别是繁杂的运算。
三,如果运算复杂度过大,绘制特耗时的可以选择用异步处理,首先考虑SurfaceView和GLSurfaceView。
四,绘制刷新尽可能使用局部绘制,这样会大大缩减绘制事件,减少开销。
五,在设置控件背景的时候慎重考虑,可以不设的话尽量不设。
在android的绘制机制中,没绘制一次的单元事件是16ms,如果超出这个时间,界面就可能会卡,失帧。所以在界面绘制优化的时候可以考虑调试一下耗时,逐渐调整至16ms以内。
在这里,我想说一个观点。有些人一开始就想很多优化的地方开始折腾,但我觉得,一起这样不如把效果弄出来了再去细细讨论如何优化的问题。如果一开始就想得很完美,做起来就很纠结,缚手缚脚,非常被动。
后续:http://blog.csdn.net/rj113/article/details/66585474