学习篇--我的自定义View学习记录

本文是学习记录文章

1、打造下拉放大效果
2、下拉放大部分,并且有下拉转圈圈的
3、卫星菜单
4、自定义带入场动画的弧形百分比进度条
5、制作雷达图
6、实现流动标签布局
7、声音录制
8、拖动控件到随意位置
9、模仿ViewPager
10、NestedScrollingParent2
第一章学习文章地址:【Android】打造下拉放大效果

俗话说学好数理化走遍天下都不怕,现在是学好自定义走遍android都不怕
原文作者在demo里面一共写了3个demo,我要自己一边学习作者的思想,一边自己跟着思想来敲代码。

第一个效果学习总结
学习记录一.gif
知识摘要

(1)

xuexiFlexibleLayout flexibleLayout = (xuexiFlexibleLayout) findViewById(R.id.fv);
        flexibleLayout.setReadyListener(new OnReadyPullListener() {
            @Override
            public boolean isReady() {
                return manager.findFirstCompletelyVisibleItemPosition() == 0;
            }
        });
        flexibleLayout.setHeader(headerImage);

findFirstCompletelyVisibleItemPosition,是获取recyclerview里面第一个参数完全可见,如果此时的位置是0,在onInterceptTouchEvent里面是要被拦截的。如果不作此判断当recyclerview里面第一个参数不可见,此时顶部的放大缩小的图片同样是不可见的,你滑动的时候会发现滑动不了,因为它被拦截了

(2)

private void pullAnimator(View headerView, int headerHeight, int headerWidth, int offsitY, int maxHeight) {
        if (headerView == null) {
            return;
        }

        int pullOffset = (int) Math.pow(offsitY, 0.8);//为了模拟出类似阻尼的效果,就是offsitY的0.8次幂
        int newHeight = Math.min(maxHeight + headerHeight, pullOffset + headerHeight);
        int newWidth = (int)(((((float)newHeight / headerHeight))) * headerWidth);//算出宽度增加的量



        log(((float)(newHeight / headerHeight))+"");
        headerView.getLayoutParams().width = newWidth;
        headerView.getLayoutParams().height = newHeight;
//        //如果不要下面这2行,你运行代码,你会发现图片左边的白边部分会更大(在拖动期间)
        int margin = (newWidth - headerWidth) / 2;
        headerView.setTranslationX( -margin);

        headerView.requestLayout();
    }

Math.pow是为了模拟出阻尼的效果才写上的,还有一点要注意(float)newHeight / headerHeight,是要先把newHeight先转为float,我开始模仿写的时候,把float写在外边了,导致算出的newHeight和headerWidth是相等的。
headerView.setTranslationX( -margin)是为了解决图片会像右边拉伸,这样就不会达到我们的预期了

第二个学习总结
scrolview学习.gif

在下拉的时候,小圆圈从上而下的显示出来
第一步:设置小圆圈的位置
第二步:下拉的时候滑出小圆圈
第三步:释放小圆圈回去
第一步代码:

 public xuexiScrollFlexibleLayout setRefreshView(View refreshViewm, OnRefreshListener listener) {
        if (mRefreshView != null) {
            removeView(mRefreshView);
        }
        mRefreshView = refreshViewm;
        mRefreshListener = listener;
        FrameLayout.LayoutParams layoutParams = new LayoutParams(mRefreshSize, mRefreshSize);
        layoutParams.gravity = Gravity.CENTER_HORIZONTAL;
        mRefreshView.setLayoutParams(layoutParams);
        mRefreshView.setTranslationY(-mRefreshSize);
        addView(mRefreshView);
        return this;
    }

这里通过设置setTranslationY来定义了Y轴的位置,这样就会被隐藏起来,也设置了方向是水平居中
第二步代码:
首先被onInterceptTouchEvent拦截了事件都交要给touchEvent处理

 public void changeRefreshView(int offsetY) {
        if (!isRefreshable || mRefreshView == null || isRefreshing()){
            return;
        }
        int pullOffset = (int) Math.pow(offsetY, 0.9);
        int newHeight = Math.min(mMaxRefreshPullHeight, pullOffset);
      //这里定义了可以下拉的最大的高度了
        mRefreshView.setTranslationY(-mRefreshSize + newHeight);
        mRefreshView.setRotation(pullOffset);
        mRefreshView.requestLayout();
    }

Math.pow方法主要是模仿出阻尼效果。 mRefreshView.setTranslationY(-mRefreshSize + newHeight);
首先newHeight的高度是从小慢慢变大的,所以小圆圈的位置会慢慢从看不见转移到看见小圆圈,当然了你移动了位置需要requestLayout()刷新
setRotation,表示围绕Z轴旋转,我黏贴一张图出来

20150516112156313.jpg

图片来源地址:View的平移、缩放、旋转以及位置、坐标系

第三步代码:

 public void changeRefreshViewOnActionUp(int offsetY) {
        if (!isRefreshable || isRefreshing())
            return;
        mIsRefreshing = true;
//如果下拉的位移>给定的最大下拉高度,就继续旋转
        if (offsetY > mMaxRefreshPullHeight){
            float rotation = mRefreshView.getRotation();
            mRefreshingAnimator = ObjectAnimator.ofFloat(mRefreshView,"rotation",rotation,rotation+360);
            mRefreshingAnimator.setDuration(1000);
            mRefreshingAnimator.setInterpolator(new LinearInterpolator());
            mRefreshingAnimator.setRepeatMode(ValueAnimator.RESTART);
            mRefreshingAnimator.setRepeatCount(-1);
            mRefreshingAnimator.start();
            if (mRefreshListener != null) {
                mRefreshListener.onRefreshing();
            }
        }else {// 下拉的位移<可允许下拉的最大高度
            if (mRefreshingAnimator!=null){
                mRefreshingAnimator.cancel();
            }
            float translation = mRefreshView.getTranslationY();
//Y轴上要回到原始位置,所以先获取现在的位置然后,回到原来的位置
            ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(mRefreshView,"translationY",translation,-mRefreshSize);
            objectAnimator.setDuration(500);
            objectAnimator.setInterpolator(new LinearInterpolator());
            objectAnimator.addListener(mRefreshAnimatorListener);
            objectAnimator.start();
        }
    }
第二章学习文章地址: Android 自定义弧形旋转菜单栏——卫星菜单
GIF.gif
知识摘要
 private void startOpenAnim() {
        int count = mMenuItemResIds.size();
        List animators = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            //知识点:Math.sin(double d);参数d为弧度值,需要将度数转换成弧度值
            int tranX = -(int) (mRadius * Math.sin(Math.toRadians(90 * i / (count - 1))));
            int tranY = -(int) (mRadius * Math.cos(Math.toRadians(90 * i / (count - 1))));
            ObjectAnimator animatorX = ObjectAnimator.ofFloat(mImgViews.get(i), "translationX", 0f, tranX);
            ObjectAnimator animatorY = ObjectAnimator.ofFloat(mImgViews.get(i), "translationY", 0f, tranY);
            ObjectAnimator alpha = ObjectAnimator.ofFloat(mImgViews.get(i), "alpha", 0, 1);
            ObjectAnimator scaleX = ObjectAnimator.ofFloat(mImgViews.get(i), "scaleX", 0.1f, 1);
            ObjectAnimator scaleY = ObjectAnimator.ofFloat(mImgViews.get(i), "scaleY", 0.1f, 1);

            animators.add(animatorX);
            animators.add(animatorY);
            animators.add(alpha);
            animators.add(scaleX);
            animators.add(scaleY);
        }
        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.setDuration(mDuration);
        animatorSet.playTogether(animators);
        animatorSet.start();
        rotateMenu(0, 90);
    }

Math.sin(Math.toRadians(90 * i / (count - 1):原来Math.sin(double d)方法里面不是填写角度,而是填写弧度值,Math.toRadians就是计算弧度值的方法

第三章学习地址:Android 自定义带入场动画的弧形百分比进度条
progressBar.gif
知识点摘要
private void setAnimprogressTime(float start,float endValue) {
          //知识点ValueAnimator.ofFloat(start, endValue);
        ValueAnimator animator = ValueAnimator.ofFloat(start, endValue);
        animator.setDuration(animTime);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float fav = (float) animation.getAnimatedValue();
                progressValue = (int)fav;
                invalidate();
            }
        });
        animator.start();
    }

我最开始自己写的时候是这样写的
ValueAnimator animator = ValueAnimator.ofFloat(start, progressValue );
这里progressValue 是当前进度条的值且我在onAnimationUpdate还一直赋值给我progressValue ,结果我这样写导致我无法进入到页面,手机一片黑

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int center = getWidth() / 2;
        float radius = center - roundWidth / 2;
        /**
         * 绘制后面的整圆
         */
        paint.setStyle(Paint.Style.STROKE); //设置空心
        paint.setStrokeWidth(bgStrokeWidth); //设置圆环的宽度
        paint.setColor(roundColor);
        paint.setAntiAlias(true);  //消除锯齿
        canvas.drawCircle(center, center, radius, paint);


        /**
         * 画进度百分比
         */
        paint.setStrokeWidth(0);
        paint.setColor(textColor);
        paint.setTextSize(textSize);
        paint.setTypeface(Typeface.DEFAULT);
        if (!TextUtils.isEmpty(centerText)) {
            //如果是设置文本内容,则直接测量文本长度并绘制
            float textWidth = paint.measureText(centerText);
            canvas.drawText(centerText, center - textWidth / 2, center + textSize / 2, paint); //画出进度百分比
        } else {
            //如果是设置百分比,则计算百分比并绘制
            int percent = (int) (((float) progressValue / (float) maxValue) * 100);  //中间的进度百分比,先转换成float在进行除法运算,不然都为0
            float textWidth = paint.measureText(percent + "%");   //测量字体宽度,我们需要根据字体的宽度设置在圆环中间
            if (percent != 0) {
                canvas.drawText(percent + "%", center - textWidth / 2, center + textSize / 2, paint); //画出进度百分比
            }
        }
        paint.setStrokeWidth(progressStrokeWidth);
        paint.setColor(progressColor);
        paint.setStrokeCap(Paint.Cap.ROUND);//知识点:画线端是否带有圆角
        mArcRectF.left = center - radius;
        mArcRectF.top = center - radius;
        mArcRectF.right = center + radius;
        mArcRectF.bottom = center + radius;
        paint.setStyle(Paint.Style.STROKE);
                                        //知识点
        canvas.drawArc(mArcRectF, 90 - 180 * ((float) progressValue / (float) maxValue), 360 * progressValue / maxValue, false, paint);  //根据进度画圆弧
    }

我一直思考,为啥他可以从底部中间开始,慢慢像2变画弧线呢?这个问题我是这样想的
开始角度:90 - 180 * ((float) progressValue / (float) maxValue)
弧度值:360 * progressValue / maxValue,这个很好理解,占有百分比嘛

解释:

首先要明白drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter,)里面的startAngle,sweepAngle。
startAngle:表示开始的角度
sweepAngle:sweepAngle划过的弧度


解释.png

代码里面我们一直有调用invalidate(),所以ondrow()方法一直会调用。无论如何progressValue 都是先从0开始变化到40。
假如现在 progressValue = 0 maxValue = 100;红色的那根实线与X轴的夹角为90°弧度值=0,画弧线是按顺时针的方向,随着时间的流逝与X轴的夹角会慢慢变小最后会形成如上的效果,弧度值占整个360°的百分比也会慢慢变大,假如此时的progressValue = 29,下一秒就是30,那么实线红色的高度会比之前高一点点,弧度值会比之前大一点点,然后就真的实现了作者的那种往2边走的效果了(其实是你眼睛欺骗了你)

学习制作雷达图

Android Path 最佳实践之绘制雷达图

雷达图效果.png

做出这个图,让我断断续续的做了几天才弄出来。
这里我想分3个步骤来做
1、画出网格
2、往上面标记文字
3、区域渲染
第一步骤画出网格

//重要,我这里将画布迁移到屏幕中间,这样我们在计算的时候,不需要+/-
 canvas.translate(centerX, centerY);
 Path path = new Path();
        Path linePath = new Path();
        float r = radius / (count - 1);//每个边边的半径
        for (int i = 0; i < count; i++) {
            float curR = r * i;
            path.reset();
            linePath.reset();

            for (int j = 0; j < count; j++) {
//绘制交叉线
                linePath.moveTo(0, 0);
                linePath.lineTo((float) (curR * Math.cos(angle * j)), (float) (curR * Math.sin(angle * j)));
                canvas.drawPath(linePath, paint);
//开始绘制单个多边型
                if (j == 0) {
                    path.moveTo(curR, 0);
                } else {
                    path.lineTo((float) (curR * Math.cos(angle * j)), (float) (curR * Math.sin(angle * j)));
                }
            }
            path.close();
            canvas.drawPath(path, paint);
        }

首先这里有5个多边行,我们绘制应该从最小的那个多变行绘制开始,我们用大的半径(radius)/个数,算出平均绘制一个的半径是多大。
path.reset();linePath.reset();主要目的是为了每次画完一个6边行,就重置一下。
绘制交叉线,我们首先应该把笔放到圆心处,然后就开始绘制直线,注意使用moveTo不会导致线条是连起来的,lineTo会导致线条连起来
第二步骤绘制文字

double pi = Math.PI;
        Paint textPaint = new Paint();
        textPaint.setTextSize(35);
        Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
        float textHeight = fontMetrics.descent - fontMetrics.ascent;
        float textRadius = radius + textHeight;
        for (int i = 0; i < arrayList.size(); i++) {
            float dgress = angle * i;
            float x = (float) (textRadius * Math.cos(dgress));
            float y = (float) (textRadius * Math.sin(dgress));
            if (pi / 2 >= dgress && dgress >= 0) {//第四象限
                float dist = textPaint.measureText(arrayList.get(i)) / (arrayList.get(i).length());
                canvas.drawText(arrayList.get(i), x-30 , y, textPaint);
            } else if (dgress > pi / 2 && dgress <= 2 * pi) {//第三象限
                float dist = textPaint.measureText(arrayList.get(i)) / (arrayList.get(i).length());
                canvas.drawText(arrayList.get(i), x - dist, y, textPaint);
            }else if (dgress > 2 * pi && dgress <= pi/2 * 3){//第二象限
                float dist = textPaint.measureText(arrayList.get(i)) / (arrayList.get(i).length() - 1);
                canvas.drawText(arrayList.get(i), x - dist, y, textPaint);
            }else {//第一象限
                float dist = textPaint.measureText(arrayList.get(i)) / (arrayList.get(i).length() - 1);
                canvas.drawText(arrayList.get(i), x + dist, y, textPaint);
            }
        }

这里的第一,二,三,四象限,是这样划分的,我们将画布移动到中间后,X,Y轴正方向是第一象限


象限.png

第三步,绘制覆盖区域

 Paint fugaiPain = new Paint();
        fugaiPain.setStyle(Paint.Style.FILL_AND_STROKE);
        fugaiPain.setColor(Color.RED);
        fugaiPain.setAlpha(200);
        Path fugaiPath = new Path();

        for (int i = 0; i < integerList.size(); i++) {
            float currentScorePercnter = (Float) integerList.get(i) / 100f;
            float currentRadius = currentScorePercnter * radius;
            float x = (float) (currentRadius * Math.cos(angle * i));
            float y = (float) (currentRadius * Math.sin(angle * i));
            canvas.drawCircle(x,y,10,fugaiPain);
            if (i==0){
                fugaiPath.moveTo(x,y);
            }else {
                fugaiPath.lineTo(x,y);
            }
        }
        fugaiPath.close();
        fugaiPain.setAlpha(100);
        canvas.drawPath(fugaiPath,fugaiPain);

//全部代码

public class SixBorderView extends View {
    private Paint paint;
    private int centerX, centerY;
    private float radius;//半径
    private float angle;//角度
    private int count = 6;//绘制边的个数
    private List arrayList = new ArrayList<>();
    private List integerList = new ArrayList<>();


    public SixBorderView(Context context) {
        this(context, null);
    }

    public SixBorderView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SixBorderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        paint = new Paint();
        paint.setStrokeWidth(5);
        paint.setStyle(Paint.Style.STROKE);

        arrayList.add("物理");
        integerList.add(30f);
        arrayList.add("化学");
        integerList.add(60f);
        arrayList.add("生物");
        integerList.add(80f);
        arrayList.add("语文");
        integerList.add(50f);
        arrayList.add("地理");
        integerList.add(45f);
        arrayList.add("数学");
        integerList.add(38f);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        centerX = w / 2;
        centerY = h / 2;
        radius = Math.min(centerX, centerY) * 0.8f;
        angle = (float) (2 * Math.PI / count);
        postInvalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(centerX, centerY);
        //1、绘制出网格
        Path path = new Path();
        Path linePath = new Path();
        float r = radius / (count - 1);//每个边边的半径
        for (int i = 0; i < count; i++) {
            float curR = r * i;
            path.reset();
            linePath.reset();
            for (int j = 0; j < count; j++) {
                //绘制交叉线
                linePath.moveTo(0, 0);
                linePath.lineTo((float) (curR * Math.cos(angle * j)), (float) (curR * Math.sin(angle * j)));
                canvas.drawPath(linePath, paint);
                //开始绘制单个多边型
                if (j == 0) {
                    path.moveTo(curR, 0);
                } else {
                    path.lineTo((float) (curR * Math.cos(angle * j)), (float) (curR * Math.sin(angle * j)));
                }
            }
            path.close();
            canvas.drawPath(path, paint);
        }
        //2、绘制文字
        double pi = Math.PI;
        Paint textPaint = new Paint();
        textPaint.setTextSize(35);
        Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
        float textHeight = fontMetrics.descent - fontMetrics.ascent;
        float textRadius = radius + textHeight;
        for (int i = 0; i < arrayList.size(); i++) {
            float dgress = angle * i;
            float x = (float) (textRadius * Math.cos(dgress));
            float y = (float) (textRadius * Math.sin(dgress));
            if (pi / 2 >= dgress && dgress >= 0) {//第四象限
                float dist = textPaint.measureText(arrayList.get(i)) / (arrayList.get(i).length());
                canvas.drawText(arrayList.get(i), x-30 , y, textPaint);
            } else if (dgress > pi / 2 && dgress <= 2 * pi) {//第三象限
                float dist = textPaint.measureText(arrayList.get(i)) / (arrayList.get(i).length());
                canvas.drawText(arrayList.get(i), x - dist, y, textPaint);
            }else if (dgress > 2 * pi && dgress <= pi/2 * 3){//第二象限
                float dist = textPaint.measureText(arrayList.get(i)) / (arrayList.get(i).length() - 1);
                canvas.drawText(arrayList.get(i), x - dist, y, textPaint);
            }else {//第一象限
                float dist = textPaint.measureText(arrayList.get(i)) / (arrayList.get(i).length() - 1);
                canvas.drawText(arrayList.get(i), x + dist, y, textPaint);
            }
        }

        //3、绘制覆盖区域
        Paint fugaiPain = new Paint();
        fugaiPain.setStyle(Paint.Style.FILL_AND_STROKE);
        fugaiPain.setColor(Color.RED);
        fugaiPain.setAlpha(200);
        Path fugaiPath = new Path();

        for (int i = 0; i < integerList.size(); i++) {
            float currentScorePercnter = (Float) integerList.get(i) / 100f;
            float currentRadius = currentScorePercnter * radius;
            float x = (float) (currentRadius * Math.cos(angle * i));
            float y = (float) (currentRadius * Math.sin(angle * i));
            canvas.drawCircle(x,y,10,fugaiPain);
            if (i==0){
                fugaiPath.moveTo(x,y);
            }else {
                fugaiPath.lineTo(x,y);
            }
        }
        fugaiPath.close();
        fugaiPain.setAlpha(100);
        canvas.drawPath(fugaiPath,fugaiPain);
    }


}

学习实现流动标签布局

学习地址

aa.gif

特别需要注意的点

1、onMeasure会被多次测量,主要是因为performTranversals的原因导致的

public class TagGroupView extends ViewGroup {
    private  final String TAG = "TagGroupView";
    private int customInterval = 15;
    private int customSonPaddingLeft = 20;
    private int customSonPaddingRight = 20;
    private int customSonPaddingTop = 10;
    private int customSonPaddingBottom = 10;
    private Drawable customSonBackground = null;
    private Drawable customSonChoseBackground = null;
    private int customSonTextColor;
    private float customSonTextSize = 0;
    private ArrayList mSonTextContents = new ArrayList<>();
    private ArrayList mSonTextViews = new ArrayList<>();
    private Context mContext = null;
    private OnItemClickListener mOnItemClickListener;
    private int customSelectMode;//101单选  102多选

    /**
     * 设置标签点击事件
     *
     * @param onItemClickListener 回调接口
     */
    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.mOnItemClickListener = onItemClickListener;
    }

    public interface OnItemClickListener {
        void onItemClick(View view, int position, String sonContent);
    }

    public TagGroupView(Context context) {
        this(context,null);
    }

    public TagGroupView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
        initAttrs(context,attrs);
    }

    public TagGroupView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        //初始化自定义属性
        initAttrs(context,attrs);
    }



    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int mParentMearWidht = MeasureSpec.getSize(widthMeasureSpec);
        int line = 1;
        int currentWidth = customInterval;//子view的宽度;
        int currentHeight = 0;//子view的高度
        if (mSonTextContents.size() > 0){
            mSonTextViews.clear();
            for (int i = 0; i < mSonTextContents.size(); i++) {
                TextView textView = getSonTextView(i,mSonTextContents.get(i));
                //获取TextView的宽高
                textView.measure(0,0);
                currentHeight = textView.getMeasuredHeight() + customInterval + customInterval;
                int textViewWidht = textView.getMeasuredWidth() + customInterval;
                //判断是否可以在一行显示,如果显示不了,需要换行
                if (mParentMearWidht - currentWidth  >= textViewWidht){
                    currentWidth = currentWidth + textViewWidht;
                }else {
                    line = line + 1;
                    currentWidth = customInterval;
                }
            }
        }
        setMeasuredDimension(mParentMearWidht,currentHeight * line );
    }

    private TextView getSonTextView(int i,final String content){
        TextView textView = new TextView(mContext);
        textView.setPadding(customSonPaddingLeft,customSonPaddingTop,customSonPaddingRight,customSonPaddingBottom);
        textView.setTextColor(customSonTextColor);
        textView.setText(content);
        textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
        //去掉默认的内边距
        textView.setIncludeFontPadding(false);
        textView.setBackground(customSonBackground);
        textView.setTag(i);
        textView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                int i = (int) v.getTag();
                TextView textView1 = mSonTextViews.get(i);
                if (customSelectMode==101){//单选
                    for (TextView mSonTextView : mSonTextViews) {
                        mSonTextView.setSelected(false);
                        mSonTextView.setBackground(customSonBackground);
                    }
                    textView1.setSelected(true);
                }else {//多选
                    if (textView1.isSelected()){
                        textView1.setSelected(false);
                    }else {
                        textView1.setSelected(true);
                    }
                }

                if (textView1.isSelected()){
                    textView1.setBackground(customSonChoseBackground);
                }else {
                    textView1.setBackground(customSonBackground);
                }

                if (mOnItemClickListener!=null){
                    mOnItemClickListener.onItemClick(v,i,content);
                }
            }
        });
        mSonTextViews.add(textView);
        return textView;
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //定义left,top的默认高度
        int left = customInterval;
        int top  = customInterval;
        //整个View的宽度
        int mParentWidth = this.getMeasuredWidth();
        //移除重复的
        this.removeAllViews();
        //循环遍历,取出TextView,将TextView添加到里面去
        for (int i = 0; i < mSonTextViews.size(); i++) {
            TextView textView = mSonTextViews.get(i);
            this.addView(textView);
            //获取子View的宽高,判断一行是否可以放的下,如果放不下,需要换行
            int childHeight = textView.getMeasuredHeight() + customInterval;
            int childWidth = textView.getMeasuredWidth() + customInterval;
            if ((mParentWidth - left) >= childWidth){
                textView.layout(left,top,left + childWidth,top + childHeight);
                left = left + childWidth + customInterval;
            }else{
                left = customInterval;
                top = top + childHeight +customInterval;
                textView.layout(left,top,left + childWidth,top + childHeight);
            }
        }
    }

    /**
     * 初始化TypeArray参数
     * @param context
     * @param attrs
     */
    private void initAttrs(Context context,AttributeSet attrs){
        TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.TagGroupView);
        customSonBackground = mTypedArray.getDrawable(R.styleable.TagGroupView_customSonBackground);
        customSonChoseBackground = mTypedArray.getDrawable(R.styleable.TagGroupView_customSonChoseBackground);
        if (customSonBackground==null){
            customSonBackground = ContextCompat.getDrawable(context,R.drawable.rectangle_b_radious5_gray_solid_white);
        }
        if (customSonChoseBackground==null){
            customSonChoseBackground = ContextCompat.getDrawable(context,R.drawable.rectangle_b_radious5_red_solid_white);
        }
        customInterval = (int) mTypedArray.getDimension(R.styleable.TagGroupView_customInterval, customInterval);
        customSonPaddingLeft = (int) mTypedArray.getDimension(R.styleable.TagGroupView_customSonPaddingLeft, customSonPaddingLeft);
        customSonPaddingRight = (int) mTypedArray.getDimension(R.styleable.TagGroupView_customSonPaddingRight, customSonPaddingRight);
        customSonPaddingTop = (int) mTypedArray.getDimension(R.styleable.TagGroupView_customSonPaddingTop, customSonPaddingTop);
        customSonPaddingBottom = (int) mTypedArray.getDimension(R.styleable.TagGroupView_customSonPaddingBottom, customSonPaddingBottom);
        customSonTextSize = (int) mTypedArray.getDimension(R.styleable.TagGroupView_customSonTextSize, 0);
        customSonTextColor = mTypedArray.getColor(R.styleable.TagGroupView_customSonTextColor,getResources().getColor(R.color.blue));
        customSelectMode = mTypedArray.getInt(R.styleable.TagGroupView_customSelectMode, 101);
        if (customSonTextSize == 0) {
            customSonTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12, getResources().getDisplayMetrics());
        }
        mTypedArray.recycle();
    }

    /**
     * 设置标签内容集合
     *
     * @param sonContent 标签内容
     */
    public void setSonContent(List sonContent) {
        if (sonContent != null) {
            mSonTextContents.clear();
            mSonTextContents.addAll(sonContent);
            requestLayout();
        }
    }

    /**
     * 添加一个标签
     *
     * @param sonContent 标签内容
     */
    public void addSonContent(String sonContent) {
        mSonTextContents.add(0, sonContent);
        requestLayout();
    }
}

//为了图方便,我就直接放这里了
public class TagActivity extends Activity {
    private TagGroupView tagGroupView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tag);
        tagGroupView = findViewById(R.id.tag_taggroupview);
        List stringList = new ArrayList<>();
        for (int i=0;i<30;i++){
            stringList.add("德玛西亚"+i);
        }
        tagGroupView.setSonContent(stringList);
    }
}


    
        
        
        
        
        
        
        
        
        
        
            
            
        
    

xml




    


声音录制

学习文章地址:https://www.jianshu.com/p/6dd10a5adca8

voice.gif

实现这个很简单,只要掌握了底部下标的数字根据柱状图的个数变化规律就行
voice规律.png

首先是平分一半,黑色的0,1表示第0位,第1位,红色的表示是一个等比数列,0跟2直接相差了2个lineWidth宽度,我们2个矩形直接的宽度是lineWidth,0跟文字之间的距离也是lineWidth。由此我们可以得到公式
偶数:2 * i * lineWidth + lineWidth
奇数:(2 * i + 1) * lineWidth + lineWidth

 for (int i = 0; i < 10; i++) {
            //左边的柱状图
            rectLeft.right = parentWidth - textWidth -  2 * i * lineWidth - lineWidth;
            rectLeft.left = parentWidth - textWidth -  (2 * i +1) * lineWidth - lineWidth;
            rectLeft.top = parentHeight - mWaveList.get(i) * lineWidth / 2;
            rectLeft.bottom = parentHeight +  mWaveList.get(i) * lineWidth / 2;

            //右边的柱状图
            rectRight.left =parentWidth + textWidth +  2 * i * lineWidth + lineWidth;
            rectRight.right =  parentWidth + textWidth +  (2 * i +1) * lineWidth + lineWidth;
            rectRight.top = parentHeight - mWaveList.get(i) * lineWidth / 2;
            rectRight.bottom = parentHeight +  mWaveList.get(i) * lineWidth / 2;

            canvas.drawRect(rectLeft,paint);
            canvas.drawRect(rectRight,paint);
        }
心得

1、我们画矩形,先从左边开始画起,然后再画右边。
2、如果我们定义的这个标签宽度不够宽,但是绘画的宽度比我们定义的宽的话,会导致只能显示部分




    


 
    
        
        
        
        
        
    
 LineWaveView lineWaveView = findViewById(R.id.linwave_line_wave_voice);
  lineWaveView.startRecord();
/**
 * @date: 2019/5/13 0013
 * @author: gaoxiaoxiong
 * @description:自定义声音View
 **/
public class LineWaveView extends View {
    private static final String DEFAULT_TEXT = " 请录音 ";
    private static final int LINE_WIDTH = 9;//默认矩形波纹的宽度,9像素, 原则上从layout的attr获得
    private Paint paint = new Paint();
    private Runnable task;
    private RectF rectRight = new RectF();
    private RectF rectLeft = new RectF();
    private String text = DEFAULT_TEXT;
    private int updateSpeed;
    private int lineColor;
    private int textColor;
    private float lineWidth = LINE_WIDTH;
    private float textSize;
    private Context mContext;

    private static final int MIN_WAVE_HEIGHT = 2;//矩形线最小高
    private static final int MAX_WAVE_HEIGHT = 12;//矩形线最大高
    private static final int[] DEFAULT_WAVE_HEIGHT = {2, 2, 2, 2, 2, 2, 2, 2, 2, 2,2};
    private static final int UPDATE_INTERVAL_TIME = 100;//100ms更新一次
    private LinkedList mWaveList = new LinkedList<>();
    private float maxDb;
    public boolean isStart = false;
    public LineWaveView(Context context) {
        this(context, null);
        this.mContext = context;
    }

    public LineWaveView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        this.mContext = context;
    }

    public LineWaveView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LineWaveView);
        lineColor = typedArray.getColor(R.styleable.LineWaveView_lwvLineColor, context.getResources().getColor(R.color.blueColorPrimary));
        lineWidth = typedArray.getDimension(R.styleable.LineWaveView_lwvLineWidth, LINE_WIDTH);
        textSize = typedArray.getDimension(R.styleable.LineWaveView_lwvTextSize, context.getResources().getDimensionPixelSize(R.dimen.text_size_14));
        textColor = typedArray.getColor(R.styleable.LineWaveView_lwvTextColor, context.getResources().getColor(R.color.black_000000));
        updateSpeed = typedArray.getDimensionPixelSize(R.styleable.LineWaveView_lwvUpdateSpeed, UPDATE_INTERVAL_TIME);
        typedArray.recycle();
        //设置默认的值
        resetView(mWaveList, DEFAULT_WAVE_HEIGHT);
    }

    private synchronized void refreshElement() {
        Random random = new Random();
        maxDb = random.nextInt(5) + 2;
        int waveH = MIN_WAVE_HEIGHT + Math.round(maxDb * (MAX_WAVE_HEIGHT - MIN_WAVE_HEIGHT));
        mWaveList.add(0, waveH);
        mWaveList.removeLast();
    }

    private class MyRunnerTask implements Runnable{
        @Override
        public void run() {
            while (isStart) {
                refreshElement();
                try {
                    Thread.sleep(updateSpeed);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                postInvalidate();
            }
        }
    }

    /**
     * @date: 2019/5/27 0027
     * @author: gaoxiaoxiong
     * @description:开始任务
     **/
    public synchronized void startRecord() {
        isStart = true;
        new Thread(new MyRunnerTask(),"线程").start();
    }

    /**
     * @date: 2019/5/27 0027
     * @author: gaoxiaoxiong
     * @description:停止任务
     **/
    public synchronized void stopRecord() {
        isStart = false;
        mWaveList.clear();
        resetView(mWaveList, DEFAULT_WAVE_HEIGHT);
        postInvalidate();
    }

    private void resetView(List list, int[] array) {
        list.clear();
        for (int anArray : array) {
            list.add(anArray);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int parentWidth = getWidth() / 2;
        int parentHeight = getHeight() / 2;
        paint.setColor(textColor);
        paint.setTextSize(textSize);
        paint.setStrokeWidth(0);
        float textWidth = paint.measureText(text) / 2;
        canvas.drawText(text, parentWidth - textWidth, parentHeight - (paint.ascent() + paint.descent()) / 2, paint);
        paint.setColor(lineColor);
        paint.setStrokeWidth(lineWidth);
        paint.setStyle(Paint.Style.FILL);
        paint.setAntiAlias(true);

        for (int i = 0; i < 10; i++) {
            //左边的柱状图
            rectLeft.right = parentWidth - textWidth -  2 * i * lineWidth - lineWidth;
            rectLeft.left = parentWidth - textWidth -  (2 * i +1) * lineWidth - lineWidth;
            rectLeft.top = parentHeight - mWaveList.get(i) * lineWidth / 2;
            rectLeft.bottom = parentHeight +  mWaveList.get(i) * lineWidth / 2;

            //右边的柱状图
            rectRight.left =parentWidth + textWidth +  2 * i * lineWidth + lineWidth;
            rectRight.right =  parentWidth + textWidth +  (2 * i +1) * lineWidth + lineWidth;
            rectRight.top = parentHeight - mWaveList.get(i) * lineWidth / 2;
            rectRight.bottom = parentHeight +  mWaveList.get(i) * lineWidth / 2;

            canvas.drawRect(rectLeft,paint);
            canvas.drawRect(rectRight,paint);
        }

    }
}
8、拖动控件到随意位置

学习地址 Android之View拖拽效果

拖动控件随意位置.gif

说明:
原作者没有像我这样可以在空白的地方继续的拖动,我进行了一波完善
心得:
1、需要拖动的控件需要使用startDragAndDrop方法,该方法含有4个参数
分别是ClipData,DragShadowBuilder,Object,flags。ClipData一般我们去实现复制文本到剪切板的时候用到,DragShadowBuilder是生成一个虚拟的影像,Object是要拖动的View,flags我们传递0即可
2、setOnDragListener方法,我的白色的是一个Relayout,我想监听按钮,那么我就必须去实现他,且我return的值必须是true,否则我无法监听到我拖动View的具体位置
按钮长按监听

 dragView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                dragView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
                View.DragShadowBuilder builder = new View.DragShadowBuilder(dragView);
                // 剪切板数据,可以在DragEvent.ACTION_DROP方法的时候获取。
                ClipData data = ClipData.newPlainText("Label", "我是文本内容!");
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//参与拖动
                    dragView.startDragAndDrop(data, builder, dragView, 0);
                } else {
                    dragView.startDrag(data, builder, dragView, 0);
                }
                return true;
            }
        });

白色部分监听,蓝色部分监听

//白色部分
 relativeLayout.setOnDragListener(new View.OnDragListener() {
            @Override
            public boolean onDrag(View v, DragEvent event) {
                switch (event.getAction()) {
                    case DragEvent.ACTION_DROP://释放拖拽的view
                        float x = event.getX();
                        float y = event.getY();
                        DragView dragView = (DragView) event.getLocalState();
                        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                        layoutParams.topMargin = (int) (y - dragView.getHeight() / 2);
                        layoutParams.leftMargin = (int) (x - dragView.getWidth() / 2);
                        if (dragView.getParent() instanceof  LinearLayout){
                            ((LinearLayout)dragView.getParent()).removeView(dragView);
                            relativeLayout.addView(dragView,layoutParams);
                        }else {
                            dragView.setLayoutParams(layoutParams);
                        }

                        break;
                }
                return true;
            }
        });
//蓝色部分
        ll_dragview.setOnDragListener(new View.OnDragListener() {
            @Override
            public boolean onDrag(View v, DragEvent event) {
                switch (event.getAction()) {
                    case DragEvent.ACTION_DRAG_STARTED:
                        Log.i(TAG, "开始拖拽");
                        break;
                    case DragEvent.ACTION_DRAG_ENDED:
                        Log.i(TAG, "结束拖拽");
                        break;

                    case DragEvent.ACTION_DRAG_ENTERED:
                        Log.i(TAG, "拖拽的view进入监听的view时");
                        break;
                    case DragEvent.ACTION_DRAG_EXITED:
                        Log.i(TAG, "拖拽的view离开监听的view时");
                        ll_dragview.setBackgroundColor(Color.parseColor("#3F51B5"));
                        break;

                    case DragEvent.ACTION_DRAG_LOCATION://拖拽的view在监听view中的位置
                        break;
                    case DragEvent.ACTION_DROP://释放拖拽的view
                        float x = event.getX();
                        float y = event.getY();

                        Log.i(TAG, "释放拖拽的view:x =" + x + ",y=" + y);
                        DragView dragView = (DragView) event.getLocalState();
                        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                        layoutParams.topMargin = (int) (y - dragView.getHeight() / 2);
                        layoutParams.leftMargin = (int) (x - dragView.getWidth() / 2);
                        if (dragView.getParent() instanceof  RelativeLayout){
                            ((RelativeLayout)dragView.getParent()).removeView(dragView);
                            ll_dragview.addView(dragView,layoutParams);
                        }else {
                            dragView.setLayoutParams(layoutParams);
                        }
                        break;
                }
                return true;
            }
        });
9、模仿ViewPager

学习地址:
Android Scroller完全解析,关于Scroller你所需知道的一切
android 布局之滑动探究 scrollTo 和 scrollBy 方法使用说明

image.png

学习到的知识点
1、event.getRawX()是什么?event.getRawX()表示手指在每一个控件上距离屏幕边缘的距离
2、getScrollX()会一直叠加么?会一直叠加的,比如现在有3个,你拖动第一个,到第二个,到第三个,数值会增大的
3、有了getScrollX(),为啥还要mXLastMove - mXMove,mXLastMove - mXMove表示跟上次滑动的差距,都是在每一个控件上面滑动的距离
4、为啥要做 getScrollX() + getWidth() + scrolledX > rightBorder的判断,直接getScrollX() + scrolledX 不好么?我们滑动的时候已经滑动到了最后一个了getScrollX()是前2个已经滚动完后的距离了,所以 + getWidth() 如果还可以继续滚动的话,就会滚动出去了

public class ScrollerLayout extends ViewGroup {
    private String TAG = ScrollerLayout.class.getSimpleName();
    /**
     * 用于完成滚动操作的实例
     */
    private Scroller mScroller;

    /**
     * 判定为拖动的最小移动像素数
     */
    private int mTouchSlop;

    /**
     * 手机按下时的屏幕坐标
     */
    private float mXDown;

    /**
     * 手机当时所处的屏幕坐标
     */
    private float mXMove;

    /**
     * 上次触发ACTION_MOVE事件时的屏幕坐标
     */
    private float mXLastMove;

    /**
     * 界面可滚动的左边界
     */
    private int leftBorder;

    /**
     * 界面可滚动的右边界
     */
    private int rightBorder;

    public ScrollerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
        ViewConfiguration configuration = ViewConfiguration.get(context);
        //拖动的最小移动像素数
        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed) {
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View childView = getChildAt(i);
                childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
            }
            leftBorder = getChildAt(0).getLeft();
            rightBorder = getChildAt(childCount - 1).getRight();
            Log.i(TAG,"rightBorder:"+rightBorder);
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                mXDown = ev.getRawX();
                mXLastMove = mXDown;
            }
            break;

            case MotionEvent.ACTION_MOVE: {
                mXMove = ev.getRawX();
                float diff = Math.abs(mXMove - mXDown);
                mXLastMove = mXMove;
                if (diff > mTouchSlop) {
                    return true;
                }
            }
            break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    //问题1:getScrollX()会一直叠加么?  会一直叠加的,比如现在有3个,每一个的位置都是固定的,你拖动第一个,其它的2个都会跟着动
    //问题2:有了getScrollX(),为啥还要mXLastMove - mXMove,mXLastMove - mXMove表示跟上次滑动的差距,都是在每一个控件上面滑动的距离
    //问题3:event.getRawX()是什么?event.getRawX()表示手指在每一个控件上距离屏幕边缘的距离
    //问题4:为啥要做 getScrollX() + getWidth() + scrolledX > rightBorder的判断,直接getScrollX() + scrolledX 不好么?我们滑动的时候已经滑动到了最后一个了getScrollX()是前2个已经滚动完后的距离了,所以 + getWidth() 如果还可以继续滚动的话,就会滚动出去了
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE: {
                mXMove = event.getRawX();
                int scrolledX = (int) (mXLastMove - mXMove);
                Log.i(TAG,"mXMove:"+mXMove);
                //Log.i(TAG,"getScrollX():"+getScrollX());
                if (getScrollX() + scrolledX < leftBorder) {
                    scrollTo(leftBorder, 0);
                    return true;
                } else if (getScrollX() + getWidth() + scrolledX > rightBorder) {
                    scrollTo(rightBorder - getWidth(), 0);
                    return true;
                }
                scrollBy(scrolledX, 0);
                mXLastMove = mXMove;
            }
            break;

            case MotionEvent.ACTION_UP: {
                int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
                int dx = targetIndex * getWidth() - getScrollX();
                mScroller.startScroll(getScrollX(),0,dx,0);
                invalidate();
            }
            break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        // 第三步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }
}

9、NestedScrollingParent2

学习地址
Andorid 嵌套滑动机制 NestedScrollingParent2和NestedScrollingChild2 详解
Android嵌套滑动机制实战演练
Android NestedScrolling(嵌套滑动)机制
小结
1、recyclerView嵌套recyclerView,我子recycleView想要滑动,还是得在继承了NestedScrollingParent2里面,通过childRecyclerView.scrollBy帮助我滑动
2、recyclerView嵌套recyclerView,在第一次down下的位置如果是childRecyclerview,那么第一次响应的target就是childRecyclerView,后续的move事件,父recyclerView也会一直响应到
3、速率是矢量,是具有方向的Y轴上,竖直方向从上往下滑动是 正值,从下往上是负值
4、针对MotionEvent.ACTION_MOVE事件里面,永远都是lastX/Y-current

你可能感兴趣的:(学习篇--我的自定义View学习记录)