Android自定义View合集

自定义QQ步数

QQ计步效果分析

  1. 先画出外面的蓝色的外圆
  2. 画出里面的红色的内圆
  3. 画出中间的文字

自定义View分析的常用步骤

  1. 分析效果
  2. 确定自定义属性,编写attr.xml 文件
  3. 在布局中使用
  4. 在自定义View中获取自定义属性
  5. 开始具体逻辑画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);

现象:image-20200211102829406

  1. 可以发现最终画出来的圆弧外边少了一部分,少的一部分是mBorderWidth/2,如图中的方形的框就是我们的View的大小,这个大小已经死定了的,我们要向让圆弧全部都显露出来就需要把里面画的区域缩小,这样才能全部显露出来。所以将RectF 缩小 mBorderWidth/2

  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效果分析

  1. 两张星星图片
  2. 星星数量

自定义属性

<?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;
    }

自定义酷狗侧滑菜单

实现方式

  1. 继承自定义HorizontalScrollView ,写好两个布局(menu,content) ,运行起来
  2. 运行起来后布局是全部乱套的,menu ,content 宽度不对,需要调整
  3. 默认抽屉式关闭的,手指抬起的时候要判断是打开还是关闭状态
  4. 快速滑动的情况下需要处理
  5. 处理内容部分的缩放,菜单部分有位移和透明度
  6. 充分考虑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);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小菜的OnePiece

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值