文章目录
- 自定义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);
    }
}



















