恭喜发财! — 手把手教你仿造一个qq下拉抢红包 Android自定义view

最后更新于:2022-04-01 10:01:46

### 猴年猴赛雷啊各位,今天没吃药我感觉自己萌萌哒! qq和微信和支付宝红包大战,不知道各位的战绩是多少嘞? 反正我qq抢到的都是气泡。因为太不爽,所以自己写一个下拉抢红包自己玩(自己跟自己玩)。 先来看效果图。这个…… 呃~~ -**__**-” 。。有点丑 是低仿。  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaeab10494.jpg) 转载请注明出处:[http://blog.csdn.net/wingichoy/article/details/50662592](http://blog.csdn.net/wingichoy/article/details/50662592) ### 学习完本篇博客你能获得到的知识 1. 正确的获得view的大小 2. listview的下拉header 3. 自定义字体 4. 自己添加监听器 ## 废话不多说,快跟我来一起动手 首先来跟我打造一个自定义的listview 新建一个类,继承自listview,这里需要来重写一下他的overScrollBy()方法。 ~~~ public class HBListView extends ListView { //header显示的图片 private MyImageView mImageView; private Context mContext; //抢到红包时候的监听器 private OnSuccessListener mOnSuccessListener; @Override protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { Log.e("wing", "deltaY:" + deltaY + " scrollY:" + scrollY); return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent); } ~~~ 先来介绍一下重要参数的意思 > deltaX,Y是指速度值,也就是你手指滑动的瞬时速度。  > scrollX,Y 水平和数值方向上的变化量  > isTouchEvent 表示手指是否在触摸状态 观察log可以轻易看到他们与手机触摸的关系。大家自己试验一下就可以。 接着给listview加一个header ~~~ mListView = (HBListView) findViewById(R.id.lv); final View header = LayoutInflater.from(this).inflate(R.layout.view_header,null); mListView.addHeaderView(header); ~~~ 其实这个header就是个图片。 我们刚才从listview的监听上面看到了偏移量。所以可以根据偏移量来动态改变图片的大小。 ~~~ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <com.wingsofts.hongbao.MyImageView android:id = "@+id/imageView" android:scaleType="centerCrop" android:layout_width="match_parent" android:layout_height="0dp" android:src="@drawable/bg" /> </LinearLayout> ~~~ 因为header刚开始是隐藏的,所以这里高度设置为0. 这里来一个小插曲。。不知道大家有没有在activity中获取view的高度呢。是不是有时候只能拿到0。对于这个问题呢。正确的获取view宽高有如下几种解决方案。 > 1.onWindowsFocusChanged()  > 2.view.postRunnable() 将一个runnable投递到消息队列尾部。  > 3.ViewTreeObserver 这里采用第三种方法,在OnGlobalLayoutListener中 调用listview的changSize方法。。 ~~~ header.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { mListView.changeSize(image); header.getViewTreeObserver().removeGlobalOnLayoutListener(this); } }); ~~~ 这里看看listview的changsize方法,其实只是将header的引用传来而已..这是因为之前在这里获取高度,后来又去掉了。。偷懒没有改方法名 ~~~ public void changeSize(MyImageView imageView) { mImageView = imageView; } ~~~ 拿到了图片之后,理所当然是根据手指来改变图片的大小啦。 还记得onScrollBy里面的几个参数吗,我们只需要在 (触摸的时候&&下滑的时候) 改变图片的大小就可以了。  下滑的状态可以根据log得知为deltaY<0. ~~~ @Override protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { Log.e("wing", "deltaY:" + deltaY + " scrollY:" + scrollY); //给图片一个最大值 if (mImageView.getHeight() < 300) { //是触摸状态 以及 是下滑状态 if (isTouchEvent && deltaY < 0) { //动态改变imageView的大小 mImageView.getLayoutParams().height += Math.abs(deltaY); mImageView.requestLayout(); } } return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent); } ~~~ 这样便可以下拉了,现在的效果是这样的。  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaeab8206c.jpg) 咦。。是可以下拉了。但是没有回弹,怎么办! 还能怎么办 写一个就是了。思路就是用一个动画,来不停地改变header高度。 ### ObjectAnimator的使用 他的使用方法很简单,他通过反射调用set方法来改变view的属性然后发生动画。这里直接上例子,首先重写一下imageview,添加一个setHeight()方法. ~~~ public class MyImageView extends ImageView { private Paint mPaint; public void setHeight(int height) { getLayoutParams().height = height; requestLayout(); } } ~~~ 然后在使用ObjectAnimator的ofInt方法,获取到一个ObjectAnimator,这里ofXXX具体看参数类型。 ~~~ //获取mImageView的setHeight方法,数值从当前图片的高度逐渐到0,产生动画 private void closeHeader() { ObjectAnimator oa = ObjectAnimator.ofInt(mImageView, "height", mImageView.getHeight(), 0); oa.start(); } ~~~ 这时候关闭动画就做完啦。想要关闭的时候 只需要在action_up调用closeHeader()方法即可.现在是这样的:  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaeabb73e5.jpg) ~~~ @Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_UP: closeHeader(); break; } ~~~ 咦,有没有似曾相识的赶脚,没错!!这特么的不就是下拉刷新么。 好了本篇博客到此结束!! ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaeabe36ac.jpg) 额。。你先把手里的刀放下w(゚Д゚)w!!!  刚才结束的是下拉刷新的博客,现在继续写抢红包的博客。。。 思路就是用一个变量来保存连刷的次数,再用随机数判断是否抽中红包。  给MyImageView一个public的属性次数 ~~~ public class MyImageView extends ImageView { public int mTime; } ~~~ 然后只要在ACTION_UP的时候,判断随机数有没有抽中,决定次数是否累加: ~~~ @Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_UP: int ran = (int) (Math.random() * 10); //设置中奖概率 if (ran > 1) { //如果没中次数累加 mImageView.mTime++; closeHeader(); } else { //否则为中奖 mImageView.mTime = 0; if (mOnSuccessListener != null) { mOnSuccessListener.onSuccess(); } closeHeader(); } break; } return super.onTouchEvent(ev); } ~~~ 逻辑是不是粉简单! 聪明的你已经看到了抢红包成功的回调。这个等等说,先说拿到了次数以后怎么做。 拿到了次数之后理所应道就是把文字画上去啦~~ 这里为了方便就给了一个固定的位置。因为数字大小跟文字不一样,所以把字符串分了三串来画.. ~~~ public int mTime; private Typeface mTypeFace; private float mTxtHLength; private float mTxtRLength; private String txtH = "连刷"; private String txtR = "次,加油!"; public MyImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mPaint = new Paint(); mPaint.setColor(Color.YELLOW); mMidPaint = new Paint(); mMidPaint.setColor(Color.YELLOW); mTypeFace = Typeface.createFromAsset(context.getAssets(), "ifont.ttf"); mMidPaint.setTypeface(mTypeFace); mPaint.setTypeface(mTypeFace); //设置文字大小 mPaint.setTextSize(50); //设置数字大小 mMidPaint.setTextSize(100); //测量头文字的长度 mTxtHLength = mPaint.measureText(txtH); //测量尾文字的长度 mTxtRLength = mPaint.measureText(txtR); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); String count = mTime + ""; //数字的长度 float countLength = mMidPaint.measureText(count); //总体长度 float totalLength = mTxtRLength + countLength; //画在正中间 float start = getMeasuredWidth() / 2 - totalLength / 2; canvas.drawText(txtH, 100, getMeasuredHeight() / 2, mPaint); canvas.drawText(count, start + countLength / 2, getMeasuredHeight() / 2, mMidPaint); canvas.drawText(txtR, start + countLength / 2 + mTxtRLength / 2, getMeasuredHeight() / 2, mPaint); } ~~~ 这样就把次数提示画上去了!来看看效果:  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaeac09be8.jpg)  这个…… 呃~~ -**__**-” 是画上去了,但是 ### 字!体!好!丑! 没事没事,别着急。。我们换一个字体就是了。 ### Paint字体的改变 > 1.首先下载一个字体 放到assets文件夹下  > 2.获取到字体的引用  > 3.给画笔设置字体 ~~~ //改变字体,就是这么轻松自如~~ mTypeFace = Typeface.createFromAsset(context.getAssets(), "ifont.ttf"); mMidPaint.setTypeface(mTypeFace); mPaint.setTypeface(mTypeFace); ~~~ 看效果图:  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaeac26322.jpg) 吼吼吼~~~ 有点感觉了。 ### 监听器的添加 先来回过头来,大家知道View有setOnClickListener.. 那么我们也来加一个红包成功监听器吧。  在ListView里面增加一个内部接口,添加一个set方法 ~~~ private OnSuccessListener mOnSuccessListener; interface OnSuccessListener { void onSuccess(); } public void setOnSuccessListener(OnSuccessListener onSuccessListener) { mOnSuccessListener = onSuccessListener; } ~~~ 在ACTION_UP成果逻辑里面调用onSuccess()… 这就是监听器。。简单吧~就是个回调 现在在MainActivity里使用这个抢红包listView吧~  设置一个监听器,里面写你想要的事件,中几千万的随便写,总比气泡好,, ~~~ mListView.setAdapter(new ArrayAdapter(this,android.R.layout.simple_list_item_1,datas)); mListView.setOnSuccessListener(new HBListView.OnSuccessListener() { @Override public void onSuccess() { AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); builder.setMessage("恭喜中奖!抽到了疼逊聊天气泡!").setNegativeButton("确认", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } }).show(); } }); ~~~ 这样就能出现文章顶部预览图的效果了: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaeab10494.jpg) 如果你喜欢我的博客,请继续关注我,求赞求顶~求吐槽 本项目地址:[打开链接](https://github.com/githubwing/QQHongBao) ———————————–华丽丽的分割线———————————— ### 效果优化 处于追求完美的心态,群友 小情歌 对代码做了一些优化,主要有 > 1.修复上滑也显示红包的bug  > 2.优化动画事件,在松手的过程中如果继续ACTION_DOWN则放弃动画  > 3.增加视差特效 修改其实也不复杂 具体看这一段代码: ~~~ @Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: x1 = ev.getX(); y1 = ev.getY(); if (oa != null) { if (oa.isRunning()) { oa.cancel(); } } if (mImageView.getHeight() == 0) { mImageView.mTime = 0; } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: x2 = ev.getX(); y2 = ev.getY(); if (y1 - y2 > 0) { b = false; } else if (y2 - y1 > 0) { b = true; } int ran = (int) (Math.random() * 100); Log.e("wing", ran + ""); if (b) { //往下滑 if (ran > 3) { mImageView.mTime++; closeHeader(); } else { mImageView.mTime = 0; if (mOnSuccessListener != null) { mOnSuccessListener.onSuccess(); } closeHeader(); } } break; } return super.onTouchEvent(ev); } ~~~ 相信大家都看得懂,这里就不一一说明了
';

一个炫字都不够??!!!手把手带你打造3D自定义view

最后更新于:2022-04-01 10:01:44

分享一则最近流行的笑话:  最新科学研究表明:寒冷可以使人保持年轻,楼下的王大爷表示虽然今年已经60多岁了,但是仍然冷的跟孙子一样。 呃。好吧,这个冬天确实有点冷,在广州活生生的把我这个原生北方人,冻成一条狗。(研究表明:寒冷可以让人类基因突变。。。。) 好了不扯了。前些日子有朋友让我写博客来分析一下这个仿MIUI的时钟,从中学到了一些炫酷效果的实现。  [https://github.com/AvatarQing/MiClockView](https://github.com/AvatarQing/MiClockView) 感谢原作者的开源精神! 那么是啥3D效果呢,先来看看效果图,额。。有好多个:  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea9a6d34.jpg)  这里写图片描述  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea9ec1fc.jpg) 其实后两个都是png来的。。 转载请注明出处:[http://blog.csdn.net/wingichoy/article/details/50590051](http://blog.csdn.net/wingichoy/article/details/50590051) 那么请问,看到图形的变换,你想到了什么?  没错!就是Matrix。  关于Matrix你可以到爱哥的博客了解到及其详细的讲解(谢谢爱哥!)。 下面我们就来研究一下如何用矩阵,实现这个3d的效果。 首先新建自定义view类。 ~~~ public class TDView extends View { private Paint mPaint; private int mCenterX; private int mCenterY; public TDView(Context context, AttributeSet attrs) { super(context, attrs); mPaint = new Paint(); } } ~~~ 然后在圆心画一个圆出来 ~~~ @Override protected void onDraw(Canvas canvas) { mCenterX = getWidth() / 2; mCenterY = getHeight() / 2; canvas.drawCircle(mCenterX,mCenterY,100,mPaint); } ``` 现在是这样的: ![这里写图片描述](http://img.blog.csdn.net/20160126235856787) 我们知道,处理一个图片的时候(切错)可以使用矩阵来处理,同时处理X,Y的话可以使用Camera类,camera可以生成一个指定效果的矩阵。直接来看用法: private Camera mCamera; private Matrix mMatrix; mMatrix = new Matrix(); mCamera = new Camera(); 在onDraw里 把camera给旋转一下,并把生成的矩阵给一个矩阵。再把矩阵应用到canvas,看一下效果。 ~~~ ~~~ mMatrix.reset(); mCamera.save(); mCamera.rotateX(10); mCamera.rotateY(20); mCamera.getMatrix(mMatrix); mCamera.restore(); //将矩阵作用于整个canvas canvas.concat(mMatrix); ``` ~~~ 这里写图片描述  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaeaa2fe75.jpg)  呃。。。确实是变形了。。但是好像不是我们想要的结果?  这是因为,矩阵的变换坐标总从左上角(0,0)开始。所以我们要把变换的坐标改为中心点,方法如下: ~~~ mMatrix.reset(); mCamera.save(); mCamera.rotateX(10); mCamera.rotateY(20); mCamera.getMatrix(mMatrix); mCamera.restore(); //改变矩阵作用点 mMatrix.preTranslate(-mCenterX, -mCenterY); mMatrix.postTranslate(mCenterX, mCenterY); canvas.concat(mMatrix); ``` 此时的效果看起来像是向左倾斜了: 这里写图片描述 ![这里写图片描述](http://img.blog.csdn.net/20160127001356605) 接下来让他跟随手指移动,重写onTouchEvent: ~~~ @Override  public boolean onTouchEvent(MotionEvent event) {  float x = event.getX();  float y = event.getY(); ~~~ int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_MOVE: { //这里将camera旋转的变量赋值 mCanvasRotateY = y; mCanvasRotateX = x; invalidate(); return true; } case MotionEvent.ACTION_UP: { //这里将camera旋转的变量赋值 mCanvasRotateY = 0; mCanvasRotateX = 0; invalidate(); return true; } } return super.onTouchEvent(event); } ``` ~~~ 哈哈。。看看是什么效果:  这里写图片描述  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaeaa40a0b.jpg) 什么鬼,怎么跟转硬币一样。 因为旋转的X,Y给的太大了呗。所以要约束一下。 定义一个旋转最大值 ~~~ private float mCanvasMaxRotateDegree = 20; ~~~ 再用percent思想(前面博客有提到),来处理手指触摸点和这个度数变化的关系: ~~~ private void rotateCanvasWhenMove(float x, float y) { float dx = x - mCenterX; float dy = y - mCenterY; float percentX = dx / mCenterX; float percentY = dy /mCenterY; if (percentX > 1f) { percentX = 1f; } else if (percentX < -1f) { percentX = -1f; } if (percentY > 1f) { percentY = 1f; } else if (percentY < -1f) { percentY = -1f; } mCanvasRotateY = mCanvasMaxRotateDegree * percentX; mCanvasRotateX = -(mCanvasMaxRotateDegree * percentY); } ``` 最后将TouchEvent里面的ACTION_MOVE 调用此函数即可。 此时,完整的代码如下: ~~~ ~~~ private int mCenterX; private int mCenterY; private float mCanvasRotateX = 0; private float mCanvasRotateY = 0; private float mCanvasMaxRotateDegree = 20; private Matrix mMatrix = new Matrix(); private Camera mCamera = new Camera(); private Paint mPaint; public TDView(Context context) { super(context); } public TDView(Context context, AttributeSet attrs) { super(context, attrs); mPaint = new Paint(); mCanvasMaxRotateDegree = 20; } @Override protected void onDraw(Canvas canvas) { mCenterX = getWidth() / 2; mCenterY = getHeight() / 2; rotateCanvas(canvas); canvas.drawCircle(mCenterX, mCenterY, 100, mPaint); } private void rotateCanvas(Canvas canvas) { mMatrix.reset(); mCamera.save(); mCamera.rotateX(mCanvasRotateX); mCamera.rotateY(mCanvasRotateY); mCamera.getMatrix(mMatrix); mCamera.restore(); mMatrix.preTranslate(-mCenterX, -mCenterY); mMatrix.postTranslate(mCenterX, mCenterY); canvas.concat(mMatrix); } @Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: { rotateCanvasWhenMove(x, y); return true; } case MotionEvent.ACTION_MOVE: { rotateCanvasWhenMove(x, y); invalidate(); return true; } case MotionEvent.ACTION_UP: { mCanvasRotateY = 0; mCanvasRotateX = 0; invalidate(); return true; } } return super.onTouchEvent(event); } private void rotateCanvasWhenMove(float x, float y) { float dx = x - mCenterX; float dy = y - mCenterY; float percentX = dx / mCenterX; float percentY = dy /mCenterY; if (percentX > 1f) { percentX = 1f; } else if (percentX < -1f) { percentX = -1f; } if (percentY > 1f) { percentY = 1f; } else if (percentY < -1f) { percentY = -1f; } mCanvasRotateY = mCanvasMaxRotateDegree * percentX; mCanvasRotateX = -(mCanvasMaxRotateDegree * percentY); } ~~~ } 简简单单100行代码,实现了3D效果的view: ![这里写图片描述](http://img.blog.csdn.net/20160127002811543) 接下来做什么呢? 当然是把这个效果加到我们的自定义view里面! 比如,把我的PanelView加上这个效果: 这里写图片描述 ![这里写图片描述](http://img.blog.csdn.net/20160127003536598) 哗!瞬间高大上! 那么,你要不要跟我趁热来一发自定义view? 说搞就搞! 在原有类上进行修改 给一个好看的底色,画一条线 ~~~ mBgColor = Color.parseColor("#227BAE"); canvas.drawLine(mCenterX,100,mCenterX,130,mPaint); ``` ~~~ 这里写图片描述  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaeaa6a688.jpg)  嗯。不错 有条线了。微调下间距,旋转画布,画出整个圆形来: ~~~ //保存坐标系 canvas.save(); for (int i = 0; i < 120; i++) { canvas.rotate(3,mCenterX,mCenterY); canvas.drawLine(mCenterX, 150, mCenterX, 170, mPaint); } //恢复坐标系 canvas.restore(); ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaeaa799a6.jpg)  嗯。。看起来想点样子了。 接下来调整透明度。 ~~~ canvas.save(); for (int i = 0; i < 120; i++) { //根据i调整透明度alpha mPaint.setAlpha(255-(mAlpha * i/120)); canvas.drawLine(mCenterX, 150, mCenterX, 170, mPaint); canvas.rotate(3,mCenterX,mCenterY); } canvas.restore(); ~~~ 这里写图片描述  哈哈。。有没有点意思呢。。  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaeaa8ccd6.jpg)  我们画个圆球上去!画之前先ctrl + alt + m 把之前画弧的方法提出来。  画一个紧挨着的圆 ~~~ private void drawCircle(Canvas canvas) {  mPaint.setAlpha(255);  canvas.drawCircle(mCenterX,213,10,mPaint);  }  ~~~ 这里写图片描述  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaeaa9e9bc.jpg)  不错不错,给点动态效果吧,让圆点跟着我们触摸的地方走。怎么做呢。。 当然还是旋转画布了!  这里需要注意的是触摸点与12点钟方向形成的夹角计算。画图分析一下  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaeaaaf8f9.jpg)  可以看到 我们只需要调用Math.atan方法即可算出a的弧度,再将其转换为角度即可,在进行3D旋转之前,旋转画布: ~~~ protected void onDraw(Canvas canvas) { canvas.drawColor(mBgColor); mCenterX = getWidth() / 2; mCenterY = getHeight() / 2; Log.e("wing",alpha+""); canvas.rotate((float) alpha,mCenterX,mCenterY); alpha = Math.atan((mTouchX-mCenterX)/(mCenterY-mTouchY)); alpha = Math.toDegrees(alpha); if(mTouchY>mCenterY){ alpha = alpha+180; ~~~ 现在看一下效果:  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaeaac6571.jpg)  这里写图片描述 效果出来了,但是还美中不足呀。 因为中间太空了,所以这个3D效果看起来有点奇怪。那就给他中间加点东西吧! 比如一个指针。 ~~~ private void drawPath(Canvas canvas) { mPath.moveTo(mCenterX,223); mPath.lineTo(mCenterX-30,mCenterY); mPath.lineTo(mCenterX,2*mCenterY-223); mPath.lineTo(mCenterX+30,mCenterY); mPath.lineTo(mCenterX,233); mPath.close(); canvas.drawPath(mPath,mPaint); mPaint.setColor(Color.parseColor("#55227BAE")); canvas.drawCircle(mCenterX,mCenterY,20,mPaint); } ~~~ 最后大功告成!!! 看效果!  这里写图片描述  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea9a6d34.jpg) 如果你喜欢我的博客,请点击关注。欢迎评论~~ 下一篇! 手把手教你做一个qq下拉抢红包:打开链接 本项目地址:[https://github.com/githubwing/compassView](https://github.com/githubwing/compassView)
';

手把手带你画一个漂亮蜂窝view Android自定义view

最后更新于:2022-04-01 10:01:41

上一篇做了一个水波纹view  不知道大家有没有动手试试呢[点击打开链接](http://blog.csdn.net/wingichoy/article/details/50523713) 这个效果做起来好像没什么意义,如果不加监听回调 图片就能直接替代。写这篇博客的目的是锻炼一下思维能力,以更好的面多各种自定义view需求。 转载请注明出处:http://blog.csdn.net/wingichoy/article/details/50554058 本文是和代码同步写的。也就是说在写文章的时候才敲的代码。这样会显得文章有些许混乱。但是我想这样记录下来,一个自定义view的真正的制作过程,是一点一点,一步步跟着思路的改变,完善的。不可能一下子就做出一个完整的view。。技术也是这样,不可能一步登天。都是一步一步的积累。 另外,每一篇博客都是建立在之前博客的基础知识上的,如果你刚接触自定义view。可以来[说说自定义view简单学习的方式](http://blog.csdn.net/wingichoy/article/details/50483101)这里看我以前的文章。记录了我学习自定义view的过程,而且前几篇博客或多或少犯了一些错误。这里我并不想改正博文中的错误,因为些错误是大家经常会犯的,后来的博客都有指出这些错误,以及不再犯,这是一个学习的过程。所以我想把错误的经历记录下来。等成为高手 回头看看当年的自己是多么菜。。也会有成就感。。 老规矩效果图如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea8addcc.jpg) 首先画一个六边形,画之前来计算一下六边形的相关知识: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea8c26ef.jpg) 假设一个正六边形的边长为a  ,因为每个角都是120°  所以可得高为根号三a  ,如图所示。 有了这些信息我们就可以绘制一个六边形出来,如下: ~~~ float height = (float) (Math.sqrt(3)*mLength); mPath.moveTo(mLength/2,0); mPath.lineTo(0,height/2); mPath.lineTo(mLength/2,height); mPath.lineTo((float) (mLength*1.5),height); mPath.lineTo(2*mLength,height/2); mPath.lineTo((float) (mLength*1.5),0); mPath.lineTo(mLength/2,0); mPath.close(); ~~~ 绘制效果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea8d39f8.jpg) 然后将其根据一个偏移量进行平移,就可以用循环绘制出多个六边形 这里offset是偏移量,紧挨着的话应该是偏移一个六边形的宽,宽由上图可知为 a/2+a+a/2 即 2a;  ~~~ for(int i = 0 ; i < 3;i++) { int offset = mLength * 2 * i; mPath.moveTo(mLength / 2 + offset, 0); mPath.lineTo(0 + offset, height / 2); mPath.lineTo(mLength / 2 + offset, height); mPath.lineTo((float) (mLength * 1.5) + offset, height); mPath.lineTo(2 * mLength + offset, height / 2); mPath.lineTo((float) (mLength * 1.5)+offset, 0); mPath.lineTo(mLength / 2+offset, 0); mPath.close(); } ~~~ 发现效果如下 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea8e10bd.jpg) 这不对啊,很奇怪啊。。 底下空出来的一个三角形放不下我们的第二行啊。。 那么怎么办呢。。 加大offset!  加大多少呢。。 应该多增加一个边长。。这样就正好留空了。 来试试 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea8f1d7f.jpg) 现在来准备画第二行.... 发现我们之前path的坐标都是相对写死的。。 所以要回过头改一下,改成给定一个起点,就可以绘制出一个六边形,经过计算,得出下图 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea91087d.jpg) 这里a代表边长。 改完之后的代码是: ~~~ float height = (float) (Math.sqrt(3)*mLength); for(int i = 0 ; i < 3;i++) { //横坐标偏移量 int offset = mLength * 3 * i ; //左上角的x int x = mLength/2 + offset; int y = 0; //根据左上角一点 绘制整个正六边形 mPath.moveTo(x, y); mPath.lineTo(x -mLength/2, height / 2 + y); mPath.lineTo(x, height+y); mPath.lineTo(x + mLength, height +y); mPath.lineTo((float) (x + 1.5*mLength), height / 2+y); mPath.lineTo(x + mLength, y); mPath.lineTo(x, y); mPath.close(); } ~~~ 绘制出来的效果是一样的。但是方法以及变了。 然后来画第二行,第二行起点的path应该在这里 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea9226ad.jpg) 坐标是: 2a , height/2  这里的偏移量不变。 首先将画path的方法提取出来(as快捷键ctrl + alt + m) ~~~ //根据左上角一点 绘制整个正六边形 private void getPath(float height, float x, float y) { mPath.moveTo(x, y); mPath.lineTo(x -mLength/2, height / 2 + y); mPath.lineTo(x, height+y); mPath.lineTo(x + mLength, height +y); mPath.lineTo((float) (x + 1.5*mLength), height / 2+y); mPath.lineTo(x + mLength, y); mPath.lineTo(x, y); mPath.close(); } ~~~ 然后再给个循环,来绘制第二行的六边形 ~~~ for(int i = 0;i<2;i++){ float offset = mLength * 3 * i ; float x = mLength*2 + offset; float y = height/2; getPath(height,x,y); } canvas.drawPath(mPath,mPaint); ~~~ 得到如下的效果。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea932df1.jpg) 现在ondraw的全部代码如下: ~~~ @Override protected void onDraw(Canvas canvas) { mPaint.setColor(Color.parseColor("#FFBB33")); //正六边形的高 float height = (float) (Math.sqrt(3)*mLength); for(int i = 0 ; i < 3;i++) { //横坐标偏移量 float offset = mLength * 3 * i ; //左上角的x float x = mLength/2 + offset; float y = 0; getPath(height, x, y); } canvas.drawPath(mPath,mPaint); mPath.reset(); mPaint.setColor(Color.parseColor("#AA66CC")); for(int i = 0;i<2;i++){ float offset = mLength * 3 * i ; float x = mLength*2 + offset; float y = height/2; getPath(height,x,y); } canvas.drawPath(mPath,mPaint); } ~~~ 接下来对每行的个数进行一下控制。 ~~~ //每行的个数 private int mColumnsCount = 3; //行数 private int mLineCount = 3; ~~~ 对应的循环也改变,最外面套一个大循环,来控制多行绘制 ~~~ for (int j = 0; j < mLineCount; j++) { if(j%2 == 0) 绘制奇数行 else 绘制偶数行 } ~~~ 现在整个ondraw如下。 ~~~ @Override protected void onDraw(Canvas canvas) { //正六边形的高 float height = (float) (Math.sqrt(3) * mLength); for (int j = 0; j < mLineCount; j++) { if (j % 2 == 0) { mPaint.setColor(Color.parseColor("#FFBB33")); for (int i = 0; i < mColumnsCount; i++) { //横坐标偏移量 float offset = mLength * 3 * i; //左上角的x float x = mLength / 2 + offset; float y = j * height / 2; getPath(height, x, y); } canvas.drawPath(mPath, mPaint); mPath.reset(); } else { mPaint.setColor(Color.parseColor("#AA66CC")); for (int i = 0; i < mColumnsCount; i++) { float offset = mLength * 3 * i; float x = mLength * 2 + offset; float y = (height / 2) * j; getPath(height, x, y); } canvas.drawPath(mPath, mPaint); mPath.reset(); } } } ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea94394c.jpg) 好像颜色一样就不好看了。。那我们来动态改变一下颜色.. 添加一个属性list来存放color ~~~ private ArrayList<Integer> mColorList; ~~~ ~~~ mColorList = new ArrayList<>(); mColorList.add(Color.parseColor("#33B5E5")); mColorList.add(Color.parseColor("#AA66CC")); mColorList.add(Color.parseColor("#99CC00")); mColorList.add(Color.parseColor("#FFBB33")); mColorList.add(Color.parseColor("#FF4444")); ~~~ 在循环中,取出颜色值 ~~~ for (int j = 0; j < mLineCount; j++) { mPaint.setColor(mColorList.get(j)); ~~~ 效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea95307f.jpg) 嗯。。看起来像一点样子了。。。 给中间加点文字吧。。 先给每个蜂窝编号 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea962af3.jpg) 按上面的循环   j为行数  i为列数   **研究规律发现   编号等于 j*3 + i ** 我们有六边形左上角的坐标xy 可以轻易的计算出中心坐标 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea974574.jpg) 这些都有了。开一个list存放中间的文字: ~~~ //存放文字的list private ArrayList<String> mTextList ; ~~~ 在初始化的时候给添加点数据 ~~~ mTextList = new ArrayList<>(); for(int i =0;i<mLineCount*mColumnsCount;i++){ mTextList.add("wing "+i); } mTextPaint = new Paint(); mTextPaint.setTextSize(20); ~~~ 绘制文字: 这里要注意他和path的绘制顺序,如果path后绘制则会覆盖掉文字 ~~~ float txtLength = mTextPaint.measureText(mTextList.get(txtId)); canvas.drawText(mTextList.get(txtId),x+mLength/2-txtLength/2,y+height/2+5, mTextPaint); ~~~ 下面是全部的ondraw ~~~ @Override protected void onDraw(Canvas canvas) { //正六边形的高 float height = (float) (Math.sqrt(3) * mLength); for (int j = 0; j < mLineCount; j++) { mPaint.setColor(mColorList.get(j)); if (j % 2 == 0) { // mPaint.setColor(Color.parseColor("#FFBB33")); for (int i = 0; i < mColumnsCount; i++) { int txtId = j*3 +i; //横坐标偏移量 float offset = mLength * 3 * i; //左上角的x float x = mLength / 2 + offset; float y = j * height / 2; mPath.reset(); getPath(height, x, y); canvas.drawPath(mPath, mPaint); float txtLength = mTextPaint.measureText(mTextList.get(txtId)); canvas.drawText(mTextList.get(txtId),x+mLength/2-txtLength/2,y+height/2+5, mTextPaint); } } else { // mPaint.setColor(Color.parseColor("#AA66CC")); for (int i = 0; i < mColumnsCount; i++) { int txtId = j*3 +i; float offset = mLength * 3 * i; float x = mLength * 2 + offset; float y = (height / 2) * j; mPath.reset(); getPath(height, x, y); canvas.drawPath(mPath, mPaint); float txtLength = mTextPaint.measureText(mTextList.get(txtId)); canvas.drawText(mTextList.get(txtId),x+mLength/2-txtLength/2,y+height/2+5, mTextPaint); } } } } ~~~ 现在的效果图如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea8addcc.jpg) 好,那现在让他灵活一点。添加各种set方法,比如行数啊 列数啊  边长啊 文字内容啊 颜色啊之类的。 ~~~ /** * 设置列数 * @param mColumnsCount */ public void setColumnsCount(int mColumnsCount) { this.mColumnsCount = mColumnsCount; invalidate(); } /** * 设置行数 * @param mLineCount */ public void setLineCount(int mLineCount) { this.mLineCount = mLineCount; invalidate(); } /** * 设置文本数据 */ public void setTextList(ArrayList<String> textList) { mTextList.clear(); mTextList.addAll(textList); invalidate(); } /** * 设置颜色数据 * @param colorList */ public void setColorList(ArrayList<Integer> colorList) { mColorList.clear(); mColorList.addAll(colorList); invalidate(); } ~~~ 然后 你有没有忘记测量呢? 只要把最外面的矩形大小给他就行 ~~~ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); if(widthMode == MeasureSpec.AT_MOST){ widthSize = (int) ((3f*mColumnsCount+0.5f) *mLength); }else{ // throw new IllegalStateException("only support wrap_content"); } if(heightMode == MeasureSpec.AT_MOST){ heightSize = (int) ((mLineCount/2f +0.5f) * (Math.sqrt(3) * mLength)); }else{ // throw new IllegalStateException("only support wrap_content"); } setMeasuredDimension(widthSize,heightSize); } ~~~ 这下使用wrap_content 来看看view的大小: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea9897c7.jpg) 嗯。。测量也对着。。。   这里我只实现了wrap_content  大家可以以及扩展 让他支持EXACTLY 这样 一个蜂窝煤的view 就完成了。。。但是好像没鸟用的样子。。因为没有交互的话。。图片完全可以代替。所以这次就先遗留一个问题,事件的处理。其实逻辑也不是很复杂,就是判断触摸点 是否在Path内,如果action_up的时候在,分开编号,按照编号进行回调即可,这个问题,准备下篇博客解决,请大家继续关注我的博客 蟹蟹!。 本项目地址:[点击打开链接](https://github.com/githubwing/HoneycombView) 如果你觉得我写的还不错,请点击一下顶,继续关注我。谢谢!!
';

有坑?? 为何wing坠入PorterDuffXferMode的万丈深渊(PorterDuffXferMode深入试验)

最后更新于:2022-04-01 10:01:39

菜鸡wing遇敌PorterDuffXferMode,不料过于轻敌,应战吃力。随后与其大战三天三夜,三百余回合不分胜负。幸得 @咪咪控 相助,侥幸获胜。 关键字:PorterDuffXferMode  错误 不正确  不达到预期  bug 上一篇带来一个使用PorterDuffXferMode  做的 [水波纹loadingview](http://blog.csdn.net/wingichoy/article/details/50523713),中间遇到了点小困难。 (说人话)  PorterDuffXferMode总是不能按照效果图预期的效果执行。关于PorterDuffXferMode的错误显示是一个对初学者十分深的坑,到底是bug呢,还是有需要注意的地方呢。这里就跟随我 带上手电筒,去一探究竟。 转载请注明出处:http://blog.csdn.net/wingichoy/article/details/50534175 首先,大家都知道有一个图片: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea6b35c1.jpg) 然后,大部分时候 是看到了觉得很神奇,就跃跃欲试,尤其是src_in  和dstIn可以实现遮罩效果,例如圆角图片 圆形图片都用了这种模式。 于是就挨个测试各种模式是否生效,结果往往不能达到预期效果。我们来做个测试。 从最简单的开始: **1.直接在canvas上面绘制图形**: ~~~ @Override protected void onDraw(Canvas canvas) { //dst canvas.drawRect(20,20,80,80,mDstPaint); //src canvas.drawCircle(30,30,30,mSrcPaint); } ~~~ 原图效果是这样的: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea77adc4.jpg) 现在加一个mode上来,XOR ~~~ @Override protected void onDraw(Canvas canvas) { //dst canvas.drawRect(20,20,80,80,mDstPaint); mSrcPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR)); //src canvas.drawCircle(30,30,30,mSrcPaint); } ~~~ 跑起来的结果是这样的: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea78d8d1.jpg) WTF!!?? 这是什么鬼。不应该是相交部分消失吗。 网上说“硬件加速”对这个有影响,那么在构造器里关闭硬件加速试一下: ~~~ public TestView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mDstPaint = new Paint(); mSrcPaint = new Paint(); mDstPaint.setColor(Color.YELLOW); mSrcPaint.setColor(Color.BLUE); setLayerType(View.LAYER_TYPE_SOFTWARE, null); } ~~~ 运行的结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea79d8fe.jpg) 这下正常了。相交的部分消失了。 **结论1:硬件加速对PorterDuffXferMode有影响,使用前请关闭硬件加速。** 那么这下真的天下太平了吗?nonono~不要太天真,不然怎么能叫万丈深渊呢。 继续试验其他模式:  将模式改为SRC_IN ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea7aff26.jpg) WTF?????跟效果图根本不一致好吗!!!! 在试试DST_IN ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea7c35ce.jpg) 你确定你没有在逗我????  怎么是这个鬼东西。  (当时鼓捣了我三天四夜,一直在日狗,不过先别急,慢慢来。) 为什么一定要按照那个效果图来呢。。。 因为特么的那个图是官方的一个demo。。 那么我们就来看看这个demo长什么样! ~~~ package io.appium.android.apis.graphics; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.Xfermode; import android.os.Bundle; import android.view.View; public class Xfermodes extends GraphicsActivity { // create a bitmap with a circle, used for the "dst" image static Bitmap makeDst(int w, int h) { Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(bm); Paint p = new Paint(Paint.ANTI_ALIAS_FLAG); p.setColor(0xFFFFCC44); c.drawOval(new RectF(0, 0, w*3/4, h*3/4), p); return bm; } // create a bitmap with a rect, used for the "src" image static Bitmap makeSrc(int w, int h) { Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(bm); Paint p = new Paint(Paint.ANTI_ALIAS_FLAG); p.setColor(0xFF66AAFF); c.drawRect(w/3, h/3, w*19/20, h*19/20, p); return bm; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(new SampleView(this)); } private static class SampleView extends View { private static final int W = 64; private static final int H = 64; private static final int ROW_MAX = 4; // number of samples per row private Bitmap mSrcB; private Bitmap mDstB; private Shader mBG; // background checker-board pattern private static final Xfermode[] sModes = { new PorterDuffXfermode(PorterDuff.Mode.CLEAR), new PorterDuffXfermode(PorterDuff.Mode.SRC), new PorterDuffXfermode(PorterDuff.Mode.DST), new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER), new PorterDuffXfermode(PorterDuff.Mode.DST_OVER), new PorterDuffXfermode(PorterDuff.Mode.SRC_IN), new PorterDuffXfermode(PorterDuff.Mode.DST_IN), new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT), new PorterDuffXfermode(PorterDuff.Mode.DST_OUT), new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP), new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP), new PorterDuffXfermode(PorterDuff.Mode.XOR), new PorterDuffXfermode(PorterDuff.Mode.DARKEN), new PorterDuffXfermode(PorterDuff.Mode.LIGHTEN), new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY), new PorterDuffXfermode(PorterDuff.Mode.SCREEN) }; private static final String[] sLabels = { "Clear", "Src", "Dst", "SrcOver", "DstOver", "SrcIn", "DstIn", "SrcOut", "DstOut", "SrcATop", "DstATop", "Xor", "Darken", "Lighten", "Multiply", "Screen" }; public SampleView(Context context) { super(context); mSrcB = makeSrc(W, H); mDstB = makeDst(W, H); // make a ckeckerboard pattern Bitmap bm = Bitmap.createBitmap(new int[] { 0xFFFFFFFF, 0xFFCCCCCC, 0xFFCCCCCC, 0xFFFFFFFF }, 2, 2, Bitmap.Config.RGB_565); mBG = new BitmapShader(bm, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); Matrix m = new Matrix(); m.setScale(6, 6); mBG.setLocalMatrix(m); } @Override protected void onDraw(Canvas canvas) { canvas.drawColor(Color.WHITE); Paint labelP = new Paint(Paint.ANTI_ALIAS_FLAG); labelP.setTextAlign(Paint.Align.CENTER); Paint paint = new Paint(); paint.setFilterBitmap(false); canvas.translate(15, 35); int x = 0; int y = 0; for (int i = 0; i < sModes.length; i++) { // draw the border paint.setStyle(Paint.Style.STROKE); paint.setShader(null); canvas.drawRect(x - 0.5f, y - 0.5f, x + W + 0.5f, y + H + 0.5f, paint); // draw the checker-board pattern paint.setStyle(Paint.Style.FILL); paint.setShader(mBG); canvas.drawRect(x, y, x + W, y + H, paint); // draw the src/dst example into our offscreen bitmap int sc = canvas.saveLayer(x, y, x + W, y + H, null, Canvas.MATRIX_SAVE_FLAG | Canvas.CLIP_SAVE_FLAG | Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.FULL_COLOR_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG); canvas.translate(x, y); canvas.drawBitmap(mDstB, 0, 0, paint); paint.setXfermode(sModes[i]); canvas.drawBitmap(mSrcB, 0, 0, paint); paint.setXfermode(null); canvas.restoreToCount(sc); // draw the label canvas.drawText(sLabels[i], x + W/2, y - labelP.getTextSize()/2, labelP); x += W + 10; // wrap around when we've drawn enough for one row if ((i % ROW_MAX) == ROW_MAX - 1) { x = 0; y += H + 30; } } } } } ~~~ 一点一点看,截取onDraw里面的片段,这里 ~~~ canvas.drawBitmap(mDstB, 0, 0, paint); paint.setXfermode(sModes[i]); canvas.drawBitmap(mSrcB, 0, 0, paint); paint.setXfermode(null); ~~~ 他是画了两个bitmap。网上有人说好像只对bitmap生效,那到底是不是这样呢。我们来试验一下。 我们新定义一个canvas  再定义一个bitmap   现在bitmap上画出来src  然后将bitmap画到canvas上: ~~~ public TestView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mDstPaint = new Paint(); mSrcPaint = new Paint(); mDstPaint.setColor(Color.YELLOW); mSrcPaint.setColor(Color.BLUE); setLayerType(View.LAYER_TYPE_SOFTWARE, null); mSrcBitmap = Bitmap.createBitmap(50,50, Bitmap.Config.ARGB_8888); mCanvas = new Canvas(mSrcBitmap); } ~~~ ~~~ @Override protected void onDraw(Canvas canvas) { //dst canvas.drawRect(20,20,80,80,mDstPaint); //src // mSrcPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); mCanvas.drawCircle(25,25,25,mSrcPaint); canvas.drawBitmap(mSrcBitmap,0,0,null); } ~~~ 现在的效果是这样的: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea7d551e.jpg) 加一个XOR 试试。。 日了狗了!!!!!没反应啊,到底是什么鬼。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea7edd5f.jpg) 是不是两个都需要bitmap才可以呢,再创建一个dstBitmap和dstCanvas? ~~~ mDstBitmap = Bitmap.createBitmap(50,50, Bitmap.Config.ARGB_8888); mDstCanvas = new Canvas(mDstBitmap); ~~~ 加一个XOR 试试 ~~~ @Override protected void onDraw(Canvas canvas) { //dst mDstCanvas.drawRect(20,20,80,80,mDstPaint); canvas.drawBitmap(mDstBitmap,0,0,mDstPaint); //src mSrcPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR)); mSrcCanvas.drawCircle(25,25,25,mSrcPaint); canvas.drawBitmap(mSrcBitmap,0,0,mSrcPaint); } ~~~ 效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea8098dc.jpg) 终于他妈的出来了!!!!那其他效果呢? clear ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea81ae65.jpg) 一致的!!!!好激动有没有!!!!搞了4天 越来越接近结论了!!! **结论2:只有两个bitmap的时候,才可以生效。** 不要高兴太早。。如果坑到这里就完了,那还叫坑么。 继续试。。嗯 好多模式都是一致的。 直到!!!SRC_IN和DST_IN ,会发现。。。都消失了。 为毛呢?? 检查代码  发现 在往bitmap画圆之前就set了mode  这样会有影响 纠正 ~~~ @Override protected void onDraw(Canvas canvas) { //dst mDstCanvas.drawRect(20,20,80,80,mDstPaint); canvas.drawBitmap(mDstBitmap,0,0,mDstPaint); //src mSrcCanvas.drawCircle(25,25,25,mSrcPaint); //再画圆之后 设置mode mSrcPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR)); canvas.drawBitmap(mSrcBitmap,0,0,mSrcPaint); } ~~~ 发现 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea82df9a.jpg) 艹!!!!!!终于好了!!!!!!!!经过不懈努力!!!撒花!*★,°*:.☆\( ̄▽ ̄)/$:*.°★* 。  其实我们刚才bitmap的大小是一样的。 然后都是从0,0开始 完全覆盖了。 那么错开一点点 是什么效果呢,调整代码如下 ~~~ protected void onDraw(Canvas canvas) { //dst mDstCanvas.drawRect(20,20,80,80,mDstPaint); canvas.drawBitmap(mDstBitmap,20,20,mDstPaint); //src mSrcCanvas.drawCircle(25,25,25,mSrcPaint); mSrcPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); canvas.drawBitmap(mSrcBitmap,0,0,mSrcPaint); } ~~~ 效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea8421d4.jpg) 可以看有效果了!!!! 但是是一个什么鬼!!!  矩形缺角??加蓝色一点?? 这样看是很难看出效果的。。来调整一下bitmap的底色 和矩形的大小: 把两个bitmap的底色都画成灰色, 矩形不要占满画布 留出空间 ~~~ mDstCanvas.drawColor(Color.GRAY); ~~~ ~~~ @Override protected void onDraw(Canvas canvas) { //dst 黄色 mDstCanvas.drawRect(0,0,40,40,mDstPaint); canvas.drawBitmap(mDstBitmap,20,20,mDstPaint); //src 蓝色 mSrcCanvas.drawCircle(25,25,25,mSrcPaint); canvas.drawBitmap(mSrcBitmap,0,0,mSrcPaint); } ~~~ 效果如下。。  嗯 这样就是两个bitmap了。。很明了  bitmap的内容 位置, ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea851966.jpg) 然后再搞SRC_IN模式 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea8616cf.jpg) dst_in  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea872c07.jpg) 那么把bitmap底色去掉。 改成透明的呢? dst_in: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea886ee8.jpg) SRC_IN: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea89826c.jpg) **总结3:两个bitmap位置不完全重叠的效果如上,并不能总结出效果,要按实际效果来。** ---------------------------------------------------------------------------------------------------------华丽丽的分割线----------------------------------------------------------------------------------------- **最终大总结,如果想让PorterDuffXferMode按照预期Demo(或者效果图)的效果图像实现,必须满足以下条件:** 1、关闭硬件加速。 2、两个bitmap大小尽量一样。 3、背景色为透明色。 4、如果两个bitmap位置不完全一样,可能也是预期效果,只不过你看到的效果和你自己脑补的预期效果不一致。 最后想再说几句。鼓捣这个模式鼓捣了几乎一周,每天晚上下班都在搞。查了无数资料。但是好多不完整,甚至有一些误导性。所以为了避免后来者入坑。亲自试验,尽量总结。 如果有说的不正确的地方请及时向我提出。我会及时改正。 如果本文帮助到了你,请点一个顶,或者评论一下,蟹蟹!!!!
';

手把手教你画一个 逼格满满圆形水波纹loadingview Android

最后更新于:2022-04-01 10:01:37

才没有完结呢o( ̄︶ ̄)n 。大家好,这里是番外篇。 拜读了爱哥的博客,又学到不少东西。爱哥曾经说过: 要站在巨人的丁丁上。 那么今天,我们就站在爱哥的丁丁上来学习制作一款自定义view(开个玩笑,爱哥看到别打我)。 转载请注明出处:http://blog.csdn.net/wingichoy/article/details/50523713 上一篇 带领大家做了一款炫酷的loading动画view [手把手带你做一个超炫酷loading成功动画view](http://blog.csdn.net/wingichoy/article/details/50482271)  不知道大家跟着做了一遍没有呢? 在开始之前,首先来说说预备知识,这些知识在爱哥的博客上都有详细的介绍:[点我进入爱哥自定义view系列](http://blog.csdn.net/aigestudio/article/details/41316141) 效果图如下:  应用场景很多。。比如。。。内存占用百分比之类的 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea609a09.jpg) 预备的知识有: 1.贝塞尔曲线    如果你不了解,可以来这里进行基础知识储备:[神奇的贝塞尔曲线](http://blog.csdn.net/wingichoy/article/details/50492828)  2.Paint.setXfermode()  以及PorterDuffXfermode 千万不要被这个b的名字吓到,不熟悉看到可能会认为很难记,其实 只要站在巨人的丁丁上 还是很简单的。 好了 废话不多说 ,跟我一步步来做一个炫酷的view吧。 首先给一些属性,在构造器里初始化(不要再ondraw new东西不要再ondraw new东西不要再ondraw new东西不要再ondraw new东西) ~~~ //绘制波纹 private Paint mWavePaint;         private PorterDuffXfermode mMode = new PorterDuffXfermode(PorterDuff.Mode.XOR);//设置mode 为XOR //绘制圆 private Paint mCirclePaint; private Canvas mCanvas;//我们自己的画布 private Bitmap mBitmap; private int mWidth; private int mHeight; public WaveLoadingView(Context context) { this(context,null); } public WaveLoadingView(Context context, AttributeSet attrs) { this(context, attrs,0); } public WaveLoadingView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mWavePaint = new Paint(); mWavePaint.setColor(Color.parseColor("#33b5e5")); mCirclePaint = new Paint(); mCirclePaint.setColor(Color.parseColor("#99cc00")); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (widthMode == MeasureSpec.EXACTLY) { mWidth = widthSize; } if (heightMode == MeasureSpec.EXACTLY) { mHeight = heightSize; } setMeasuredDimension(mWidth, mHeight); mBitmap = Bitmap.createBitmap(300,300, Bitmap.Config.ARGB_8888); //生成一个bitmap mCanvas = new Canvas(mBitmap);//讲bitmp放在我们自己的画布上,实际上mCanvas.draw的时候 改变的是这个bitmap对象 } ~~~ 然后,我们给他绘制一点东西,用来介绍PorterDuffXfermode ~~~ @Override protected void onDraw(Canvas canvas) { mCanvas.drawCircle(100,100,50,mCirclePaint); mCanvas.drawRect(100,100,200,200,mWavePaint); canvas.drawBitmap(mBitmap,0,0,null); super.onDraw(canvas); } ~~~ 嗯,可以看到 是我们现在自己的画布上铺了一个bitmap(这里可以理解canvas为桌子  bitmap为画纸,我们在bimap上画画), 然后在bitmap上画了 一个圆,和一个矩形。最后把我们的mBitmap画到系统的画布上(显示到屏幕上),得到了以下效果。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea6758d6.jpg) 然后我们用setXfermode()方法给他设置一个mode,这里设置XOR。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea690dfd.jpg) 可以发现! 相交的地方消失了! 是不是很神奇。 在改一个mode 试试 ~~~ private PorterDuffXfermode mMode = new PorterDuffXfermode(PorterDuff.Mode.DST_OVER); ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea6a2042.jpg) 可以看到 圆形跑到了矩形上面来。  然后巨人给我们总结各个模式如了下图,这里给一个说明dst为先画的 src为后画的:. ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea6b35c1.jpg) 大家可以根据这个规律试一下。 现在,我们把圆和矩形重叠。模式去掉。 ~~~ protected void onDraw(Canvas canvas) { //dst mCanvas.drawCircle(150,150,50,mCirclePaint); // mWavePaint.setXfermode(mMode); //src mCanvas.drawRect(100,100,200,200,mWavePaint); canvas.drawBitmap(mBitmap,0,0,null); super.onDraw(canvas); } ~~~ 运行是这样的。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea6cb23a.jpg) 日了狗了!!我的圆怎么没了。。  其实圆是被覆盖掉了。 然后我们想实现一个效果,就是在圆的范围内,显示矩形的内容,该怎么做呢。自己照着图找找吧哈哈。 --------------------------------------------回归正题------------------------------------ 我们要实现的是一个圆形的水波纹那种loadingview。。首要就是实现这个水波纹。 这时候贝塞尔曲线就派上用场了。这里采用三阶贝塞尔, 不停地改变X 模拟水波效果。 ~~~ if (x > 50) { isLeft = true; } else if (x < 0) { isLeft = false; } <span style="white-space:pre"> </span>if (y > -50) { //大于-50是因为辅助点是50 为了让他充满整个屏幕             y--;         } if (isLeft) { x = x - 1; } else { x = x + 1; } mPath.reset(); mPath.moveTo(0, y); mPath.cubicTo(100 + x*2, 50 + y, 100 + x*2, y-50, mWidth, y);//前两个参数是辅助点 mPath.lineTo(mWidth, mHeight);//充满整个画布 mPath.lineTo(0, mHeight);//充满整个画布 mPath.close(); ~~~ 之后用mCanvas来绘制这个bitmap,要注意的是 绘制之前要清空mBitmap,不然path会重叠 ~~~ mBitmap.eraseColor(Color.parseColor("#00000000")); //dst  mCanvas.drawPath(mPath, mPaint); ~~~ ~~~ canvas.drawBitmap(mBitmap, 0, 0, null); postInvalidateDelayed(10); ~~~ 在最上面动态改变Y  通知重绘,现在的效果是这样的 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea6dd46f.jpg)  哈,水波效果出来了。   接着想办法让他画到一个圆形中。 首先绘制一个圆 ~~~ mCanvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2, mSRCPaint); ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea708097.jpg) 额。。有点海上日出的感觉(看太阳都是绿色!) 现在让我们回到刚才的问题,如何在dst的范围内绘制src呢。。。答案是。。SRC_IN 你找对了吗。添加模式 ~~~ //dst mCanvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2, mSRCPaint); mPaint.setXfermode(mMode); //src mCanvas.drawPath(mPath, mPaint); canvas.drawBitmap(mBitmap, 0, 0, null); ~~~ 运行 效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea72aeae.jpg) 咦 哈哈哈,是不是有点感觉了。如果不这样做 就需要考虑好多问题。动态计算沿着圆弧x,y坐标  计算arcTo的范围(我已经算出来了。。有兴趣的可以提。。没兴趣的话我就不写了。。) 完善一下,添加一个percent来代表进度,让y来根据percent动态改变 ~~~ y = (int) ((1-mPercent /100f) *mHeight); ~~~ 添加setPercent方法 ~~~ public void setPercent(int percent){ mPercent = percent; } ~~~ 画上百分比的文字。 ~~~ String str = mPercent + "%"; float txtLength = mTextPaint.measureText(str); canvas.drawText(mPercent + "%", mWidth / 2-txtLength/2, mHeight / 2, mTextPaint); ~~~ 然后配合seekBar。 效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea74d335.jpg) 最后改改字体大小  画笔透明度。 添加个背景图 就成了效果图上的效果。 本项目地址:[点击打开链接](https://github.com/githubwing/WaveLoadingView/tree/master)   求star 如果你觉得本博客还可以,那么求关注,点个顶,评个论咯。。以后还会有更多的文章等着你~
';

wing带你玩转自定义view系列(3)模仿微信下拉眼睛

最后更新于:2022-04-01 10:01:35

发现了爱神的自定义view系列,我只想说一个字:凸(艹皿艹 ) !!相见恨晚啊,早看到就不会走这么多弯路了 另外相比之下我这完全是小儿科。。所以不说了,这篇是本系列完结篇....我要从零开始跟随爱哥脚步去学自定义view了:[爱哥自定义view专题](http://blog.csdn.net/column/details/androidcustomview.html) 然后要说的就是  之前的博客都犯了很严重的错误,那就是不要在onDraw里new东西,不要在onDraw里new东西,不要在onDraw里new东西。重要的话说三遍。 上一篇介绍了[ qq未读消息提醒去除效果的简化实现](http://blog.csdn.net/wingichoy/article/details/50503630),不知道小伙伴们掌握的怎么样了。 转载请注明出处:http://write.blog.csdn.net/postedit/50503858 今天带给大家一个很熟悉的东西,当当当当,就是微信下拉眼睛的实现了。 先看效果图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea5afb45.jpg) 自评相似度  80%  哈哈=  = 。 用我们一贯的方法来剖析这个view。 首先 从内到外: 1.内部其实是两段弧,只不过在改变画笔的宽度。 2.中间是个圆,一直在改变透明度。 3.最外面是两条贝塞尔曲线(重点加难点)。 首先来画静态的眼睛。按照顺序,相信你已经轻车熟路: ~~~ mPaint.setStrokeWidth(10); canvas.drawArc(mRectF, 180, 10, false, mPaint); canvas.drawArc(mRectF, 205, 25, false, mPaint); //画圆圈 mPaint.setStrokeWidth(2); canvas.drawCircle(225, 225, 40, mPaint); canvas.drawPath(mPath, mPaint); ~~~ 这样就画出了眼珠的静态部分,然后根据我们的percent大法,分三个阶段**:1.弧变粗  2.圆圈透明度改变  3.贝塞尔区限 起点,终点,辅助点改变**。 那么将这三个阶段用同一个percent来控制  0-33为1阶段  33-66为2阶段 66-100为3阶段: 意外收获:当setStrokeWidth 为0时,实际上不是0. ~~~ if(mPercent<33) {//如果为1阶段,改变画笔的大小 float stroke = mPercent/3f; //用0-33 来控制0-10的变化 计算的方法 Log.e("wing","st" + stroke); if(stroke == 0.0){ //如果为0 则不绘制,这里用背景色解决 mPaint.setColor(Color.BLACK); }else { mPaint.setColor(Color.GRAY); } mPaint.setStrokeWidth(stroke); canvas.drawArc(mRectF, 180, 10, false, mPaint); canvas.drawArc(mRectF, 205, 25, false, mPaint); }else if(mPercent < 66) { //如果为2阶段 则画静态的1阶段 //画内部 mPaint.setStrokeWidth(10); canvas.drawArc(mRectF, 180, 10, false, mPaint); canvas.drawArc(mRectF, 205, 25, false, mPaint); //画圆圈 mPaint.setStrokeWidth(2); int alpha = (int) ((mPercent - 33f) / 33f * 255);//根据百分比去动态控制透明度的值、 mPaint.setAlpha(alpha); canvas.drawCircle(225, 225, 40, mPaint); }else ~~~ 以上都没什么好说的,现在来说最难的第三阶段(搞了我好久) 第三阶段全局用一个percent参数,由 66-100演变来的 ~~~ float percent = (mPercent-66)*3f/100; ~~~ 然后观察曲线的动态变化,是从顶点开始像两侧绘制、这时候很容易想到 根据百分比动态改变起点,终点的值,代码如下: ~~~ float mStartX = 225 -(225-115)*percent; float mEndX = 225+ (335-225)*percent; mTopPath.moveTo(mStartX ,175+(225-175)*percent); // Log.e("wing","start:"+(225 -(225-145)*percent) + " y:"+125+(225-125)*percent); mTopPath.quadTo(225, 175, mEndX, 175+(225-175)*percent ); canvas.drawPath(mTopPath, mPaint); ~~~ 然后你会惊奇的发现如下效果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea5cb1ea.jpg) 这是因为只改变了起终点,并没有改变辅助点的Y轴。那么辅助点到底应该怎么去改变呢,来看一张图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea5de2d0.jpg) 根据之前在  模仿360内存清理效果的研究里发现,  在辅助点x为线段一半的情况下, 弧的切点y轴也为辅助点y的一半。 所以得出 辅助点的Y变化应为: 175-50*percent 然后来改写贝塞尔曲线绘制代码: ~~~ float mStartX = 225 -(225-115)*percent;//贝塞尔区限的开始x坐标 float mEndX = 225+ (335-225)*percent;//贝塞尔区限的结束x坐标 mTopPath.moveTo(mStartX ,175+(225-175)*percent); // Log.e("wing","start:"+(225 -(225-145)*percent) + " y:"+125+(225-125)*percent); mTopPath.quadTo(225, 175-50*percent, mEndX, 175+(225-175)*percent );//辅助点的Y坐标动态改变 canvas.drawPath(mTopPath, mPaint); mTopPath.reset(); // // //画静态下边线 //// mPath.moveTo(145, 225); //// mPath.quadTo(225, 325, 305, 225); //// canvas.drawPath(mPath, mPaint); // mPath.moveTo(mStartX ,275-(275-225)*percent); mPath.quadTo(225, 275+50*percent, mEndX , 275-(275-225)*percent ); canvas.drawPath(mPath,mPaint); mPath.reset(); ~~~ 最后 给他一个setPercent方法: ~~~ public void setPercent(int percent){ mPercent = percent; invalidate(); } ~~~ 然后在MainActivity内 用seekbar动态改变他的percent值 即可达到我们想要的效果。 本篇难度较大,请读者动手认真练习,文中坐标可根据个人喜好改变。 本项目地址:[点击打开链接](https://github.com/githubwing/WeChatEye)      求star
';

wing带你玩转自定义view系列(2) 简单模仿qq未读消息去除效果

最后更新于:2022-04-01 10:01:32

上一篇介绍了贝塞尔曲线的简单应用 [仿360内存清理效果](http://blog.csdn.net/wingichoy/article/details/50500479) 这一篇带来一个  两条贝塞尔曲线的应用 : 仿qq未读消息去除效果。 转载请注明出处:http://blog.csdn.net/wingichoy/article/details/50503630 老规矩,先上效果图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea53ef5b.jpg) qq的未读消息去除很炫酷,其实就是用了两条贝塞尔曲线,我们按思路来,先来画两个圆,及两条贝塞尔曲线,辅助点为圆心y坐标的一半。我们把下面移动的圆,叫做mMoveCircle. ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea5541d7.jpg) 这样一画,就很简单明了了对不对。只要在拖动的时候 去改变辅助点的Y,和固定圆的半径, 就可以出来如效果图的效果。 那么重写onTouchEvent ~~~ @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_MOVE: mMoveCircleY = event.getY(); // mSupY = mCircleY; if (mMoveCircleY < lastY) { isUp = false; } else { isUp = true; } lastY = mMoveCircleY; invalidate(); break; case MotionEvent.ACTION_UP: if((mMoveCircleY-mCircleY)>mMoveCircleRadius*3){ Log.e("wing","超过"); isCanDraw = false; invalidate(); }else { Log.e("wing","没超过"); } } return true; } ~~~ 这里Action_move记录了最后一次移动和这次移动的值,来代表是往上滑还是往下滑,改变一个标记为,并且让mMoveCircleY改变为触摸的y坐标。 ACTION_UP主要记录 圆点离开的范围,如果超过这个范围,则代表消息提醒去除掉。 在ondraw里绘制贝塞尔曲线 ~~~ //左边的线 path.moveTo(mCircleX - mCircleRadius + mPaintStrokeWidth / 2, mCircleY); path.quadTo(mCircleX , mSupY, mCircleX - mMoveCircleRadius + mPaintStrokeWidth / 2, mMoveCircleY); canvas.drawPath(path, p); //右边的线 path.moveTo(mCircleX + mCircleRadius - mPaintStrokeWidth / 2, mCircleY); path.quadTo(mCircleX, mSupY, mCircleX + mMoveCircleRadius - mPaintStrokeWidth / 2, mMoveCircleY); canvas.drawPath(path, p); ~~~ 这样可以画成两条贝塞尔曲线,但是是空心的 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea5673eb.jpg) 那么我们想象办法,如何才能让他封闭呢。还记得绘制多边形的办法吗。。直接在两条贝塞尔曲线之间lineto就可以了,然后close; ~~~ path.moveTo(mCircleX - mCircleRadius + mPaintStrokeWidth / 2, mCircleY); path.quadTo(mCircleX , mSupY, mCircleX - mMoveCircleRadius + mPaintStrokeWidth / 2, mMoveCircleY); path.lineTo(mCircleX + mMoveCircleRadius, mMoveCircleY); path.quadTo(mCircleX, mSupY, mCircleX + mCircleRadius, mCircleY); path.lineTo(mCircleX - mCircleRadius, mCircleY); path.close(); canvas.drawPath(path, p); ~~~ 其实我们绘制的是这个效果,然后在把两个实心圆画上去,画笔style设置为fill 即可、 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea57713a.jpg) 封闭画笔,动态改变半径,绘制圆。 ~~~ p.setStyle(Paint.Style.FILL); ~~~ ~~~ if (isUp) { canvas.drawCircle(mCircleX, mCircleY, mCircleRadius--, p); canvas.drawCircle(mCircleX, mMoveCircleY, mMoveCircleRadius, p); } else { canvas.drawCircle(mCircleX, mCircleY, mCircleRadius++, p); canvas.drawCircle(mCircleX, mMoveCircleY, mMoveCircleRadius, p); } ~~~ 得到如下效果。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea588fdf.jpg) 最后,来判断是否超过一定下拉区域,如果超过 则圆点跟着手一起走 然后在手松开的时候消失。 ~~~ if ((mMoveCircleY-mCircleY)<mMoveCircleRadius*3) { Log.e("wing",mSupY-mCircleY+""); Path path = new Path(); //左边的线 path.moveTo(mCircleX - mCircleRadius + mPaintStrokeWidth / 2, mCircleY); path.quadTo(mCircleX , mSupY, mCircleX - mMoveCircleRadius + mPaintStrokeWidth / 2, mMoveCircleY); path.lineTo(mCircleX + mMoveCircleRadius, mMoveCircleY); path.quadTo(mCircleX, mSupY, mCircleX + mCircleRadius, mCircleY); path.lineTo(mCircleX - mCircleRadius, mCircleY); path.close(); canvas.drawPath(path, p); //右边的线 // path.moveTo(mCircleX + mCircleRadius - mPaintStrokeWidth / 2, mCircleY); // path.quadTo(mCircleX, mSupY, mCircleX + mMoveCircleRadius - mPaintStrokeWidth / 2, mMoveCircleY); // canvas.drawPath(path, p); p.setStyle(Paint.Style.FILL); // canvas.drawCircle(mCircleX, mCircleY, mCircleRadius, p); if (isUp) { canvas.drawCircle(mCircleX, mCircleY, mCircleRadius--, p); canvas.drawCircle(mCircleX, mMoveCircleY, mMoveCircleRadius, p); } else { canvas.drawCircle(mCircleX, mCircleY, mCircleRadius++, p); canvas.drawCircle(mCircleX, mMoveCircleY, mMoveCircleRadius, p); } // canvas.drawPoint(mSupX, mSupY, p); } else { p.setStyle(Paint.Style.FILL); canvas.drawCircle(mCircleX, mMoveCircleY, mMoveCircleRadius, p); // canvas.drawCircle(mCircleX, mMoveCircleY, mMoveCircleRadius, p); } ~~~ 最后遗留一个复杂的问题, 用户可以360度拖动,这时候要根据圆 来计算切点的坐标,再来重新计算辅助点的坐标。比较繁琐,这里就不算了。 #其实我是不会#本篇的目的,了解贝塞尔曲线的应用我想已经达到了,读者若感兴趣,可以自行实现。#其实我是不会# ------------------------------------------------------------------------------解决篇----------------------------------------------------------------------------------------------------- 回到家又研究了一下360度动态转变的,终于想通怎么算了。如下图(无视那么丑 还有想吐槽下中性笔的油): ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea598726.jpg) 其实我们要面临的问题就是计算出 圆的切点坐标。  换言之就是求 ∠1 的角度。 由图可知 ∠1 = ∠2     ∠2  = ∠3  由于等价代换可得 ∠3 = ∠1 由图得:tan∠3 = (mCirlceY - mMoveCircleY)/(mMoveCircleX-mCircleX)  即∠1 = arctan (mCirlceY - mMoveCircleY)/(mMoveCircleX-mCircleX 则可计算右上点的坐标为: x: mMoveCircleX - mMoveCircleRadius * sin∠1 y: mMoveCircleY - mMoveCircleRadius * cos∠1 其他三个切线点的坐标计算方法如上。得到四个点的坐标,只需要改变path的起终点 和 辅助点的坐标即可。由于过于繁杂,就不用代码给各位实现了。大家如果感兴趣,可以自行实现。#其实就是懒#  下一篇:[wing带你玩转自定义view系列(3)模仿微信下拉眼睛](http://blog.csdn.net/wingichoy/article/details/50503858) 本项目地址:[点击打开链接](https://github.com/githubwing/QQMessage)   求关注 求评论  求star
';

wing带你玩转自定义view系列(1) 仿360内存清理效果

最后更新于:2022-04-01 10:01:30

本篇是接自 [手把手带你做自定义view系列](http://blog.csdn.net/wingichoy/article/details/50468674)  宗旨都是一样,带大家一起来研究自定义view的实现,与其不同的是本系列省去了简单的坐标之类的讲解,重点在实现思路,用简洁明了的文章,来与大家一同一步步学习。 转载请注明出处:http://blog.csdn.net/wingichoy/article/details/50500479 上一篇介绍了:[神奇的贝塞尔曲线](http://blog.csdn.net/wingichoy/article/details/50492828),这篇就来研究其应用。  我自己的学习方法是:学习了贝塞尔曲线之后,去研究他的规律,然后开始联想有没有见过类似的效果,最后自己去研究实现,在没有遇到绝对困难的时候,独立思考。只有遇到了绝对困难或者做出来效果之后,才去参考其他人的做法。 好了,废话不多说了,来看看效果图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea5044da.jpg) 图片是从360安全卫士apk里面解压的。一张背景图,一张小绿的图片。 先定义一些属性 ~~~ private int mWidth; private int mHeight; //线的Y坐标 private int mLineY = 600; //判断是否画线回弹 private boolean isDrawBack = false; private int mBitmapX; private int mBitmapY; //飞行的百分比 private int mFlyPercent = 100; //辅助点坐标 x坐标为线段中点, Point supPoint = new Point(350, mLineY); ~~~ 首先来画背景图片 ,和小人,这里背景图片大小不对,没想到有什么好的方法,这里先写死(求解决,求不打死)。 ~~~ Bitmap bitmap = BitmapFactory.decodeResource(getResources(), com.wingsofts.my360clean.R.drawable.mb); BitmapFactory.Options opt = new BitmapFactory.Options(); opt.inJustDecodeBounds = true; // BitmapFactory.decodeResource(getResources(), com.wingsofts.my360clean.R.drawable.t,opt); // int bgWidth = opt.outWidth; // int bgHeight = opt.outHeight; //按线的长度缩放背景图 // Log.e("wing",bgWidth + " " +scale+""); opt.inSampleSize= 2; // opt.outWidth = 500; opt.inJustDecodeBounds = false; Bitmap background =BitmapFactory.decodeResource(getResources(), com.wingsofts.my360clean.R.drawable.t,opt); Paint p = new Paint(); p.setStyle(Paint.Style.STROKE); p.setStrokeWidth(20); Path path = new Path(); //坐标写死微调。。。别打我 canvas.drawBitmap(background,100,mLineY - background.getHeight()+100,p); Point endPoint = new Point(600, mLineY); ~~~ 然后画两个圈圈。 ~~~ p.setColor(Color.GRAY); canvas.drawCircle(100, endPoint.y, 10, p); canvas.drawCircle(endPoint.x,endPoint.y,10,p); ~~~ 之后根据一个标记位 isDrawBack来判断是否画线反弹。这里默认是不反弹。 ~~~ p.setColor(Color.YELLOW); path.moveTo(100, mLineY); path.quadTo(supPoint.x, supPoint.y, endPoint.x, endPoint.y);//绘制贝塞尔曲线, canvas.drawPath(path, p); canvas.drawPoint(supPoint.x, supPoint.y, p); mBitmapX = supPoint.x - bitmap.getWidth() / 2; mBitmapY = (supPoint.y -mLineY)/2 + mLineY- bitmap.getHeight(); canvas.drawBitmap(bitmap, mBitmapX, mBitmapY, p); ~~~ 注意上面bitmap的坐标计算,这里为了方便,贝塞尔曲线只画中点的。#实际上是不会# 观察辅助点坐标 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea522115.jpg) 猜测辅助点到切线点  和切线点到mLineY的距离相等,然后计算出bitmap所在的坐标,进行绘制。 然后来绘制下拉时候的样子,重写onTouchEvent,主要是改变辅助点坐标和bitmap坐标,在action_move改变坐标 最后通知重绘。重写action_up来改变最重点的坐标, 改变isDrawBack标记位,通知阶段为上弹阶段。 知识补充:getX是相对view的坐标  getRawX是相对屏幕的坐标. ~~~ @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_MOVE: supPoint.x = (int) event.getX(); if(event.getY()>mLineY) supPoint.y = (int) event.getY(); invalidate(); break; case MotionEvent.ACTION_UP: endX = (int) event.getX(); endY = (int) event.getY(); isDrawBack = true; invalidate(); break; } return true; } ~~~ 最后来绘制回弹的效果,相信看过之前我的文章的都知道我采用一种percent办法。这里给一个参数mFlyPercent,从100开始递减,递减辅助点的y坐标和bitmap的y坐标,来实现动画效果。 如果mFlyPercent<0 则代表绘制完毕  重置标志位和百分比。 ~~~ if (isDrawBack) { p.setColor(Color.YELLOW); path.moveTo(100, mLineY); path.quadTo(supPoint.x, mLineY + (supPoint.y - mLineY) * mFlyPercent / 100, endPoint.x, endPoint.y); canvas.drawPath(path, p); if(mFlyPercent >0) { canvas.drawBitmap(bitmap, mBitmapX, mBitmapY * mFlyPercent/100, p); mFlyPercent -=5; postInvalidateDelayed(10); }else { mFlyPercent = 100; isDrawBack = false; } ~~~ 这样,变结束了模仿360内存清理效果,对于x坐标的计算。。。日后再研究。 本项目地址:[点击打开链接](https://github.com/githubwing/My360Clean) 下一篇:[wing带你玩转自定义view系列(2) 简单模仿qq未读消息去除效果](http://blog.csdn.net/wingichoy/article/details/50503630)
';

Android自定义view进阶– 神奇的贝塞尔曲线

最后更新于:2022-04-01 10:01:28

上一篇介绍了[自定义view需要知道的基本函数](http://blog.csdn.net/wingichoy/article/details/50487009)。新开一篇献给借给我vpn的深圳_奋斗小哥。 转载请注明出处:http://blog.csdn.net/wingichoy/article/details/50492828 今天给大家介绍一个非常神奇的曲线,贝塞尔曲线。相信大家之前都有耳闻。 很久之前就久闻该线大名,但是一直不是很了解,在经过一番谷歌之后,有了初步的概念:三点确定一条曲线:起点,终点,辅助点。 三个点的基本关系如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea473a05.jpg) 当初看这图我也看了老半天,只知道是非常平滑,不知道三个点的具体关系,于是变写了一段程序来测试辅助点与始终点的关系。 Android 的Path类提供了绘制二阶贝塞尔曲线的方法,使用方法如下: ~~~ //设置起点 path.moveTo(200,200); //设置辅助点坐标 300,200 终点坐标400,200 path.quadTo(300, 200, 400, 200); ~~~ 这里我将贝塞尔曲线的辅助点y轴和起始点设置相同,draw以后效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea49b770.jpg) 看到是一条直线,这是因为他y轴没有拉伸,只是x轴进行了拉伸。把辅助点y+100尝试 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea4ad267.jpg) 看到已经拉伸。。其实这样还是不能很好的体现 贝塞尔曲线的规律。 所以要持续改变,研究他的规律,这里重写onTouchEvent,让触摸点的位置作为辅助点。观察变化。 ~~~ @Override protected void onDraw(Canvas canvas) { Paint p = new Paint(); p.setStyle(Paint.Style.STROKE); p.setStrokeWidth(10); Path path = new Path(); path.moveTo(200, 200); path.quadTo(mSupX, mSupY, 400, 200); canvas.drawPath(path,p); super.onDraw(canvas); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_MOVE: mSupX = event.getX(); mSupY = event.getY(); invalidate(); } return true; } ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea4bf762.jpg) 可以看到 是根据鼠标位置变化的曲线,可是现在还是不能很好的表现曲线的突出点和辅助点关系,接下来把辅助点也画出来,方便观察。 ~~~ canvas.drawPoint(mSupX,mSupY,p); ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea4d97ef.jpg) 这下,辅助点和曲线的关系就很明显了。 许多炫酷的效果都离不开贝塞尔曲线,贝塞尔曲线的应用:[仿360内存清理效果](http://blog.csdn.net/wingichoy/article/details/50500479)。
';

关于Android自定义view 你所需要知道的基本函数

最后更新于:2022-04-01 10:01:25

开始时之前想吐槽一句。。iphone的闹钟,12小时制。我成功的把闹钟订到了下午5:00 导致错过一班飞机。心疼改签费。 候机ing,没有事做,来写一下学习自定义view必须掌握的基本函数。这里只挑一些常用的进行讲解。 首先 往Canvas上面draw需要一个Paint。 画笔常用的函数有哪些呢。由于木有调试环境,函数基本上默写,有错请评论提出,蟹蟹! ~~~ Paint p = new Paint(); //设置画笔的颜色 p.setColor(Color.parseColor("#2EA4F2")); //设置画笔的风格:全部填充FILL 只画轮廓STROKE p.setStyle(Paint.Style.STROKE); //设置画笔的宽度 p.setStrokeWidth(8); //设置是否抗锯齿 p.setAntiAlias(true); ~~~ ~~~ //设置文字大小 p.setTextSize(30); //测量字符串的长度 p.MeasureText("Hello World"); ~~~ 当我们有了画笔后,就可以绘制基本图形。 **线**: ~~~ //绘制一条从0,0到100,100的线 canvas.drawLine(0,0,100,100,p); ~~~ **三角形&多边形** 是用Path类实现的。Path类提供了点绘制线的功能,看例子 ~~~ path.MoveTo(0,0);//给定path的起点 path.LineTo(10,10);//往10,10绘制一条路径 path.LineTo(5,3);//继续从10,10往5,3绘制一条路径 path.close;//将绘制的线形成封闭空间 canvas.drawPath(path,p); ~~~ **矩形**: ~~~ //画一个矩形,左上角的坐标为0,0 右下角的坐标为100,50 canvas.drawRect(0,0,100,50,p); ~~~ **圆角矩形**: ~~~ //一个矩形 RectF rectF = new RectF(0,0,100,50); //画一个圆角矩形,大小为rectF,20,20分表表示左边圆角的半径和右边圆角的半径 canvas.drawRoundRect(RectF,20,20,p); ~~~ **圆形** ~~~ //画一个圆,圆心为50,50 半径为100 canvas.drawCircle(50,50,100,p); ~~~ **弧形** 注意这里第二个参数,是从三点钟方向为0°计算,所以想从12点中方向开始绘制,那么就是270°。第四个参数是决定是否经过圆心(自己改变一下这个参数就知道区别了)。 ~~~ //画一个弧,弧所在矩形为rectF 从270°开始,画90° 不经过圆心 canvas.drawArc(rectF,270,90,false,p); ~~~ 以上基本上是自定义view所用到的最基本的函数,欢迎补充。
';

手把手带你做一个超炫酷loading成功动画view Android自定义view

最后更新于:2022-04-01 10:01:23

写在前面: 本篇可能是手把手自定义view系列最后一篇了,实际上我也是一周前才开始真正接触自定义view,通过这一周的练习,基本上已经熟练自定义view,能够应对一般的view需要,那么就以本篇来结尾告一段落,搞完毕设的开题报告后去学习新的内容。 有人对我说类似的效果网上已经有了呀,直接拿来就可以用,为什么还要写。我个人的观点是:第三方控件多数不能完全满足UI的要求,如果需要修改,那么必须理解他的实现,所以很有必要自己去写一款出来,成为程序的创造者,而不单单是使用者。所以,写一写已经实现的效果,对学习来说还是很重要的。我相信,等从使用者变成创造者之后,会有很大的提高。 转载请注明出处:http://blog.csdn.net/wingichoy/article/details/50482271 好了,前面说的有点多。。那么开始正题,这次实现的是一个带有动画效果的loading,  难度比之前的所有文章都要复杂。但是其实一步一步分解做下来,用心去做还是不难的。 为了美观一些,我把它放到了dialog里面   上效果图:  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea428b75.jpg) 怎么样!是不是很炫酷,自我感觉很融入系统,像原生的东西#王婆卖瓜# 开始之前。我想说一下我的大体思路,不知道有没有更好的方法。欢迎拍砖指点交流! 对于动画效果,我全局采用一种percent,和标记位思想。也就是绘制分段,如果不达到100%,就一直画到底。如:对于一条线,从一端画到另一端,他的第二个点的坐标不断变化,我就用 最终的长度 * 百分比 来作为过度时期的变量, 即canvas.drawLine(0,0,x * percent /100,y * percent /100);    标记位的话就一一个boolean值,如果来判断该怎么画。 缺点也很明显:每次都要if 他percent到没到100%  嵌套太多次,不知道有没有解决办法。 现在来分析一下动画的过程。基础的坐标计算一下就好,这里就不多说,如果你这些还不熟练,你可以看看我之前的文章:[手把手带你绘制一个时尚仪表盘](http://blog.csdn.net/wingichoy/article/details/50468674) 1.首先要做的是绘制出来刚开始静态的圆和箭头, 箭头用path画。 2.是竖线缩短的过程,变成一个点。 3.箭头变横线。 4.点被横线抛出到圆。 5.按百分比绘制的弧,同时直线变对勾。 之后我们就一个一个来画。构造函数测量的就省去了。 那么 拿起你的paint 开始跟我一起draw吧 初始化一下画笔 ~~~ Paint p = new Paint(); Path path = new Path(); p.setColor(Color.parseColor("#2EA4F2")); p.setStyle(Paint.Style.STROKE); p.setStrokeWidth(8); p.setAntiAlias(true); //百分比弧的矩形 RectF rectF = new RectF(5, 5, mWidth - 5, mHeight - 5); //绘制圆 canvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2 - 5, p); ~~~ 这里设置一个标志位,用于标记是否可以开始绘制动画。 ~~~ if (canStartDraw) { isDrawing = true; ~~~ 如果不可以,则绘制静态的箭头。 ~~~ else { //绘制静态箭头 p.setColor(Color.WHITE); canvas.drawLine(mWidth / 2, mHeight / 4, mWidth / 2, mHeight * 0.75f, p); // Path path = new Path(); path.moveTo(mWidth / 4, mHeight * 0.5f); path.lineTo(mWidth / 2, mHeight * 0.75f); path.lineTo(mWidth * 0.75f, mHeight * 0.5f); canvas.drawPath(path, p); } ~~~ 现在的效果是这样的 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea4449a7.jpg) 2.线变成一个点。 这里的效果其实就是不断绘制一个线,基本上坐标的变化可以用如下表示:   x,y1+percent,x,y2-percent. 那么这里就开始drawLine   用百分比控制具体的变化量。   这里为什么percent 不自增到100呢,原因是线最终要缩短成和点一样的大小,并非消失。这里就完成了第一阶段的动画。 ~~~ isDrawing = true; //开始变形 p.setColor(Color.WHITE); //如果小于95 就继续缩短。 95是微调值 和point大小相等 if (mLineShrinkPercent < 95) { //线段逐渐缩短(终点为mWidth/2,mHeight/2) float tmp = (mWidth / 2 - mHeight / 4) * mLineShrinkPercent / 100; canvas.drawLine(mWidth / 2, mHeight / 4 + tmp, mWidth / 2, mHeight * 0.75f - tmp, p); mLineShrinkPercent += 5; } else { ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea455700.jpg) 3.箭头变横线   由于箭头是Path画的,所以我们只要改变path中间那个点的y坐标就可以了。 这里需要注意的是 在变成西线的过程中,点是一直存在的,所以要画一个circle上去(为什么不point,因为他很方= =),也是用一个百分比来控制。 ~~~ //path变成直线 isPathToLine = true; if (mPathPercent < 100) { path.moveTo(mWidth / 4, mHeight * 0.5f); path.lineTo(mWidth / 2, mHeight * 0.75f - mPathPercent / 100f * 0.25f * mHeight); path.lineTo(mWidth * 0.75f, mHeight * 0.5f); canvas.drawPath(path, p); mPathPercent += 5; //在变成直线的过程中这个点一直存在 canvas.drawCircle(mWidth / 2, mHeight / 2,2.5f, p); } else { ~~~ 4.点被横线抛出到圆弧上,同样也是百分比控制。   最终的圆心坐标为 mWidth/2,0 ~~~ //绘制把点上弹的直线 //画上升的点 if (mRisePercent < 100) { //在点移动到圆弧上的时候 线是一直存在的 canvas.drawLine(mWidth / 4, mHeight * 0.5f, mWidth * 0.75f, mHeight * 0.5f, p); canvas.drawCircle(mWidth / 2, mHeight / 2 - mHeight / 2 * mRisePercent / 100 + 5,2.5f, p); mRisePercent += 5; } else ~~~ 5.按百分比绘制的弧,同时直线变对勾。 弧的话,注意一下起始弧度是270, 绘制弧度为360*percent就可以。对勾跟上面一样,因为本身是一条Path,这次同时改变第二个点和第三个点就可以了。 ~~~ //上升的点最终的位置 canvas.drawPoint(mWidth / 2, 5, p); isRiseDone = true; //改变对勾形状 if (mLinePercent < 100) { path.moveTo(mWidth / 4, mHeight * 0.5f); path.lineTo(mWidth / 2, mHeight * 0.5f+ mLinePercent/100f * mHeight * 0.25f); path.lineTo(mWidth * 0.75f, mHeight * 0.5f - mLinePercent / 100f * mHeight * 0.3f); canvas.drawPath(path, p); mLinePercent += 5; //动态绘制圆形百分比 if (mCirclePercent < 100) { canvas.drawArc(rectF, 270, -mCirclePercent / 100.0f * 360, false, p); mCirclePercent += 5; } ~~~ 6.(为什么有6??) 很重要的一点,切忌不要忘记。如果你只写了以上代码,你运行完会发现所有绘图都消失了! 那是因为percent超过100就不进行绘制了,所以在最外面的else里需要绘制一个圆加一个对号。完成之后直接  postInvalidateDelayed(10); ~~~ <pre name="code" class="java">else { //绘制最终的path path.moveTo(mWidth / 4, mHeight * 0.5f); path.lineTo(mWidth / 2, mHeight * 0.75f); path.lineTo(mWidth * 0.75f, mHeight * 0.3f); canvas.drawPath(path, p); // 绘制最终的圆 canvas.drawArc(rectF, 270, -360, false, p); isDrawing = false; } ~~~ 7.(还有7,你逗我?)加一个start方法 用于重置各种标记位 ~~~ public void start() { if (isDrawing == false) { canStartDraw = true; isRiseDone = false; mRisePercent = 0; mLineShrinkPercent = 0; mCirclePercent = 0; mPathPercent = 0; mLinePercent = 0; invalidate(); } } ~~~ 之后就大功告成了!!! 本项目地址: [点击打开链接](https://github.com/githubwing/LoadingView)   求关注  求评论  求star    那么,手把手带你写view系列就基本完结了。 手把手番外篇:[ 手把手教你画一个 逼格满满圆形水波纹loadingview Android](http://blog.csdn.net/wingichoy/article/details/50523713)
';

手把手带你画一个动态错误提示 Android自定义view

最后更新于:2022-04-01 10:01:20

嗯。。再差1篇就可以获得持之以恒徽章了,今天带大家画一个比较简单的view。 转载请注明出处:http://blog.csdn.net/wingichoy/article/details/50477108 废话不多说,看效果图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea3e29b9.jpg) 首先 构造函数 测量... 这里就一笔带过了。  ~~~ public ErrorView(Context context) { this(context, null); } public ErrorView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ErrorView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (widthMode == MeasureSpec.EXACTLY) { mWidth = widthSize; } else { mWidth = 200; } if (heightMode == MeasureSpec.EXACTLY) { mHeight = heightSize; } else { mHeight = 200; } setMeasuredDimension(mWidth, mHeight); } ~~~ 如果你看不懂上面这些,翻翻我的这一篇博客,有一些补充的知识点。[圆形百分比view](http://blog.csdn.net/wingichoy/article/details/50334595) 接下来draw,如果让你绘制一个静态的这个突然,你一定闭着眼也能画出来。。那么怎么才能实现动态的效果呢。。 其实就是模仿手绘的过程,我们是一点一点画出来的,一条线的逐渐延迟。  那我们就来模仿这个自然的过程。 首先画一个圆形。 ~~~ Paint p = new Paint(); p.setStrokeWidth(10); p.setAntiAlias(true); p.setColor(Color.RED); p.setStyle(Paint.Style.STROKE); RectF rectF = new RectF(0 + 10, 0 + 10, mWidth - 10, mHeight - 10); canvas.drawArc(rectF, 180, 360 * mProgress / 100, false, p); mProgress+=5; ~~~ 可以看到drawArc的第三个参数 是变化的 , 其中mProgress的初值是零,这里让他自增,也就是说,每次调用onDraw方法,他就会增加。所以每次的弧都会比原来长一点点,直到最后画完。  所以在程序的最后 一定有 postInvalidateDelayed(10); 方法。 接下来来绘制两条线,这里的坐标我直接取半径的4分之一啦,唯一注意一点就是,只有在progress大于100的时候 我们才绘制两条线,两条线段也是根据一个变量自增的,原理同上。  这里mLineOneX等参数均表示画线的时候两点的坐标。 当mLineOneX = mWidth * 0.5的时候  mWidth /4 + mLineOneX 就等于我们要画线段的最终点。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea40c9ac.jpg) ~~~ if (mProgress > 100) { //画左边的线 if (mLineOneX < mWidth * 0.5) { mLineOneX+=20; mLineOneY+=20; } canvas.drawLine(mWidth / 4, mHeight / 4, mWidth / 4 + mLineOneX, mHeight / 4 + mLineOneY, p); if (mLineOneX == mWidth * 0.5) { if (mLineTwoX < mWidth * 0.5) { mLineTwoX+=20; mLineTwoY+=20; }else { //判断全部绘制完成 isLineDrawDone = true; } canvas.drawLine(mWidth / 4, (float) (mHeight * 0.75), mWidth / 4 + mLineTwoX, (float) (mHeight * 0.75) - mLineTwoY, p); } } ~~~ 之后 添加一个标记位   isLineDrawDone  判断一下 如果没有画完 则 : ~~~ if(isLineDrawDone){ Log.e("wing","draw done"); }else{ postInvalidateDelayed(10); } ~~~ 现在 基本上完成了绘制,  别急 还没加震动,震动效果是怎么实现的呢,大家还记得吗,如果忘了,可以看看我这篇博客:[自定义动画 实现抖一抖效果](http://blog.csdn.net/wingichoy/article/details/47122619) 所以我们要写一个接口,来回调onStop ~~~ public interface OnStopListener{ void onStop(View v); } ~~~ 把最后的绘制完成完善,继续增加一个标志位,代表全部绘制完成 ~~~ if(isLineDrawDone){ Log.e("wing","draw done"); if(!isDrawDone) { if (mOnStopListener != null) { mOnStopListener.onStop(this); } isDrawDone = true; } }else{ postInvalidateDelayed(10); } ~~~ 提供一个reset()方法 让用户可以手动控制重绘 ~~~ public void reset() { mProgress = 0; mLineOneX = 0; mLineOneY = 0; mLineTwoX = 0; mLineTwoY = 0; isLineDrawDone = false; isDrawDone = false; invalidate(); } ~~~ 在提供一个添加监听器的方法 ~~~ public void setOnStopListener(OnStopListener onStopListener){ mOnStopListener = onStopListener; } ~~~ 最后 在Activity中 为这个View添加 震动效果 ~~~ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mErrorView = (ErrorView) findViewById(R.id.errorView); mErrorView.setOnStopListener(new ErrorView.OnStopListener() { @Override public void onStop(View v) { ShakeAnimation sa = new ShakeAnimation(); sa.setDuration(1000); v.startAnimation(sa); } }); mErrorView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mErrorView.reset(); } }); ~~~ 嘿嘿嘿。。。这样就骗到了持之以恒勋章。 源码地址:http://download.csdn.net/detail/wingichoy/9394685
';

手把手带你画一个 时尚仪表盘 Android 自定义View

最后更新于:2022-04-01 10:01:17

拿到美工效果图,咱们程序员就得画得一模一样。 为了不被老板喷,只能多练啊。 听说你觉得前面几篇都so easy,那今天就带你做个相对比较复杂的。 转载请注明出处:http://blog.csdn.net/wingichoy/article/details/50468674 今天的效果图如下(左边是ui图 右边是实现图): ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea2d86a0.jpg) 自我感觉总体效果还不错,至少大概画得一样了。上一个动态图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea2eb7a3.jpg) 其实这个效果实现起来也不是很难,就是计算坐标,弧度之类的可能会比较麻烦,这里分享写这个其中一张手稿,请无视掉很丑的字,其实做自定义view 还是要在纸上多画。所以希望大家也能这么画画,思路会很顺。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea30ede8.jpg) 好的了,废话不多说,快开始。 首先自定义属性  构造函数,测量什么的 你肯定已经很熟练 直接贴代码了,注释写的很清楚 ~~~ public class PanelView extends View { private int mWidth; private int mHeight; private int mPercent; //刻度宽度 private float mTikeWidth; //第二个弧的宽度 private int mScendArcWidth; //最小圆的半径 private int mMinCircleRadius; //文字矩形的宽 private int mRectWidth; //文字矩形的高 private int mRectHeight; //文字内容 private String mText = ""; //文字的大小 private int mTextSize; //设置文字颜色 private int mTextColor; private int mArcColor; //小圆和指针颜色 private int mMinCircleColor; //刻度的个数 private int mTikeCount; private Context mContext; public PanelView(Context context) { this(context, null); } public PanelView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public PanelView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mContext = context; TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.PanelView,defStyleAttr,0); mArcColor = a.getColor(R.styleable.PanelView_arcColor, Color.parseColor("#5FB1ED")); mMinCircleColor = a.getColor(R.styleable.PanelView_pointerColor,Color.parseColor("#C9DEEE")); mTikeCount = a.getInt(R.styleable.PanelView_tikeCount,12); mTextSize = a.getDimensionPixelSize(PxUtils.spToPx(R.styleable.PanelView_android_textSize,mContext),24); mText = a.getString(R.styleable.PanelView_android_text); mScendArcWidth = 50; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (widthMode == MeasureSpec.EXACTLY) { mWidth = widthSize; }else { mWidth = PxUtils.dpToPx(200,mContext); } if (heightMode == MeasureSpec.EXACTLY) { mHeight = heightSize; }else { mHeight = PxUtils.dpToPx(200,mContext); } Log.e("wing",mWidth+""); setMeasuredDimension(mWidth, mHeight); } ~~~ 自定义属性attr.xml ~~~ <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="PanelView"> <attr name="arcColor" format="color"/> <attr name="arcWidth" format="dimension"/> <attr name="android:text"/> <attr name="tikeCount" format="integer"/> <attr name="pointerColor" format="color"/> <attr name="android:textSize"/> </declare-styleable> </resources> ~~~ 之后来重头戏,也就是绘制。就像画画一样,再复杂的view也是一笔一笔画出来的。所以我们把这个view分解。 大概分解成如下:**1.最外面的弧   2.里面的粗弧   3.中间小圆   4.最小的圆  5.刻度   6.指针  7.矩形  8.文字** 相信让你分开画一定难不倒你。那组合在一起 就是这个view啦。下面开始我们的ondraw() 按照这个分解来: 1.绘制最外面的弧   这里需要注意的一点是,如果想让这个圆在view里 记得减去画笔宽度的一半  因为半径是从圆心到画笔宽度的中间算的,所以这里画弧的矩形是  new RectF(strokeWidth, strokeWidth, mWidth - strokeWidth, mHeight - strokeWidth) ~~~ Paint p = new Paint(); int strokeWidth = 3; p.setStrokeWidth(strokeWidth); p.setAntiAlias(true); p.setStyle(Paint.Style.STROKE); p.setColor(mArcColor); //最外面线条 canvas.drawArc(new RectF(strokeWidth, strokeWidth, mWidth - strokeWidth, mHeight - strokeWidth), 145, 250, false, p); ~~~ 画出来是这样的效果。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea32df78.jpg) 2.绘制里面的粗弧,这里比较麻烦的就是需要分为四段,看图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea33ef38.jpg) 因为大圆和里面粗弧的长短不一致,这里使用百分比来计算 所以会造成指针偏差,那么这里把 1、2两个部分固定来画,然后是3 充满的部分,用百分比来计算需要画多少度,最后是4 空白的部分。 首先把粗弧的矩形画出来,这里固定了比大弧半径少50(这里其实可以改进,你可以改成动态的让他更灵活),然后计算出百分比。 ~~~ RectF secondRectF = new RectF(strokeWidth + 50, strokeWidth + 50, mWidth - strokeWidth - 50, mHeight - strokeWidth - 50); float secondRectWidth = mWidth - strokeWidth - 50 - (strokeWidth + 50); float secondRectHeight = mHeight - strokeWidth - 50 - (strokeWidth + 50); float percent = mPercent / 100f; ~~~ 接下来绘制1弧,先算出fill充满部分的度数,因为是突出的,所以如果百分比为0,突出左端为白色 如果不为零,则和充满颜色统一。 ~~~ //充满的圆弧的度数 -5是大小弧的偏差 float fill = 250 * percent ; //空的圆弧的度数 float empty = 250 - fill; // Log.e("wing", fill + ""); if(percent==0){ p.setColor(Color.WHITE); } //画粗弧突出部分左端 canvas.drawArc(secondRectF,135,11,false,p); ~~~ 然后绘制2弧 也就是fill充满的弧, ~~~ canvas.drawArc(secondRectF, 145, fill, false, p); ~~~ 接下来是3弧,也就是empty未充满的弧,是白色的 ~~~ p.setColor(Color.WHITE); //画弧胡的未充满部分 canvas.drawArc(secondRectF, 145 + fill, empty, false, p); ~~~ 最后,画出右边突出的4弧, 如果百分比为100 那么和充满的颜色一致,否则为白色 ~~~ //画粗弧突出部分右端 if(percent == 1){ p.setColor(mArcColor); } canvas.drawArc(secondRectF,144+fill+empty,10,false,p); ~~~ 这样粗弧也就画完了 来看看效果,就画了两条弧线(实际是5条),就成型了。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea34f25e.jpg) 3.中间的小圆外圈,他的圆心不用多说 是整个view的中心 ~~~ p.setColor(mArcColor); //绘制小圆外圈 p.setStrokeWidth(3); canvas.drawCircle(mWidth / 2, mHeight / 2, 30, p); ~~~ 4.绘制内圆,圆心一样的,半径和画笔粗度改变一下 ~~~ //绘制小圆内圈 p.setColor(mMinCircleColor); p.setStrokeWidth(8); mMinCircleRadius = 15; canvas.drawCircle(mWidth / 2, mHeight / 2, mMinCircleRadius, p); ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea3602ea.jpg) 5.刻度  刻度处理起来可能比较麻烦,用三角函数算坐标啊 循环画出来。。 这里提供一种比较简单的方法:旋转画布。 首先引入一个概念,什么叫旋转画布呢,就是把你的画布旋转。。经过测试,旋转以后,整个坐标轴都会对应旋转,一张图举例说明下。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea3752f2.jpg) 大概就是这个意思,画布旋转之后 坐标系也就旋转了,但是原来的图像还在,所以说你比如这个点 x,y旋转前在这个位置, 那么旋转后就是另外一个位置了,但是他们的坐标是相同的。 所以刻度也可以考这种方法画。我们只要画出最顶端的刻度 然后旋转就可以了。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea38a8de.jpg) 绘制第一段刻度, 然后总共是250的弧度  计算出每个刻度的度数     用250除以刻度数mTikeCount,就是每次旋转的度数。接下来把画布逐步旋转,按照原坐标绘制,即可绘制出右半部分刻度。  注意:为了让之后的绘制正常,务必把画布转回原来的位置 ~~~ //绘制刻度! p.setColor(mArcColor); //绘制第一条最上面的刻度 mTikeWidth = 20; p.setStrokeWidth(3); canvas.drawLine(mWidth / 2, 0, mWidth / 2, mTikeWidth, p); //旋转的角度 float rAngle = 250f / mTikeCount; //通过旋转画布 绘制右面的刻度 for (int i = 0; i < mTikeCount / 2; i++) { canvas.rotate(rAngle, mWidth / 2, mHeight / 2); canvas.drawLine(mWidth / 2, 0, mWidth / 2, mTikeWidth, p); } //现在需要将将画布旋转回来 canvas.rotate(-rAngle * mTikeCount / 2, mWidth / 2, mHeight / 2); ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea39d43e.jpg) 左半部分同理,需要改变的度数为负 就好了 ~~~ //通过旋转画布 绘制左面的刻度 for (int i = 0; i < mTikeCount / 2; i++) { canvas.rotate(-rAngle, mWidth / 2, mHeight / 2); canvas.drawLine(mWidth / 2, 0, mWidth / 2, mTikeWidth, p); } //现在需要将将画布旋转回来 canvas.rotate(rAngle * mTikeCount / 2, mWidth / 2, mHeight / 2); ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea3ad2a7.jpg) 6.指针   指针的绘制和刻度相似,先算出来百分比所占的度数 然后根据 是否大于50%来旋转画布。 指针的起终点是 总view高度的一半 粗弧矩形的一半 加上小圆,前面坐标讲解了那么,这个也一样,自己拿起笔算一算。 注意这里画布旋转我通过计算得出一个公式 250 * percent - 250/2。 如果小于50% 则为负   如果大于50%则为正,然后进行旋转。 切忌最后一定要将画布转回来。 ~~~ //绘制指针 p.setColor(mMinCircleColor); p.setStrokeWidth(4); //按照百分比绘制刻度 canvas.rotate(( 250 * percent - 250/2), mWidth / 2, mHeight / 2); canvas.drawLine(mWidth / 2, (mHeight / 2 - secondRectHeight / 2) + mScendArcWidth / 2 + 2, mWidth / 2, mHeight / 2 - mMinCircleRadius, p); //将画布旋转回来 canvas.rotate(-( 250 * percent - 250/2), mWidth / 2, mHeight / 2); ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea3be503.jpg) 接下来就是画矩形和文字。没什么好说的了,坐标也是X周围mWidth/2   y轴自己根据圆心微调一个距离 ~~~ //绘制矩形 p.setStyle(Paint.Style.FILL); p.setColor(mArcColor); mRectWidth = 60; mRectHeight = 25; //文字矩形的最底部坐标 float rectBottomY = mHeight/2 + secondRectHeight/3+mRectHeight; canvas.drawRect(mWidth/2-mRectWidth/2,mHeight/2 + secondRectHeight/3,mWidth/2+mRectWidth/2,rectBottomY,p); p.setTextSize(mTextSize); mTextColor = Color.WHITE; p.setColor(mTextColor); float txtLength = p.measureText(mText); canvas.drawText(mText,(mWidth-txtLength)/2,rectBottomY + 40,p); super.onDraw(canvas); ~~~ 这样完成了整个view的绘制。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea3ceedb.jpg) 下面要做的就是为了方便使用者,提供一些设置属性的方法。 ~~~ /** * 设置百分比 * @param percent */ public void setPercent(int percent) { mPercent = percent; invalidate(); } /** * 设置文字 * @param text */ public void setText(String text){ mText = text; invalidate(); } /** * 设置圆弧颜色 * @param color */ public void setArcColor(int color){ mArcColor = color; invalidate(); } /** * 设置指针颜色 * @param color */ public void setPointerColor(int color){ mMinCircleColor = color; invalidate(); } /** * 设置文字大小 * @param size */ public void setTextSize(int size){ mTextSize = size; invalidate(); } /** * 设置粗弧的宽度 * @param width */ public void setArcWidth(int width){ mScendArcWidth = width; invalidate(); } ~~~ 大功告成!!!一个看似复杂的view  经过我们一步一步绘制遍完成了。 其实技术的养成也是这样,只要一步一步脚踏实地的去练习,我相信总有一天我能成为大神。 本项目地址 :[PanelView](https://github.com/githubwing/PanelView)   求关注  求评论  求star!!!!!!
';

新手自定义view练习实例之(二) 波浪view

最后更新于:2022-04-01 10:01:15

本系列是为新手准备的自定义view练习项目(大牛请无视),相信在学习过程中,想学自定义view又无从下手,不知道做什么。本系列为新手提供了一系列自定义view的简单实例。看过理解之后,自己实现,相信会有很大提高。 转载请注明本篇出处:http://blog.csdn.net/wingichoy/article/details/50460213 继续来本系列的第二篇,启发是吃口香糖看到了包装纸,觉得挺好看,就想画一个出来#职业病#,本次的目标是做一个波浪形状的view,可以是尖角,也可以是圆角。 那么老规矩,上效果图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea29540a.jpg) 聪明的你一眼就看出来了,不就是个矩形加好多三角嘛。答对了,就是这么简单,事不宜迟,快拿起武器动手练一练。毕竟程序员的秘诀就是“无他,唯手熟尔”。 首先,新建一个类 起名为WaveView 继承自View,重写他的构造方法,在第三个构造方法里获取自定义属性。 ~~~ public WaveView(Context context) { this(context, null); } public WaveView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public WaveView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mContext = context; TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.WaveView,defStyleAttr,0); mWaveCount = a.getInt(R.styleable.WaveView_waveCount,10); mWaveWidth = a.getInt(R.styleable.WaveView_waveWidth,20); mMode = a.getInteger(R.styleable.WaveView_mode,-2); mColor = a.getColor(R.styleable.WaveView_android_color,Color.parseColor("#2C97DE")); } ~~~ 自定义属性如下 attr.xml: ~~~ <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="WaveView"> <attr name="waveCount" format="integer"/> <attr name="waveWidth" format="integer"/> <attr name="android:color"/> <attr name="mode" > <enum name = "circle" value="-1"/> <enum name = "triangle" value = "-2"/> </attr> </declare-styleable> </resources> ~~~ 这些都没有什么好说的,你一定已经轻车熟路。 然后重写他的onMeasure()  来告诉系统这个view有多大。 ~~~ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); //矩形宽度为view的80% if (widthMode == MeasureSpec.EXACTLY) { mWidth = widthSize; mRectWidth = (float) (mWidth * 0.8); //如果是wrap_content 直接给一个定值 }else if(widthMode == MeasureSpec.AT_MOST){ mWidth = PxUtils.dpToPx(300,mContext); mRectWidth = (float) (mWidth * 0.8); } //矩形高度为view的80% if (heightMode == MeasureSpec.EXACTLY) { mHeight = heightSize; mRectHeight = (float) (mHeight * 0.8); //如果是wrap_content 直接给一个定值 }else if(heightMode == MeasureSpec.AT_MOST){ mHeight = PxUtils.dpToPx(200,mContext); mRectHeight = (float) (mHeight * 0.8); } setMeasuredDimension(mWidth, mHeight); } ~~~ 准备工作大致已经完成,接下来开始绘图。首先 画一个矩形。 这个矩形让他处于view的中间,看图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea2a45e9.jpg) 由图可知, 矩形的左上坐标为 padding ,padding 矩形的右下坐标为padding +mRectWidth, padding + mRectHeight  其中padding 为 (mWidth - mRectWidth)/2   注意这里只是左右padding 为了简易 就不计算上下padding了。 所以我们先将矩形绘制出来,如下图:   可以看到左右padding是相等的,上下不等,这是因为上面只计算了左右padding ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea2b29b7.jpg) ~~~ protected void onDraw(Canvas canvas) { Paint p = new Paint(); p.setColor(mColor); //计算每个三角形的高 mWaveHeight = mRectHeight / mWaveCount; //绘制矩形 //计算padding float padding = ((mWidth - mRectWidth) / 2); canvas.drawRect(padding, padding, mRectWidth + padding, mRectHeight + padding, p); ~~~ 绘制矩形完毕,需要来一个判断,判断当前模式是圆角还是尖角 ~~~ if(mMode == MODE_TRIANGLE) {}else{} ~~~ 我们首先来绘制尖角,还记得上一篇泡泡窗的那个三角吗,我们只要画多次不就形成波浪了吗,所以用循环就可以搞定。坐标计算如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea2c3d23.jpg) 矩形的右上角暂定为 StartX,StartY    三角形的宽度为 mWaveWidth  高度为 mWaveHeight  那么还是用path来画,首先将path MoveTo startX,startY 然后计算得出各个坐标,在用一个i来代表第几个三角形来做循环,代码如下: ~~~ if(mMode == MODE_TRIANGLE) { //绘制右边的波浪 float startX = padding + mRectWidth; float startY = padding; Path path = new Path(); path.moveTo(startX, startY); for (int i = 0; i < mWaveCount; i++) { path.lineTo(startX + mWaveWidth, startY + i * mWaveHeight + (mWaveHeight / 2)); path.lineTo(startX, startY + mWaveHeight * (i + 1)); } path.close(); canvas.drawPath(path, p); ~~~ 只要把上面的坐标带入进去即可,那么左边的波浪只是改变了一个x坐标的加减值,也很简单。 ~~~ //绘制左边的波浪 startX = padding; startY = padding; path.moveTo(startX, startY); for (int i = 0; i < mWaveCount; i++) { path.lineTo(startX - mWaveWidth, startY + i * mWaveHeight + (mWaveHeight / 2)); path.lineTo(startX, startY + mWaveHeight * (i + 1)); } path.close(); canvas.drawPath(path, p); ~~~ 这样便完成了整个波浪view的绘制。本篇就到此结束了。 别急,我没忘圆形模式,这里给大家留个家庭作业,大家下去根据这个思路自己把圆形波浪画出来吧~ 毕竟只有多练,才能提高 本项目github地址:[点击打开链接](https://github.com/githubwing/WaveView) ,求关注,求评论,求star
';

新手自定义view练习实例之(一) 泡泡弹窗

最后更新于:2022-04-01 10:01:13

转载请注明出处:http://blog.csdn.net/wingichoy/article/details/50455412 本系列是为新手准备的自定义view练习项目(大牛请无视),相信在学习过程中,想学自定义view又无从下手,不知道做什么。本系列为新手提供了一系列自定义view的简单实例。看过理解之后,自己实现,相信会有很大提高。 公司需要做地图,无奈美工没有给那种泡泡窗口,于是只能自定义view自己画一个。 由于实现比较简单,便想写下来,给新手做一个自定义view的参考,新人学自定义view的时候也可以先拿这个练手。 首先看效果图,就是一般的泡泡窗啦。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea26ee3e.jpg) 那么现在就开始了,分析一下,其实就是一个圆角矩形 加上一个三角。画出来就是了。 首先先创建一个类,继承自View  ,重写他的构造方法,这里为了简便就不加自定义属性了。 ~~~ public PopupView(Context context) { this(context,null); } public PopupView(Context context, AttributeSet attrs) { this(context, attrs,0); } public PopupView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } ~~~ 然后是测量他的大小,为了方便,这里只给出EXACTLY的测量值,AT_MOST的时候大家可以自己给一个初始值,这里就不加了 里面有个mRect,其实就是圆角矩形啦。圆角矩形要比整体的view小一点,这里我直接乘上一个百分比来体现,我给的值是mRectPercent = 0.8;(这个处理方式是十分不好的,是一种错误的方式,这里我不想更正了,因为只有错误才能提高。你可以查看下一篇博客,计算了padding,让矩形处于最中间)。 ~~~ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); if(widthMode== MeasureSpec.EXACTLY){ mWidth = widthSize; //获取到当前view的宽度 mRectWidth = (int) (mWidth * mRectPercent);//计算矩形的大小 这里是直接给的初值为0.8 也就是说矩形是view大小的0.8倍 下同 } if(heightMode == MeasureSpec.EXACTLY){ mHeight = heightSize;//获取当前view的高度 mRectHeight = (int) (mHeight * mRectPercent+0.1); } setMeasuredDimension(mWidth,mHeight); } ~~~ 测量完成以后接下来就是绘制了,绘制起来也比较简单,关键是计算出关键点的坐标。这里来一张图表示一下 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea27f141.jpg) 只需要把三角的三个点的坐标计算出来就可以了。至于后面加减的数字,其实就是三角形的高和底边长 也很简单,聪明的你一定一看就懂。 那么就把这些坐标画成图形。 ~~~ @Override protected void onDraw(Canvas canvas) { Paint p = new Paint(); p.setColor(Color.parseColor("#2C97DE")); p.setStyle(Paint.Style.FILL); canvas.drawRoundRect(new RectF(0,0,mRectWidth,mRectHeight),10,10,p); Path path = new Path(); path.moveTo(mRectWidth/2-30,mRectHeight); path.lineTo(mRectWidth/2,mRectHeight+20); path.lineTo(mRectWidth/2+30,mRectHeight); path.close(); canvas.drawPath(path,p); super.onDraw(canvas); } ~~~ 其中圆角矩形是用drawRoundRect方法来绘制的,里面是一个基本的矩形,和圆角的半径 三角形是用path来绘制,首先用moveTo设定起点坐标,最后将两条线连接起来,别忘了close成一个封闭的图形。 这样就大功告成泡泡窗,不用麻烦美工啦! 如果你觉得这个自定义view比较简单可以闭着眼睛写出来,你还可以参考进阶一点点的 [自定义圆形百分比,进度条](http://blog.csdn.net/wingichoy/article/details/50334595) 进行练习,总之自定义view并不难,只要多练,就一定会掌握熟练。
';

Android 自定义View — 简约的折线图

最后更新于:2022-04-01 10:01:11

转载请注明出处:http://write.blog.csdn.net/postedit/50434634 接上篇 [Android 圆形百分比(进度条) 自定义view](http://blog.csdn.net/wingichoy/article/details/50334595) 昨天分手了,不开心,来练练自定义view麻痹自己,毕竟菜鸟只能靠不断练习提高。#程序员不应该有女朋友# 我们要实现的是一种只有来看趋势,不需要看具体数值,比较简约的折线图。比如下图这样的: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea1ada45.jpg) 这个时候,一些比较优秀的第三方图表库如:MPChart 就显得比较臃肿了。所以我们需要自定义一个折线图。 老规矩,先来看最终的实现效果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea223261.jpg) 其实这种做的很简约,大概分三个步骤: 一、画坐标轴 二、画点 三、画线 那么我们开始吧Let's go (Let it go)。 设计一下大概需要的东西。首先把X轴和Y轴的数据存放在两个String[]里。 具体的点的位置用一个Map<Integer,Integer>来存放. **步骤:** 一、新建一个类,取名为SimpleLineChart继承View 重写他的构造方法。这里为了简便,就不添加自定义属性了attr.xml。 ~~~ <span style="font-size:18px;"> </span><span style="font-size:12px;"> public SimpleLineChart(Context context) { this(context, null); } public SimpleLineChart(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SimpleLineChart(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }</span> ~~~ 二、测量大小。(继续偷懒,只支持EXACTLY,AT_MOST直接丢异常) ~~~ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (widthMode == MeasureSpec.EXACTLY) { mWidth = widthSize; }else if(widthMode == MeasureSpec.AT_MOST){ throw new IllegalArgumentException("width must be EXACTLY,you should set like android:width=\"200dp\""); } if (heightMode == MeasureSpec.EXACTLY) { mHeight = heightSize; }else if(widthMeasureSpec == MeasureSpec.AT_MOST){ throw new IllegalArgumentException("height must be EXACTLY,you should set like android:height=\"200dp\""); } setMeasuredDimension(mWidth, mHeight); } ~~~ 三、重点来了,开始draw。首先处理几种异常情况。当X轴或者Y轴为没有文字(也就是没有刻度的时候),抛出异常。 ~~~ if(mXAxis.length==0||mYAxis.length==0){ throw new IllegalArgumentException("X or Y items is null"); } ~~~ 当没有任何点的数据的时候,显示字符串提醒用户没有数据(实际上是往中心drawText)。  ~~~  //画坐标线的轴         Paint axisPaint = new Paint();         axisPaint.setTextSize(mYAxisFontSize);         axisPaint.setColor(Color.parseColor("#3F51B5")); ~~~ ~~~ if (mPointMap == null || mPointMap.size() == 0) { int textLength = (int) axisPaint.measureText(mNoDataMsg); canvas.drawText(mNoDataMsg, mWidth/2 - textLength/2, mHeight/2, axisPaint); } ~~~ 异常情况处理完了,开始上面三个步骤挨个画,首先来画个Y轴,计算每个刻度的间隔。他的值应该是mWidth / Y轴文字个数,然后用循环把每个刻度都画出来。再申请一个数组yPoints,存放每个Y刻度的具体坐标。 ~~~ //画 Y 轴 //存放每个Y轴的坐标 int[] yPoints = new int[mYAxis.length]; //计算Y轴 每个刻度的间距 int yInterval = (int) ((mHeight - mYAxisFontSize - 2) / (mYAxis.length)); //测量Y轴文字的高度 用来画第一个数 Paint.FontMetrics fm = axisPaint.getFontMetrics(); int yItemHeight = (int) Math.ceil(fm.descent - fm.ascent); Log.e("wing", mHeight + ""); for (int i = 0; i < mYAxis.length; i++) { canvas.drawText(mYAxis[i], 0, mYAxisFontSize + i * yInterval, axisPaint); yPoints[i] = (int) (mYAxisFontSize + i * yInterval); } ~~~ 我们运行一下,看到了如下效果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea235e04.jpg) 需要注意的是,这里的坐标需要微调,大家多试一下。同理开始画X轴: ~~~ //画 X 轴 //x轴的刻度集合 int[] xPoints = new int[mXAxis.length]; Log.e("wing", xPoints.length + ""); //计算Y轴开始的原点坐标 int xItemX = (int) axisPaint.measureText(mYAxis[1]); //X轴偏移量 int xOffset = 50; //计算x轴 刻度间距 int xInterval = (int) ((mWidth - xOffset) / (mXAxis.length)); //获取X轴刻度Y坐标 int xItemY = (int) (mYAxisFontSize + mYAxis.length * yInterval); for (int i = 0; i < mXAxis.length; i++) { canvas.drawText(mXAxis[i], i * xInterval + xItemX + xOffset, xItemY, axisPaint); xPoints[i] = (int) (i * xInterval + xItemX + axisPaint.measureText(mXAxis[i]) / 2 + xOffset + 10); // Log.e("wing", xPoints[i] + ""); } ~~~ 注意这里X轴的y坐标就是画Y轴时候的最下面的文字(最后一个)的坐标,存成了xItemY。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea249448.jpg) 之后我们来画点,这里我采用的方法是画圆。直接drawCircle。从map中取出所有点的对应i,j然后再从两个数组 xPoints[] yPoints[]取出真实的X,Y坐标,最后画出来 ~~~ //画点 Paint pointPaint = new Paint(); pointPaint.setColor(mLineColor); Paint linePaint = new Paint(); linePaint.setColor(mLineColor); linePaint.setAntiAlias(true); //设置线条宽度 linePaint.setStrokeWidth(mStrokeWidth); pointPaint.setStyle(Paint.Style.FILL); for (int i = 0; i < mXAxis.length; i++) { if (mPointMap.get(i) == null) { throw new IllegalArgumentException("PointMap has incomplete data!"); } //画点 canvas.drawCircle(xPoints[i], yPoints[mPointMap.get(i)], mPointRadius, pointPaint); if (i > 0) {//画线 canvas.drawLine(xPoints[i - 1], yPoints[mPointMap.get(i - 1)], xPoints[i], yPoints[mPointMap.get(i)], linePaint); } } ~~~ 上面画完点之后开始画线drawLine,参数是前一个点的坐标,和后一个点的坐标。挨个画出来。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea25d4db.jpg)   这时候我们的最复杂的绘制就完成了。接下来来加入一点功能:参数的设置。 ~~~ /** * 设置map数据 * @param data */ public void setData(HashMap<Integer,Integer> data){ mPointMap = data; invalidate(); } /** * 设置Y轴文字 * @param yItem */ public void setYItem(String[] yItem){ mYAxis = yItem; } /** * 设置X轴文字 * @param xItem */ public void setXItem(String[] xItem){ mXAxis = xItem; } public void setLineColor(int color){ mLineColor = color; invalidate(); } ~~~ 以上代码很简单 我就不多说了。整个View完工,接下来介绍如何使用。 **使用:** ****和普通的View一样,我们直接在XML布局文件中加入SimpleLineChart,注意不要忘记包名。 ~~~ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context="com.wingsofts.simplelinechart.MainActivity"> <com.wingsofts.simplelinechart.SimpleLineChart android:id="@+id/simpleLineChart" android:layout_width="400dp" android:layout_height="200dp" /> </RelativeLayout> ~~~ 然后在Activity中findviewbyid,给他设置X轴的文字 Y轴的文字 还有数据源 ~~~ public class MainActivity extends AppCompatActivity { private SimpleLineChart mSimpleLineChart; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mSimpleLineChart = (SimpleLineChart) findViewById(R.id.simpleLineChart); String[] xItem = {"1","2","3","4","5","6","7"}; String[] yItem = {"10k","20k","30k","40k","50k"}; if(mSimpleLineChart == null) Log.e("wing","null!!!!"); mSimpleLineChart.setXItem(xItem); mSimpleLineChart.setYItem(yItem); HashMap<Integer,Integer> pointMap = new HashMap(); for(int i = 0;i<xItem.length;i++){ pointMap.put(i, (int) (Math.random()*5)); } mSimpleLineChart.setData(pointMap); } } ~~~ 简单的几步,就可以得到预览图的效果了!是不是很好玩!觉得好的话评论一下,star一下。祭奠我死去的爱情。 项目下载地址(求关注 求星星 ):[点击打开链接](https://github.com/githubwing/SimpleLineChart/) 下一篇来一个比较炫 比较复杂的view 自定义仪表盘 :[时尚自定义仪表盘](http://blog.csdn.net/wingichoy/article/details/50468674)
';

Android 自定义view –圆形百分比(进度条)

最后更新于:2022-04-01 10:01:08

转载请注明出处:[http://blog.csdn.net/wingichoy/article/details/50334595](http://blog.csdn.net/wingichoy/article/details/50334595) #### 起因 ~~~ **注:本文由于是在学习过程中写的,存在大量问题(overdraw onDraw new对象),请读者们不要被误导!!解决办法见后面的博客。** 最近公司项目有需求需要用到轻量级图表如下图,是一些简单的扇形图,圆形图,折线图,虽然有好用的三方库MPChart (教程地址http://blog.csdn.net/wingichoy/article/details/50428246), 但是过于庞大,像这样的简约的界面明显不合适,又因为许久没有坚持写博客,觉得自己很是堕落,深知自学不易,于是便想写一些博客来帮助后来像我一样的初学者,所以便想把这些效果写一个系列。其中包括: 1.圆形百分比图表 2.简易折线图图表 3.简易柱状图图表 ~~~ ![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea1ada45.jpg "") #### 效果图 说了这么多没有效果图还是废话,效果图如下: ![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea1c41d2.jpg "") #### 预备知识 本篇博客的预备知识内容有: 1.自定义属性 2.自定义view 关于以上两点,可以查看 [Hongyang大神的博客:自定义View(一)](http://blog.csdn.net/lmj623565791/article/details/24252901 "Android 自定义View (一)") #### 知识点补充 如果你已经掌握了以上的基本知识,那么可以开始本篇博客的内容啦。但是还是有几点内容补充。 1.MeasureSpec对象包含了测量的模式和大小。他是一个32位的int值,其中高两位为测量的模式,低30位是测量的大小。采用位运算和运行效率有关。所以可以从一个MeasureSpec对象分别获取模式和值 如: ~~~ //获取模式 值为 EXACTLY AT_MOST UNSPECIFIED int specMode = MeasureSpec.getMode(measureSpec); //获取测量值 int specSize = MeasureSpec.getSize(measureSpec); ~~~ 2.画扇形,调用drawArc()方法,其中第一个参数是圆所在的矩形,第二个参数是开始的弧度,值得主意的是,最顶的弧度是270度,而地图东方向的弧度为0.第三个参数是需要画的弧度,第四个参数是一个布尔值,表示是否连接圆心,画为扇形,第五个是Paint,即所需要的画笔。 ~~~ canvas.drawArc(rect, 270, mEndAngle, true, sectorPaint); ~~~ #### 现在开始吧 1.首先分析需求,我们的圆形进度总共分为三层, 最底层是一个大园形,作为底色。 第二层是一个扇形,大小跟底色一样,用来表示进度。 第三层是一个小圆形,比背景色小,用于实现弧线的效果。 2.新建一个类叫做CirclePercentView.java 他继承与View类重写他的构造函数,来获取他的自定义属性。 ~~~ public CirclePercentView(Context context) { this(context, null); } public CirclePercentView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CirclePercentView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // 获取自定义属性 TypedArray a=context.obtainStyledAttributes(attrs,R.styleable.CirclePercentView, defStyleAttr, 0); //获取色带的宽度 mStripeWidth = a.getDimension(R.styleable.CirclePercentView_stripeWidth, PxUtils.dpToPx(30, context)); //获取当前的百分比 mCurPercent = a.getInteger(R.styleable.CirclePercentView_percent, 0); //获取小园的颜色 mSmallColor = a.getColor(R.styleable.CirclePercentView_smallColor,0xffafb4db); //获取大圆的颜色 mBigColor = a.getColor(R.styleable.CirclePercentView_bigColor,0xff6950a1); //获取中心文字的大小 mCenterTextSize = a.getDimensionPixelSize(R.styleable.CirclePercentView_centerTextSize,PxUtils.spToPx(20,context)); //获取园的半径 mRadius = a.getDimensionPixelSize(R.styleable.CirclePercentView_radius,PxUtils.dpToPx(100,context)); } ~~~ 3.获取到了自定义属性以后,我们来测量这个view,告诉系统这个view有多大 ~~~ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //获取测量模式 int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); //获取测量大小 int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); //如果为确定大小值,则圆的半径为宽度/2 if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) { mRadius = widthSize / 2; x = widthSize / 2; y = heightSize / 2; mWidth = widthSize; mHeight = heightSize; } //如果为wrap_content 那么View大小为圆的半径大小*2 if(widthMode == MeasureSpec.AT_MOST&&heightMode ==MeasureSpec.AT_MOST){ mWidth = (int) (mRadius*2); mHeight = (int) (mRadius*2); x = mRadius; y = mRadius; } //设置视图的大小 setMeasuredDimension(mWidth,mHeight); } ~~~ 4.测量完成后,我们就要开始画view了。首先画大圆,之后画扇形图,最后画小圆盖住 就有了圆形进度条的效果,之后把百分比进度文字画在上面就大功告成啦。 具体步骤如图:![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea1e17f4.jpg "") ![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea1f07c3.jpg "") ![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea20b96c.jpg "") ~~~ @Override protected void onDraw(Canvas canvas) { mEndAngle = (int) (mCurPercent * 3.6); //绘制大圆 Paint bigCirclePaint = new Paint(); //消除锯齿 bigCirclePaint.setAntiAlias(true); bigCirclePaint.setColor(mBigColor); //x,y 为圆心坐标 mRadius为半径 canvas.drawCircle(x, y, mRadius, bigCirclePaint); //饼状图 Paint sectorPaint = new Paint(); sectorPaint.setColor(mSmallColor); sectorPaint.setAntiAlias(true); RectF rect = new RectF(0, 0, mWidth, mHeight); //参数说明见知识补充 canvas.drawArc(rect, 270, mEndAngle, true, sectorPaint); //绘制小圆 Paint smallCirclePaint = new Paint(); smallCirclePaint.setAntiAlias(true); smallCirclePaint.setColor(mBigColor); //圆心是相同的 不过半径有差别,这个差别就是我们的 色带宽度 canvas.drawCircle(x, y, mRadius - mStripeWidth, smallCirclePaint); //绘制文本 Paint textPaint = new Paint(); String text = mCurPercent + "%"; textPaint.setTextSize(mCenterTextSize); //测量字符串长度 float textLength = textPaint.measureText(text); textPaint.setColor(Color.WHITE); //把文本画在圆心居中 canvas.drawText(text, x - textLength/2, y, textPaint); } ~~~ 值得注意的是,在编写的过程中,文字的位置总是不对,但是根据画图计算, 公式是没错的,最后才发现我先测量了字符串长度,又修改了字体的大小导致偏差。所以一定要注意这些细节,先改变字体大小,再去测量。 5.添加setPercent方法 ~~~ public void setPercent(int percent) { //百分比不可能超过100 如果超过100则抛出异常 if (percent > 100) { throw new IllegalArgumentException("percent must less than 100!"); } setCurPercent(percent); } //内部设置百分比 用于动画效果 private void setCurPercent(int percent) { mPercent = percent; new Thread(new Runnable() { @Override public void run() { for(int i =0;i<mPercent;i++){ try { Thread.sleep(15); } catch (InterruptedException e) { e.printStackTrace(); } mCurPercent = i; CirclePercentView.this.postInvalidate(); } } }).start(); } ~~~ 这里在setPercent改变的时候,开启一个线程,来不断重绘view,达到一个动画效果。其实view的动画效果就是不停地重绘 (即执行onDraw(),因为每次执行onDraw()里面的参数会变化,所以会看到不同的画面)。这样便完成了一个轻量级的圆形百分比进度条自定义View。 你可以在[https://github.com/githubwing/CirclePercentView](https://github.com/githubwing/CirclePercentView)克隆到源码。(求star) 你还可以看本系列第二篇博客:简易折线图,链接:[http://blog.csdn.net/wingichoy/article/details/50434634](http://blog.csdn.net/wingichoy/article/details/50434634)。
';

android自定义viewgroup初步之一—-抽屉菜单

最后更新于:2022-04-01 10:01:06

转载请注明出处 http://blog.csdn.net/wingichoy/article/details/47832151 几天前在慕课网上看到鸿洋老师的 自定义卫星菜单,感觉很有意思,于是看完视频以后,自己也尝试写了一遍,并且添加了可拖拽效果(光看视频是不管用的,一定要自己动手做!切记不要照着抄代码)。 有兴趣的同学可以去慕课网看看(并非广告):http://www.imooc.com/learn/300 自定义控件这个玩意呢,就得考多练,于是又写了一个抽屉效果的菜单,也是比较简单的。 老规矩,先上效果图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea13c430.jpg) 那么中间的圆圈就是卫星菜单拉,而左下角的呢,是抽屉菜单。 下面进入正题: 自定义Viewgroup的一般步骤: 写构造器,重写onMeasure(),重写onLayout(); 由于本篇博客是viewgroup初步,故全部从最简单的开始。 我们来讲抽屉菜单。 首先创建DrawerMenu类,使他继承于ViewGroup ~~~ public class DrawerMenu extends ViewGroup ~~~ 然后添加三个构造器,使用一般的方法,少参数的调用多参数的: ~~~ public DrawerMenu(Context context) { this(context, null); } public DrawerMenu(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DrawerMenu(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } ~~~ 一般在第三个构造器里,我们会使用TypedArray来获得他对应attr.xml里面的属性,这里为了简单,不给这个viewgroup添加任何自定义属性,所以构造器这样就可以。 接下来是重写onMeasure()方法。所谓Measure为测量view的大小 ~~~ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); for (int i = 0; i < count; i++) { measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec); } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } ~~~ 测量模式一共分三种,这里也不多介绍了。因为我们的子view都是wrap_content的,所以我们只要简单测量一下即可。 接下来是关键的地方,onLayout(), 此方法是为子view进行布局。告诉子view他应该在什么位置,首先,我们要布局主按钮,这里我们将它固定在左下角: ~~~ protected void onLayout(boolean changed, int l, int t, int r, int b) { layoutBottom(); } ~~~ ~~~ private void layoutBottom() { mButton_buttom = getChildAt(0); mButton_buttom.setOnClickListener(this); mWidth_button_buttom = mButton_buttom.getMeasuredWidth(); mHeight_button_buttom = mButton_buttom.getMeasuredHeight(); mButtonX = 0; mButtonY = getMeasuredHeight() - mHeight_button_buttom; mButton_buttom.layout(mButtonX, mButtonY, mWidth_button_buttom, getMeasuredHeight()); } ~~~ 主要的button 是第一个子view 。我们用getChildAt(index=0)来获得, 然后获取得到的测量好的宽和高. 最后将主按钮layout到合适的位置,如图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-21_56efaea193910.jpg) layout里面四个参数为画出圆圈地方的x,y 所以 左上角一点的位置为: 0,height - cHeight  右下角的坐标为   cWidth,height 这样我们便确定了主button的位置。 那么接下来当然是去layout 子view的位置了。相信大家也明白了,子view的位置只要找出坐标就好。所以我们这里继续确定子view的位置。 ~~~ protected void onLayout(boolean changed, int l, int t, int r, int b) { Log.i("wing", mIsChanged + ""); if (mIsChanged) { layoutBottom(); int count = getChildCount(); for (int i = 0; i < count - 1; i++) { View child = getChildAt(i + 1); int childWidth = child.getMeasuredWidth(); int childHeight = child.getMeasuredHeight(); child.layout(0, mButtonY - mHeight_button_buttom * (i + 1) * 2, childWidth, getMeasuredHeight()); child.setVisibility(GONE); } } } ~~~ 然后我们为主按钮添加监听: 来切换菜单的状态,如果菜单为关闭,那么按下的时候显示按钮,如果为开启,那么将按钮都GONE。 这里为按钮添加了动画效果,如果你还不了解安卓动画,那么看看这里:http://blog.csdn.net/wingichoy/article/details/47104433 为了好看呢,我们给每个动画的duration加了 i*100的延迟来有渐变的效果 ~~~ public void onClick(View v) { toggleMenu(); } ~~~ ~~~ private void toggleMenu() { if (mIsChanged) { int count = getChildCount(); for (int i = 0; i < count - 1; i++) { View child = getChildAt(i + 1); TranslateAnimation ta = new TranslateAnimation(-child.getMeasuredWidth(), 0, 0, 0); ta.setDuration(1000 + i * 100); child.startAnimation(ta); child.setVisibility(VISIBLE); mIsChanged = false; } } else { int count = getChildCount(); for (int i = 0; i < count - 1; i++) { View child = getChildAt(i + 1); TranslateAnimation ta = new TranslateAnimation(0, -child.getMeasuredWidth(), 0, 0); ta.setDuration(1000 + i * 100); child.startAnimation(ta); child.setVisibility(GONE); mIsChanged = true; } } ~~~ 这下我们的viewgroup基本大功告成了。添加到mainactivity的xml上来试试 ~~~ <com.wingsoft.arcmenu.DrawerMenu android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/drawer"/> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/drawer"/> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/drawer"/> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/drawer"/> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/drawer"/> </com.wingsoft.arcmenu.DrawerMenu> ~~~ 嗯。不错。  样子也实现了。 那么接下来大家动动脑筋,自己写个监听器吧~ 今天就到这里。 如果肯努力,技术很快就赶上来了~~
';

前言

最后更新于:2022-04-01 10:01:04

> 原文出处:[Android自定义view](http://blog.csdn.net/column/details/wingscustomview.html) 作者:[wingichoy](http://blog.csdn.net/wingichoy) **本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!** # Android自定义view > android自定义view 一些实例的讲解
';