文章目录
- 自定义QQ步数
- QQ计步效果分析
- 自定义View分析的常用步骤
- 自定义属性
- 获取自定义属性
- 画外圆弧
- 画内圆
- 画文字
- 增加动画让其动起来
- 自定义评分控件RatingBar
- 自定义评分View效果分析
- 自定义属性
- 获取自定义属性
- 重写onMeasure()方法
- 画出对应数量的星星
- 触摸事件处理
- 自定义酷狗侧滑菜单
- 实现方式
- 代码实现
自定义QQ步数
QQ计步效果分析
- 先画出外面的蓝色的外圆
- 画出里面的红色的内圆
- 画出中间的文字
自定义View分析的常用步骤
- 分析效果
- 确定自定义属性,编写attr.xml 文件
- 在布局中使用
- 在自定义View中获取自定义属性
- 开始具体逻辑画View
自定义属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="QQStepView">
<attr name="outColor" format="color" />
<attr name="innerColor" format="color" />
<attr name="stepTextColor" format="color" />
<attr name="stepTextSize" format="dimension" />
//圆环宽度 外圆减去内圆
<attr name="borderWidth" format="dimension"/>
//内圆半径
<attr name="radius" format="dimension"/>
</declare-styleable>
</resources>
获取自定义属性
TypedArray array=context.obtainStyledAttributes(attrs,R.styleable.QQStepView);
mInnerColor=array.getColor(R.styleable.QQStepView_innerColor,Color.RED);
mOutColor=array.getColor(R.styleable.QQStepView_outColor,Color.BLUE);
mBorderWidth= (int)array.getDimension(R.styleable.QQStepView_borderWidth,mBorderWidth) mStepTextSize=array.getDimensionPixelSize(R.styleable.QQStepView_stepTextSize,mStepTextSize);
mStepTextColor=array.getColor(R.styleable.QQStepView_stepTextColor,mStepTextColor);
initPaints();
array.recycle();
画外圆弧
//startAngle 开始角度 x轴正向是0度 sweepAngle 顺时针扫过的角度
RectF rectF=new RectF(0,0,getWidth(),getHeight());
canvas.drawArc(rectF,135,270,false,mOutPaint);
现象:
-
可以发现最终画出来的圆弧外边少了一部分,少的一部分是mBorderWidth/2,如图中的方形的框就是我们的View的大小,这个大小已经死定了的,我们要向让圆弧全部都显露出来就需要把里面画的区域缩小,这样才能全部显露出来。所以将RectF 缩小 mBorderWidth/2
-
画出来的两边是条直线很不美观,需要将其改为圆形边框 pan
private void initPaints() { mOutPaint=new Paint(); //设置抗锯齿 mOutPaint.setAntiAlias(true); mOutPaint.setStrokeWidth(mBorderWidth); mOutPaint.setColor(mOutColor); //设置边缘 mOutPaint.setStrokeCap(Paint.Cap.ROUND); //设置空心 mOutPaint.setStyle(Paint.Style.STROKE); }
画内圆
内圆的画法跟外圆不一样了,内圆就需要动态的随着步数的变化而变化。定义总共的步数和现在的步数。然后算出比例,根据比例算出应该画的角度。
//总共的 当前的
private int mStepMax=100;
private int currentStep=50;
//画内圆
float sweepAngle=(float)currentStep/mStepMax;
canvas.drawArc(rectF,135,sweepAngle*270,false,mInnerPaint);
画文字
//画文字
String setpText=currentStep+"";
Rect bound=new Rect();
mTextPaint.getTextBounds(setpText,0,setpText.length(),bound);
int dx=getWidth()/2-(bound.right-bound.left)/2;
//基线 baseLine
Paint.FontMetricsInt fontMetricsInt=mTextPaint.getFontMetricsInt();
int dy=(fontMetricsInt.bottom-fontMetricsInt.top)/2-fontMetricsInt.bottom;
int baseLine=getWidth()/2+dy;
canvas.drawText(setpText,dx,baseLine,mTextPaint);
增加动画让其动起来
public void setMaxStep(int maxStep){
this.mStepMax=maxStep;
}
public void setCurrentStep(int currentStep){
this.currentStep=currentStep;
//重新绘制
invalidate();
}
public class MainActivity extends AppCompatActivity {
ObjectAnimator objectAnimator;
ValueAnimator valueAnimator;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final QQStepView qqStepView=findViewById(R.id.stepView);
qqStepView.setMaxStep(4000);
valueAnimator=ObjectAnimator.ofInt(0,2000);
valueAnimator.setInterpolator(new AccelerateInterpolator());
valueAnimator.setDuration(2000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value= (int) animation.getAnimatedValue();
qqStepView.setCurrentStep(value);
}
});
valueAnimator.start();
}
}
自定义评分控件RatingBar
自定义评分View效果分析
- 两张星星图片
- 星星数量
自定义属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RatingBar">
<attr name="starNormal" format="reference"/>
<attr name="starFocus" format="reference"/>
<attr name="starNum" format="integer"/>
</declare-styleable>
</resources>
获取自定义属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RatingBar);
int starNormalId = typedArray.getResourceId(R.styleable.RatingBar_starNormal, 0);
if (starNormalId == 0) {
throw new RuntimeException("请设置属性 starNormal");
}
mStarNormalBitmap = BitmapFactory.decodeResource(getResources(), starNormalId);
int starFocusId = typedArray.getResourceId(R.styleable.RatingBar_starFocus, 0);
if (starFocusId == 0) {
throw new RuntimeException("请设置属性 starNormal");
}
mStarFocusBitmap = BitmapFactory.decodeResource(getResources(), starFocusId);
mStarNum = typedArray.getInt(R.styleable.RatingBar_starNum, mStarNum);
重写onMeasure()方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//指定控件宽高 控件的高即是星星的高
int starHeight = mStarFocusBitmap.getHeight();
int starWidth = mStarFocusBitmap.getWidth();
int height = starHeight + getPaddingBottom() + getPaddingTop();
int width = 0;
for (int i = 0; i < mStarNum; i++) {
width = starWidth + width + mSpace;
}
setMeasuredDimension(width + getPaddingLeft() + getPaddingRight(), height);
}
画出对应数量的星星
for (int i = 0; i < mStarNum; i++) {
canvas.drawBitmap(mStarNormalBitmap, mStarFocusBitmap.getWidth() * i + mSpace * i, 0, null);
}
触摸事件处理
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// int moveX= (int) event.getX();
// mNeedDraw=getNeedDrawNum(moveX);
// if(mNeedDraw>0){
// invalidate();
// }
case MotionEvent.ACTION_MOVE:
int moveX= (int) event.getX();
mNeedDraw=getNeedDrawNum(moveX);
if(mNeedDraw>0){
invalidate();
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
//这里一定要reture true 如果reture super.onTouchEvent 就是false false 表示不消耗这个事件,就不会继续调用 Move 方法了
return true;
}
自定义酷狗侧滑菜单
实现方式
- 继承自定义HorizontalScrollView ,写好两个布局(menu,content) ,运行起来
- 运行起来后布局是全部乱套的,menu ,content 宽度不对,需要调整
- 默认抽屉式关闭的,手指抬起的时候要判断是打开还是关闭状态
- 快速滑动的情况下需要处理
- 处理内容部分的缩放,菜单部分有位移和透明度
- 充分考虑Touch 事件分发
代码实现
package com.cailei.slidemenu;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import androidx.core.view.ViewCompat;
/**
* @author : cailei
* @date : 2020-03-23 18:02
* @description :
*/
public class SlidingMenu extends HorizontalScrollView {
private int menuWidth;
private int screenWidth;
private ViewGroup mCotentView;
private ViewGroup mMenuView;
GestureDetector gestureDetector;
private boolean isMenuOpen;
private boolean mIsIntecept = false;
public SlidingMenu(Context context) {
this(context, null);
}
public SlidingMenu(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SlidingMenu);
int menuRight = array.getInteger(R.styleable.SlidingMenu_SlidingMenu_rightMargin, ScreenUtils.dip2px(context, 50));
menuWidth = ScreenUtils.getScreenWidth(context) - menuRight;
array.recycle();
gestureDetector = new GestureDetector(context, mGestureDetector);
}
private GestureDetector.OnGestureListener mGestureDetector = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
Log.e("TAG", "velocityX" + "->" + velocityX);
//小于0 快速往左滑动 大于0 快速往右滑动
if (isMenuOpen) {
//如果侧边的菜单栏打开,快速往左滑动就关闭
if (velocityX < 0) {
closeMenu();
return true;
}
} else {
if (velocityX > 0) {
openMenu();
return true;
}
}
return super.onFling(e1, e2, velocityX, velocityY);
}
};
//宽度不对,需要知道宽高
@Override
protected void onFinishInflate() {
//布局加载完毕会调用这个方法
super.onFinishInflate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ViewGroup container = (ViewGroup) getChildAt(0);
mMenuView = (ViewGroup) container.getChildAt(0);
mMenuView.getLayoutParams().width = menuWidth;
mCotentView = (ViewGroup) container.getChildAt(1);
mCotentView.getLayoutParams().width = ScreenUtils.getScreenWidth(this.getContext());
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//初始化进来是关闭状态
closeMenu();
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if(mIsIntecept){
return true;
}
if (gestureDetector.onTouchEvent(ev)) {
return true;
}
//快速滑动触发了就不要执行
if (ev.getAction() == MotionEvent.ACTION_UP) {
//手指抬起 根据当前滚动的距离`来判断
//特别注意getScrollX 是相对与最开始的0坐标点的,不是相对于自己的手指的
int currentScrollX = getScrollX();
if (currentScrollX > menuWidth / 2) {
//超过菜单的一半 关闭
closeMenu();
} else {
openMenu();
}
//确保super.onTouchEvent 不会执行
return true;
}
return super.onTouchEvent(ev);
}
//处理各种缩放
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
Log.e("TAG", l + "");
//算一个梯度值
float scale = 1f * l / menuWidth; //从1-0
//右边缩放最小时0.7 最大是1
float rightScale = 0.7f + 0.3f * scale;
float leftScale = 1.0f - 0.3f * scale;
//设置缩放中心点,否则就会缩到看不见
ViewCompat.setPivotX(mCotentView, 0);
ViewCompat.setPivotY(mCotentView, mCotentView.getMeasuredHeight() / 2);
ViewCompat.setScaleX(mCotentView, rightScale);
ViewCompat.setScaleY(mCotentView, rightScale);
//菜单的缩放和透明度 回拉的时候慢慢变成半透明 半透明-不透明 0.5f - 1f 缩放到不缩放 0.7f-1.0f
float alpha = 1f - 0.5f * scale;
ViewCompat.setAlpha(mMenuView, leftScale);
// ViewCompat.setTranslationY(mMenuView,leftScale);
ViewCompat.setScaleX(mMenuView, leftScale);
ViewCompat.setScaleY(mMenuView, leftScale);
//最后一个效果 退出这个按钮刚开始是在右边,按照我们目前的方式退出的出字永远都是在左边
//设置平移
ViewCompat.setTranslationX(mMenuView, 0.2f * l);
}
private void closeMenu() {
smoothScrollTo(menuWidth, 0);
isMenuOpen = false;
}
private void openMenu() {
smoothScrollTo(0, 0);
isMenuOpen = true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
mIsIntecept=false;
if (isMenuOpen) {
if (ev.getX() > menuWidth) {
closeMenu();
mIsIntecept=true;
return true;
}
}
return super.onInterceptTouchEvent(ev);
}
}