恭喜发财! — 手把手教你仿造一个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 一些实例的讲解