Android自定义之QQ身边的人

简介: Android自定义之QQ身边的人

概述

本文主要是自定义QQ的身边的人,之前有位仁兄写过,这里呢主要也参考了他的实现方法和思路来实现,但是他实现的效果还有很多细节和QQ的效果差距还是蛮多的,这里面对以加以修改及美化。实乃是高仿中的高仿。如果想看下之前大神写的可以直接点击这里 http://blog.csdn.net/mr_immortalz/article/details/51319354 ,废话不多说首先看下qq的效果界面如下:



我们实现的效果:


通过2张图可以看出来几乎是差不多了,个别小图标因为没有ui就没有去放置。因为gif图录制看起来不是很清楚,大家可以运行代码看看效果。下面就可以撸码了。

这里我们分成2个部分来实现这个效果。

一、扫描view(圆环(渐变),扫描动画,小人头等)

 1、绘制圆环(这里是多个圆环,从中间开始绘制,绘制的颜色和宽度及透明度都是需要注意的)。


 2、绘制扫描动画效果。


 3、绘制中间头像icon(图片边缘还有一个白色圆圈,细节很重要啊)。


 4、扫描上面的小人(默认分2种颜色,意思男女,如果你喜欢第三性别,可以多加一个。当切换的时候需要更换成对应的图片icon)。


  以上都是通过自定义来实现。


二、底部人物切换view(来回切换,左右透明度,按钮透明度等)

  1、这里就是通过recycleview来实现。(重写部分方法实现底部切换效果)。


注:底部背景是一个图片。


待优化效果:

  1、点击扫描界面的小人的时候,下面自动切换对应的位置。


我们已经把实现方法步骤话了,那么我们就开始撸码了.

第一步:1、自定义View的属性,首先在res/values/  下建立一个attrs.xml , 在里面定义我们的属性和声明我们的整个样式。

 <declare-styleable name="RadarView">
        <attr name="mRingColor" format="color"/>
        <attr name="mScanBgColor" format="color"/>
        <attr name="mRingWidth" format="dimension"/>
        <attr name="mRingNum" format="integer"/>
        <attr name="mScanSpeed" format="integer"/>
        <attr name="mScanAngle" format="integer"/>
    </declare-styleable>

下面创建一个自定义类RadarView.class类(这里单纯包括圆环,扫描和中间icon)。

public class RadarView extends View {
 
    //圆环颜色
    public int mRingColor;
    //扫描背景色
    public int mScanBgColor;
    //圆环的看度
    public float mRingWidth;
    //圆环数量
    public int mRingNum;
    //扫描速度 越小越快  毫秒值
    public int mScanSpeed;
    //扫描角度
    private  int mScanAngle;
    //圆环画笔
    private  Paint mRingPaint;
    //中间图片
    private  Paint mCicleIconPaint;
    //扫描画笔
    private  Paint mScanPaint;
    //中间图片
    private Bitmap mCenterIcon;
    //宽
    private int mWidth;
    //高
    private int mHeight;
    //圆环比例
    private float mRingScale=1/13f;
 
    private SweepGradient mScanShader;
    //旋转需要的矩阵
    private Matrix matrix = new Matrix();
 
    private OnScanningListener mOnScanningListener;
    //是否开始回调
    private boolean startScan;
    //当前扫描的次数
    private int currentScanningCount;
    //当前扫描显示的item
    private int currentScanningItem;
    //最大扫描次数
    private int maxScanItemCount;
    private float currentScanAngle;
    public RadarView(Context context) {
        this(context,null);
    }
 
    public RadarView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }
 
    public RadarView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray=context.obtainStyledAttributes(attrs, R.styleable.RadarView);
        mRingColor=typedArray.getColor(R.styleable.RadarView_mRingColor, Color.parseColor("#3C8EAE"));
        mScanBgColor=typedArray.getColor(R.styleable.RadarView_mScanBgColor, Color.parseColor("#84B5CA"));
        mRingWidth=typedArray.getDimensionPixelSize(R.styleable.RadarView_mRingWidth, 1);
        mRingNum=typedArray.getInteger(R.styleable.RadarView_mRingNum, 6);
        mScanSpeed=typedArray.getColor(R.styleable.RadarView_mScanSpeed, 20);
        mScanAngle=typedArray.getColor(R.styleable.RadarView_mScanAngle, 5);
        typedArray.recycle();
 
        //中间图片
        mCenterIcon = BitmapFactory.decodeResource(getResources(), R.mipmap.dabai);
        //设置多个圆环画笔
        mRingPaint=new Paint();
        mRingPaint.setColor(mRingColor);
        mRingPaint.setAntiAlias(true);
        mRingPaint.setStrokeWidth(mRingWidth);
        mRingPaint.setStyle(Paint.Style.STROKE);
 
 
        //设置中间图片画笔
        mCicleIconPaint=new Paint();
        mCicleIconPaint.setColor(Color.WHITE);
        mCicleIconPaint.setAntiAlias(true);
 
 
        //扫描画笔
        mScanPaint = new Paint();
        mScanPaint.setStyle(Paint.Style.FILL_AND_STROKE);
 
        //启动扫描
        post(mRunnable);
    }
 
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureSize(widthMeasureSpec),measureSize(heightMeasureSpec));
    }
 
    /**
     * 测量宽高  默认给400
     * @param measureSpec
     * @return
     */
    private int measureSize(int measureSpec) {
        int result;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = 400;
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }
 
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 这里设置宽高  主要思想是因为是方形 所以取最小的作为宽高值
        mWidth=getMeasuredWidth();
        mHeight=getMeasuredHeight();
        mWidth = mHeight = Math.min(mWidth, mHeight);
 
        // 绘制圆环
        drawRing(canvas);
        // 绘制扫描
        drawScan(canvas);
        // 绘制中间icon
        drawCenterIcon(canvas);
    }
 
    /**
     * 绘制中间icon
     * @param canvas
     */
    private void drawCenterIcon(Canvas canvas) {
        //这里以最中间的圆为区域设置这个中间图片 中间需要去除padding值
        float scale=0.8f;
        float radius=(mWidth-getPaddingLeft()-getPaddingRight())*mRingScale*scale;
        Rect rect=new Rect((int)(mWidth/2-radius),(int)(mHeight/2-radius),(int)(mWidth/2+radius),(int)(mHeight/2+radius));
        canvas.drawBitmap(mCenterIcon,null,rect,mCicleIconPaint);
 
        mCicleIconPaint.setColor(Color.WHITE);
        mCicleIconPaint.setStyle(Paint.Style.STROKE);
        mCicleIconPaint.setStrokeWidth(2);
        canvas.drawCircle(mWidth/2,mHeight/2,radius,mCicleIconPaint);
    }
 
    /**
     * 绘制圆环
     * @param canvas
     */
    private void drawRing(Canvas canvas) {
 
        /**
         * 这里根据设置的一共所有的圆环数量来进行遍历绘制  中间就是空间的中心, 绘制的半径就是中心向两边延伸,每次延伸设定的宽度(去除padding)与比例相乘
         */
        for (int i=0;i<mRingNum;i++){
            mRingPaint.setAlpha(getAlpha(mRingNum, i));
            canvas.drawCircle(mWidth / 2, mHeight / 2,
                    (mWidth-getPaddingLeft()-getPaddingRight()) * (1+i)*mRingScale, mRingPaint);     // 绘制小圆
        }
    }
 
    /**
     * 绘制扫描
     * @param canvas
     */
    private void drawScan(Canvas canvas) {
        /**
         *   设置扫描渲染的shader  这里需要了解下这个类
         *   SweepGradient  扫描/梯度渲染
         *   public SweepGradient(float cx, float cy, int[] colors, float[] positions)
         *   cx 渲染中心点 x 坐标
         *   cy 渲染中心 y 点坐标
         *   colors 围绕中心渲染的颜色数组,至少要有两种颜色值
         *   positions  相对位置的颜色数组,可为null,  若为null,可为null,颜色沿渐变线均匀分布
         */
        mScanShader = new SweepGradient(mWidth / 2, mHeight / 2, new int[]{Color.TRANSPARENT, mScanBgColor}, null);
        //保存画布当前的状态
        canvas.save();
        mScanPaint.setShader(mScanShader);
        //canvas.concat可以理解成对matrix的变换应用到canvas上的所有对象
        canvas.concat(matrix);
        //绘制圆
        canvas.drawCircle(mWidth / 2, mHeight / 2, (mWidth-getPaddingLeft()-getPaddingRight()) * (mRingNum-1) * mRingScale, mScanPaint);
        //取出之前保存过的状态 和save成对出现 为了不影响其他部分的绘制
        canvas.restore();
    }
    /**
     * 获取透明度  通过当前index占总共数量的count的比例来设置透明度
     * @param halfCount
     * @param index
     * @return
     */
    private int getAlpha(int halfCount, int index) {
        int MAX_ALPHA_VALUE = 255;
        int alpha= MAX_ALPHA_VALUE / halfCount * (halfCount - index);
        return index==0?0:alpha-25;
    }
 
    /**
     * 实现扫描
     */
    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            matrix.postRotate(mScanAngle, getMeasuredWidth()/2,getMeasuredWidth()/2);
            invalidate();
            postDelayed(mRunnable, mScanSpeed);
            currentScanAngle = (currentScanAngle + mScanAngle) % 360;
            if (startScan && currentScanningCount <= (360 / mScanAngle)) {
                if (mOnScanningListener != null && currentScanningCount % mScanAngle == 0
                        && currentScanningItem < maxScanItemCount) {
                    mOnScanningListener.onScanning(currentScanningItem, currentScanAngle);
                    currentScanningItem++;
                } else if (mOnScanningListener != null && currentScanningItem == maxScanItemCount) {
                    mOnScanningListener.onScanSuccess();
                }
                currentScanningCount++;
            }
        }
    };
 
    /**
     * 开始扫描
     */
    public void startScan(){
        this.startScan=true;
    }
 
    /**
     * 实现接口回调
     * @param mOnScanningListener
     */
    public void setOnScanningListener(OnScanningListener mOnScanningListener){
        this.mOnScanningListener=mOnScanningListener;
    }
 
    public interface  OnScanningListener {
        /**
         * 正在扫描(此时还没有扫描完毕)时回调
         * @param position
         * @param scanAngle
         */
        void onScanning(int position, float scanAngle);
 
        /**
         * 扫描成功时回调
         */
        void onScanSuccess();
    }
 
    /**
     * 设置圆环数量
     * @param mRingNum
     */
    public void setMaxRingNum(int mRingNum) {
        this.mRingNum = mRingNum;
    }
 
    /**
     * 设置圆环颜色
     * @param mRingColor
     */
    public void setRingColor(int mRingColor) {
        this.mRingColor = mRingColor;
    }
 
    /**
     * 设置扫描颜色
     * @param mScanBgColor
     */
    public void setScanBgColor(int mScanBgColor) {
        this.mScanBgColor = mScanBgColor;
    }
 
    /**
     * 设置圆环宽度
     * @param mRingWidth
     */
    public void setRingWidth(float mRingWidth) {
        this.mRingWidth = mRingWidth;
    }
 
    /**
     * 设置圆环数量
     * @param mRingNum
     */
    public void setRingNum(int mRingNum) {
        this.mRingNum = mRingNum;
    }
 
    /**
     * 设置扫描速度  毫秒
     * @param mScanSpeed
     */
    public void setScanSpeed(int mScanSpeed) {
        this.mScanSpeed = mScanSpeed;
    }
 
 
    /**
     * 设置旋转角度 每次刷新旋转的角度
     * @param mScanAngle
     */
    public void setScanAngle(int mScanAngle) {
        this.mScanAngle = mScanAngle;
    }
 
    /**
     * 设置圆环的半径与空间的宽度的比例
     * @param mRingScale
     */
    public void setRingScale(float mRingScale) {
        this.mRingScale = mRingScale;
    }
 
    /**
     * 设置最大扫描item数量
     * @param maxScanItemCount
     */
    public void setMaxScanItemCount(int maxScanItemCount) {
        this.maxScanItemCount = maxScanItemCount;
    }
 
}

上面我已经注释写的很详细了,也蛮简单所以就不废话了。上面这个类实现的效果是这样的:



下面就让我们实现扫描上面的小人。这里我们首先创建一个人对象:

public class People {
    //名字
    private String name;
    //年龄
    private String age;
    //头像id
    private int portraitId;
    //false为男,true为女
    private boolean sex;
    //距离
    private float distance;
 
    public int getPortraitId() {
        return portraitId;
    }
 
    public void setPortraitId(int portraitId) {
        this.portraitId = portraitId;
    }
 
    public String getAge() {
        return age;
    }
 
    public void setAge(String age) {
        this.age = age;
    }
 
    public float getDistance() {
        return distance;
    }
 
    public void setDistance(float distance) {
        this.distance = distance;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public boolean getSex() {
        return sex;
    }
 
    public void setSex(boolean sex) {
        this.sex = sex;
    }

还需要自定义一个view,主要是实现男女默认小圆和真是小人图功能。这里我简称“小人头view”。

public class RadarCircleView extends View {
    //画笔
    private Paint mPaint;
    //图片
    private Bitmap mBitmap;
    //半径
    private float radius = dp2px(getContext(),7);
    //位置X
    private float disX;
    //位置Y
    private float disY;
    //旋转的角度
    private float angle;
    //根据远近距离的不同计算得到的应该占的半径比例
    private float proportion;
    public RadarCircleView(Context context) {
        this(context,null);
    }
 
    public RadarCircleView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }
 
    public RadarCircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
 
    private void init() {
        mPaint = new Paint();
        mPaint.setColor(Color.parseColor("#FF90A2"));
        mPaint.setAntiAlias(true);
    }
 
 
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(measureSize(widthMeasureSpec), measureSize(heightMeasureSpec));
    }
 
    private int measureSize(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = dp2px(getContext(),18);
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }
 
    /**
     *  将dp值转换为px值
     * @param context
     * @param dpValue
     * @return
     */
    public  int dp2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }
 
    @Override
    protected void onDraw(Canvas canvas) {
        //画圆
        canvas.drawCircle(radius, radius, radius, mPaint);
        //如果mBitmap不为空再画小人图
        if (mBitmap != null) {
            canvas.drawBitmap(mBitmap, null, new Rect(0, 0, 2 * (int) radius, 2 * (int) radius), mPaint);
        }
    }
 
    /**
     * 设置画笔的颜色
     * @param resId
     */
    public void setPaintColor(int resId) {
        mPaint.setColor(resId);
        invalidate();
    }
 
    /**
     * 设置真实小人icon id
     * @param resId
     */
    public void setPortraitIcon(int resId) {
        mBitmap = BitmapFactory.decodeResource(getResources(), resId);
        invalidate();
    }
 
    /**
     * 清楚真实小人icon
     */
    public void clearPortaitIcon(){
        mBitmap = null;
        invalidate();
    }
 
 
 
    public float getProportion() {
        return proportion;
    }
 
    public void setProportion(float proportion) {
        this.proportion = proportion;
    }
 
    public float getAngle() {
        return angle;
    }
 
    public void setAngle(float angle) {
        this.angle = angle;
    }
 
    public float getDisX() {
        return disX;
    }
 
    public void setDisX(float disX) {
        this.disX = disX;
    }
 
    public float getDisY() {
        return disY;
    }
 
    public void setDisY(float disY) {
        this.disY = disY;
    }
}

再自定义一个viewGrop主要的作用就是为了实现上面的扫描加小人的功能,上面实现的RadarView就是这个viewGrop的一个子view ,然后我们在这个viewGrop上通过数据在上面绘制很多个“小人头view”,就达到了我们需要实现的效果。自定义的viewGrop如下代码注释很详细就多说了。

public class RadarViewLayout   extends ViewGroup implements RadarView.OnScanningListener {
    //控件的宽
    private int  mWidth;
    //控件的高
    private int  mHeight;
    //数据源
    private SparseArray<People> mDatas;
    //记录展示的item所在的扫描位置角度
    private SparseArray<Float> scanAngleList = new SparseArray<>();
    //最小距离的item所在数据源中的位置
    private int minItemPosition;
    //当前展示的item
    private RadarCircleView currentShowChild;
    //最小距离的item
    private RadarCircleView minShowChild;
    public RadarViewLayout(Context context) {
        this(context,null);
    }
 
    public RadarViewLayout(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }
 
    public RadarViewLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
 
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureSize(widthMeasureSpec), measureSize(heightMeasureSpec));
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
        mWidth = mHeight = Math.min(mWidth, mHeight);
        //测量每个children
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getId() == R.id.radarView) {
                //为雷达扫描图设置需要的属性
                ((RadarView) child).setOnScanningListener(this);
                //考虑到数据没有添加前扫描图在扫描,但是不会开始为CircleView布局
                if (mDatas != null && mDatas.size() > 0) {
                    ((RadarView) child).setMaxScanItemCount(mDatas.size());
                    ((RadarView) child).startScan();
                }
                continue;
            }
        }
    }
 
    /**
     * 测量宽高  默认给400
     * @param measureSpec
     * @return
     */
    private int measureSize(int measureSpec) {
        int result;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = 400;
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }
 
 
    @Override
    protected void onLayout(boolean changed,int l, int t, int r, int b) {
        int childCount = getChildCount();
        /**
         * 首先放置扫描view的位置
         */
        View view = findViewById(R.id.radarView);
        if (view != null) {
            view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
        }
        //放置雷达图中需要展示的item圆点
        for (int i = 0; i < childCount; i++) {
            final int j = i;
            final View child = getChildAt(i);
            //如果是扫描view直接忽略
            if (child.getId() == R.id.radarView) {
                continue;
            }
            //设置CircleView小圆点的坐标信息
            //坐标 = 旋转角度 * 半径 * 根据远近距离的不同计算得到的应该占的半径比例
            if(child instanceof RadarCircleView){
                RadarCircleView radarCircleView=(RadarCircleView) child;
                radarCircleView.setDisX((float) Math.cos(Math.toRadians(scanAngleList.get(i - 1) - 5))
                        * radarCircleView.getProportion() * mWidth / 2);
                radarCircleView.setDisY((float) Math.sin(Math.toRadians(scanAngleList.get(i - 1) - 5))
                        * radarCircleView.getProportion() * mWidth / 2);
 
                //如果扫描角度记录SparseArray中的对应的item的值为0,
                // 说明还没有扫描到该item,跳过对该item的layout
                //(scanAngleList设置数据时全部设置的value=0,
                // 当onScanning时,value设置的值始终不会0,具体可以看onScanning中的实现)
                if (scanAngleList.get(i - 1) == 0) {
                    continue;
                }
                //放置Circle小圆点
                child.layout((int)radarCircleView.getDisX() + mWidth / 2, (int) radarCircleView.getDisY() + mHeight / 2,
                        (int) radarCircleView.getDisX() + child.getMeasuredWidth() + mWidth / 2,
                        (int)radarCircleView.getDisY() + child.getMeasuredHeight() + mHeight / 2);
                //设置点击事件
                child.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        resetAnim(currentShowChild);
                        currentShowChild = (RadarCircleView) child;
                        //因为雷达图是childAt(0),所以这里需要作-1才是正确的Circle
                        startAnim(currentShowChild, j - 1);
                        if (mOnRadarClickListener != null) {
                            mOnRadarClickListener.onRadarItemClick(j - 1);
                        }
                    }
                });
            }
 
 
 
        }
    }
 
    /**
     * 设置数据
     *
     * @param mDatas
     */
    public void setDatas(SparseArray<People> mDatas) {
        this.mDatas = mDatas;
        float min = Float.MAX_VALUE;
        float max = Float.MIN_VALUE;
        //找到距离的最大值,最小值对应的minItemPosition
        for (int j = 0; j <  mDatas.size(); j++) {
            People item = mDatas.get(j);
            if (item.getDistance() < min) {
                min = item.getDistance();
                minItemPosition = j;
            }
            if (item.getDistance() > max) {
                max = item.getDistance();
            }
            scanAngleList.put(j, 0f);
        }
        //根据数据源信息动态添加CircleView
        for (int i = 0; i < mDatas.size(); i++) {
            RadarCircleView circleView = new RadarCircleView(getContext());
            if (mDatas.get(i).getSex()) {
                circleView.setPaintColor(Color.parseColor("#FF90A2"));
            } else {
                circleView.setPaintColor(Color.parseColor("#43D5FF"));
            }
            //根据远近距离的不同计算得到的应该占的半径比例 0.312-0.832
            circleView.setProportion((mDatas.get(i).getDistance() / max + 0.6f) * 0.52f);
            if (minItemPosition == i) {
                minShowChild = circleView;
            }
            addView(circleView);
        }
    }
    /**
     * 雷达图没有扫描完毕时回调
     *
     * @param position
     * @param scanAngle
     */
    @Override
    public void onScanning(int position, float scanAngle) {
        if (scanAngle == 0) {
            scanAngleList.put(position, 1f);
        } else {
            scanAngleList.put(position, scanAngle);
        }
        requestLayout();
    }
    /**
     * 雷达图扫描完毕时回调
     */
    @Override
    public void onScanSuccess() {
        resetAnim(currentShowChild);
        currentShowChild = minShowChild;
        startAnim(currentShowChild, minItemPosition);
    }
 
    /**
     * 恢复CircleView小圆点原大小
     *
     * @param object
     */
    private void resetAnim(RadarCircleView object) {
        if (object != null) {
            object.clearPortaitIcon();
            ObjectAnimator.ofFloat(object, "scaleX", 1f).setDuration(300).start();
            ObjectAnimator.ofFloat(object, "scaleY", 1f).setDuration(300).start();
        }
 
    }
 
    /**
     * 放大CircleView小圆点大小
     *
     * @param object
     * @param position
     */
    private void startAnim(RadarCircleView object, int position) {
        if (object != null) {
            object.setPortraitIcon(mDatas.get(position).getPortraitId());
            ObjectAnimator.ofFloat(object, "scaleX", 2f).setDuration(300).start();
            ObjectAnimator.ofFloat(object, "scaleY", 2f).setDuration(300).start();
        }
    }
 
    /**
     * 根据position,放大指定的CircleView小圆点
     * @param position
     */
    public void setCurrentShowItem(int position) {
        RadarCircleView child = (RadarCircleView) getChildAt(position + 1);
        resetAnim(currentShowChild);
        currentShowChild = child;
        startAnim(currentShowChild, position);
    }
 
 
    /**
     * 雷达图中点击监听CircleView小圆点回调接口
     */
    public interface OnRadarClickListener {
        void onRadarItemClick(int position);
    }
    //雷达图中点击监听CircleView小圆点回调接口
    private OnRadarClickListener mOnRadarClickListener;
 
    public void setOnRadarClickListener(OnRadarClickListener mOnRadarClickListener) {
        this.mOnRadarClickListener = mOnRadarClickListener;
    }

通过上面的实现我们的效果就有了变化了。看看我们的现在的成果:


下面我们该实现底部的效果了,之前大神实现的是通过viewpager实现的,这里我之前也有实现过这里就不多讲解viewpager实现原理这里给个链接大家可以单独去看看:

https://github.com/dalong982242260/SlidingBallViewPager  这里我们使用recycleview实现这个效果。首先我们重写下RecycleView。

public class GalleryRecyclerView extends RecyclerView {
 
    private final static int MINIMUM_SCROLL_EVENT_OFFSET_MS = 20;
 
    private boolean userScrolling = false;
    private boolean mScrolling = false;
    private int scrollState = SCROLL_STATE_IDLE;
    //最后滑动时间
    private long lastScrollTime = 0;
    //handler
    private Handler mHandler = new Handler();
    //是否支持缩放
    private boolean scaleViews = false;
    //是否支持透明度
    private boolean alphaViews = false;
    //方向  默认水平
    private Orientation orientation = Orientation.HORIZONTAL;
 
    private ChildViewMetrics childViewMetrics;
    //选中回调
    private OnViewSelectedListener listener;
    // 选中的位置position
    private int selectedPosition;
    //recycleview   LinearLayoutManager
    private LinearLayoutManager mLinearLayoutManager;
 
    private TouchDownListem listem;
    //缩放基数
    private float baseScale=0.7f;
    //缩放透明度
    private float baseAlpha=0.7f;
 
    public GalleryRecyclerView(Context context) {
        this(context, null);
    }
 
    public GalleryRecyclerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
 
    public GalleryRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
 
    private void init() {
        setHasFixedSize(true);
        setOrientation(orientation);
        enableSnapping();
    }
 
    private boolean scrolling;
 
    /**
     * 获取当前位置position
     * @return
     */
    public int getCurrentPosition(){
        return selectedPosition;
    }
    @Override
    public void onChildAttachedToWindow(View child) {
        super.onChildAttachedToWindow(child);
 
        if (!scrolling && scrollState == SCROLL_STATE_IDLE) {
            scrolling = true;
            scrollToView(getCenterView());
            updateViews();
        }
    }
 
    private void enableSnapping() {
        getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                getViewTreeObserver().removeGlobalOnLayoutListener(this);
            }
        });
 
        addOnScrollListener(new OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                updateViews();
                super.onScrolled(recyclerView, dx, dy);
            }
 
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
 
                /** if scroll is caused by a touch (scroll touch, not any touch) **/
                if (newState == SCROLL_STATE_TOUCH_SCROLL) {
                    /** if scroll was initiated already, it would probably be a tap **/
                    /** if scroll was not initiated before, this is probably a user scrolling **/
                    if (!mScrolling) {
                        userScrolling = true;
                    }
                } else if (newState == SCROLL_STATE_IDLE) {
                    /** if user is the one scrolling, snap to the view closest to center **/
                    if (userScrolling) {
                        scrollToView(getCenterView());
                    }
 
                    userScrolling = false;
                    mScrolling = false;
 
                    /** if idle, always check location and correct it if necessary, this is just an extra check **/
                    if (getCenterView() != null && getPercentageFromCenter(getCenterView()) > 0) {
                        scrollToView(getCenterView());
                    }
 
                    /** if idle, notify listeners of new selected view **/
                    notifyListener();
                } else if (newState == SCROLL_STATE_FLING) {
                    mScrolling = true;
                }
 
                scrollState = newState;
            }
        });
    }
 
    /**
     * 通知回调并设置当前选中位置
     */
    private void notifyListener() {
        View view = getCenterView();
        int position = getChildAdapterPosition(view);
        /** if there is a listener and the index is not the same as the currently selected position, notify listener **/
        if (listener != null && position != selectedPosition) {
            listener.onSelected(view, position);
        }
        selectedPosition = position;
    }
 
    /**
     * 设置方向 水平 or 竖直
     * @param orientation LinearLayoutManager.HORIZONTAL or LinearLayoutManager.VERTICAL
     */
    public void setOrientation(Orientation orientation) {
        this.orientation = orientation;
        childViewMetrics = new ChildViewMetrics(orientation);
        mLinearLayoutManager=new LinearLayoutManager(getContext(), orientation.intValue(), false);
        setLayoutManager(mLinearLayoutManager);
    }
 
    /**
     * 设置选择position
     * @param position
     */
    public void setSelectPosition(int position){
        mLinearLayoutManager.scrollToPositionWithOffset(position,0);
    }
 
 
    /**
     * 设置选中回调接口
     * @param listener the OnViewSelectedListener
     */
    public void setOnViewSelectedListener(OnViewSelectedListener listener) {
        this.listener = listener;
    }
 
    /**
     * 设置两边是否可以缩放
     * @param enabled
     */
    public void setCanScale(boolean enabled) {
        this.scaleViews = enabled;
    }
 
    /**
     * 设置两边的透明度是否支持
     * @param enabled
     */
    public void setCanAlpha(boolean enabled) {
        this.alphaViews = enabled;
    }
 
 
    /**
     * 设置基数缩放值
     * @param baseScale
     */
    public void setBaseScale(float baseScale) {
        this.baseScale = 1f-baseScale;
    }
 
    /**
     * 设置基数透明度
     * @param baseAlpha
     */
    public void setBaseAlpha(float baseAlpha) {
        this.baseAlpha = 1f-baseAlpha;
    }
 
    /**
     * 更新views
     */
    private void updateViews() {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            setMarginsForChild(child);
            float percentage = getPercentageFromCenter(child);
            float scale = 1f - (baseScale * percentage);
            float alpha = 1f - (baseAlpha * percentage);
            //设置缩放
            if (scaleViews) {
                child.setScaleX(scale);
                child.setScaleY(scale);
            }
            //设置透明度
            if(alphaViews){
                child.setAlpha(alpha);
            }
            View view=child.findViewById(R.id.item_btn);
            if(view!=null){
                view.setAlpha(1f-(percentage)/(1-0.5625f));
            }
        }
    }
 
    /**
     *  Adds the margins to a childView so a view will still center even if it's only a single child
     * @param child childView to set margins for
     */
    private void setMarginsForChild(View child) {
        int lastItemIndex = getLayoutManager().getItemCount() - 1;
        int childIndex = getChildAdapterPosition(child);
 
        int startMargin = 0;
        int endMargin = 0;
        int topMargin = 0;
        int bottomMargin = 0;
 
        if (orientation == Orientation.VERTICAL) {
            topMargin = childIndex == 0 ? getCenterLocation() : 0;
            bottomMargin = childIndex == lastItemIndex ? getCenterLocation() : 0;
        } else {
            startMargin = childIndex == 0 ? getCenterLocation() : 0;
            endMargin = childIndex == lastItemIndex ? getCenterLocation() : 0;
        }
 
        /** if sdk minimum level is 17, set RTL margins **/
        if (orientation == Orientation.HORIZONTAL && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            ((ViewGroup.MarginLayoutParams) child.getLayoutParams()).setMarginStart(startMargin);
            ((ViewGroup.MarginLayoutParams) child.getLayoutParams()).setMarginEnd(endMargin);
        }
 
        /** If layout direction is RTL, swap the margins  **/
        if (ViewCompat.getLayoutDirection(child) == ViewCompat.LAYOUT_DIRECTION_RTL)
            ((ViewGroup.MarginLayoutParams) child.getLayoutParams()).setMargins(endMargin, topMargin, startMargin, bottomMargin);
        else {
            ((ViewGroup.MarginLayoutParams) child.getLayoutParams()).setMargins(startMargin, topMargin, endMargin, bottomMargin);
        }
 
        /** if sdk minimum level is 18, check if view isn't undergoing a layout pass (this improves the feel of the view by a lot) **/
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            if (!child.isInLayout())
                child.requestLayout();
        }
    }
 
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        long currentTime = System.currentTimeMillis();
 
        /** if touch events are being spammed, this is due to user scrolling right after a tap,
         * so set userScrolling to true **/
        if (mScrolling && scrollState == SCROLL_STATE_TOUCH_SCROLL) {
            if ((currentTime - lastScrollTime) < MINIMUM_SCROLL_EVENT_OFFSET_MS) {
                userScrolling = true;
            }
        }
 
        lastScrollTime = currentTime;
 
        int location = orientation == Orientation.VERTICAL ? (int)event.getY() : (int)event.getX();
 
        View targetView = getChildClosestToLocation(location);
 
        if (!userScrolling) {
            if (event.getAction() == MotionEvent.ACTION_UP) {
                if (targetView != getCenterView()) {
                    scrollToView(targetView);
                    return true;
                }
            }
        }
 
        return super.dispatchTouchEvent(event);
    }
 
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if(event.getAction()== MotionEvent.ACTION_DOWN){
            if(listem!=null)
                listem.onTouchDown();
        }
        int location = orientation == Orientation.VERTICAL ? (int)event.getY() : (int)event.getX();
        View targetView = getChildClosestToLocation(location);
        if (targetView != getCenterView()) {
            return true;
        }
        return super.onInterceptTouchEvent(event);
    }
 
    @Override
    public boolean onTouchEvent(MotionEvent e) {
        if(e.getAction()== MotionEvent.ACTION_DOWN){
            if(listem!=null)
                listem.onTouchDown();
        }
        return super.onTouchEvent(e);
    }
 
 
    public void setTouchDownlistem(TouchDownListem listem){
        this.listem=listem;
    }
    public interface TouchDownListem{
        void onTouchDown();
    }
    @Override
    public void scrollToPosition(int position) {
        childViewMetrics.size(getChildAt(0));
        smoothScrollBy(childViewMetrics.size(getChildAt(0)) * position);
    }
 
    private View getChildClosestToLocation(int location) {
        if (getChildCount() <= 0)
            return null;
 
        int closestPos = 9999;
        View closestChild = null;
 
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
 
            int childCenterLocation = (int) childViewMetrics.center(child);
            int distance = childCenterLocation - location;
 
            /** if child center is closer than previous closest, set it as closest child  **/
            if (Math.abs(distance) < Math.abs(closestPos)) {
                closestPos = distance;
                closestChild = child;
            }
        }
 
        return closestChild;
    }
 
    /**
     * Check if the view is correctly centered (allow for 10px offset)
     * @param child the child view
     * @return true if correctly centered
     */
    private boolean isChildCorrectlyCentered(View child) {
        int childPosition = (int)childViewMetrics.center(child);
        return childPosition > (getCenterLocation() - 10) && childPosition < (getCenterLocation() + 10);
    }
 
    /**
     * 获取中间的view
     * @return
     */
    public View getCenterView() {
        return getChildClosestToLocation(getCenterLocation());
    }
 
    /**
     * 滚动指定view
     * @param child
     */
    private void scrollToView(View child) {
        if (child == null)
            return;
 
        stopScroll();
 
        int scrollDistance = getScrollDistance(child);
 
        if (scrollDistance != 0)
            smoothScrollBy(scrollDistance);
    }
 
    /**
     * 获取需要滚动的距离
     * @param child
     * @return
     */
    private int getScrollDistance(View child) {
        int childCenterLocation = (int) childViewMetrics.center(child);
        return childCenterLocation - getCenterLocation();
    }
 
    private float getPercentageFromCenter(View child) {
        float center = getCenterLocation();
        float childCenter = childViewMetrics.center(child);
 
        float offSet = Math.max(center, childCenter) - Math.min(center, childCenter);
        float maxOffset = (center + childViewMetrics.size(child));
 
        return (offSet / maxOffset);
    }
 
    /**
     * 获取中间位置
     * @return
     */
    private int getCenterLocation() {
        if (orientation == Orientation.VERTICAL)
            return getMeasuredHeight() / 2;
 
        return getMeasuredWidth() / 2;
    }
 
    public void smoothScrollBy(int distance) {
        if (orientation == Orientation.VERTICAL) {
            super.smoothScrollBy(0, distance);
            return;
        }
 
        super.smoothScrollBy(distance, 0);
    }
 
    public void scrollBy(int distance) {
        if (orientation == Orientation.VERTICAL) {
            super.scrollBy(0, distance);
            return;
        }
 
        super.scrollBy(distance, 0);
    }
 
    private void scrollTo(int position) {
        int currentScroll = getScrollOffset();
        scrollBy(position - currentScroll);
    }
 
    public int getScrollOffset() {
        if (orientation == Orientation.VERTICAL)
            return computeVerticalScrollOffset();
 
        return computeHorizontalScrollOffset();
    }
 
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mHandler.removeCallbacksAndMessages(null);
    }
 
    /**
     * 绘制一个中间view
     * @param canvas
     */
    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
    }
 
 
    private static class ChildViewMetrics {
        private Orientation orientation;
 
        public ChildViewMetrics(Orientation orientation) {
            this.orientation = orientation;
        }
 
        public int size(View view) {
            if (orientation == Orientation.VERTICAL)
                return view.getHeight();
 
            return view.getWidth();
        }
 
        public float location(View view) {
            if (orientation == Orientation.VERTICAL)
                return view.getY();
 
            return view.getX();
        }
 
        public float center(View view) {
            return location(view) + (size(view) / 2);
        }
    }
 
    public enum Orientation {
        HORIZONTAL(LinearLayout.HORIZONTAL),
        VERTICAL(LinearLayout.VERTICAL);
 
        int value;
 
        Orientation(int value) {
            this.value = value;
        }
 
        public int intValue() {
            return value;
        }
    }
 
    /**
     * 中间view选中接口
     */
    public interface OnViewSelectedListener {
        void onSelected(View view, int position);
    }

再看看我们的Mainactivity

public class MainActivity extends AppCompatActivity {
    private int[] mImgs = {R.drawable.len, R.drawable.leo, R.drawable.lep,
            R.drawable.leq, R.drawable.ler, R.drawable.les, R.drawable.mln, R.drawable.mmz, R.drawable.mna,
            R.drawable.mnj, R.drawable.leo, R.drawable.leq};
    private String[] mNames = {"橘子", "花生", "菠菜", "萝卜", "豆角", "西红柿", "香蕉", "苹果",
            "小麦","大米","玉米","白菜"};
    private SparseArray<People> mDatas = new SparseArray<>();
    private List<People> mlist = new ArrayList<>();
    private RadarViewLayout mRadarViewLayout;
    private GalleryRecyclerView mGalleryRecyclerView;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }
 
    private void initView() {
        mRadarViewLayout = (RadarViewLayout) findViewById(R.id.radarViewLayout);
        mGalleryRecyclerView = (GalleryRecyclerView) findViewById(R.id.gallery);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                initData();
                mRadarViewLayout.setDatas(mDatas);
                mRadarViewLayout.setCurrentShowItem(0);
 
            }
        }, 1000);
        mGalleryRecyclerView.setCanAlpha(true);
        mGalleryRecyclerView.setCanScale(true);
        mGalleryRecyclerView.setBaseScale(0.25f);
        mGalleryRecyclerView.setBaseAlpha(0.1f);
        mGalleryRecyclerView.setAdapter(new CommonAdapter<People>(this, R.layout.item_gallery, mlist) {
            @Override
            public void convert(ViewHolder holder, final People s, int position) {
                holder.setText(R.id.name, s.getName());
                holder.setText(R.id.tv_distance, s.getAge()+" "+s.getDistance()+"km");
                holder.setImageResource(R.id.profile_image,s.getPortraitId());
                holder.getView(R.id.item_btn).setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        Toast.makeText(mContext, s.getName(), Toast.LENGTH_SHORT).show();
                    }
                });
            }
        });
        mGalleryRecyclerView.setOnViewSelectedListener(new GalleryRecyclerView.OnViewSelectedListener() {
            @Override
            public void onSelected(View view, final int position) {
                mRadarViewLayout.setCurrentShowItem(position);
            }
        });
 
        mRadarViewLayout.setOnRadarClickListener(new RadarViewLayout.OnRadarClickListener() {
            @Override
            public void onRadarItemClick(final int position) {
 
                //待完善
            }
        });
    }
 
 
    private void initData() {
        mlist.clear();
        mDatas.clear();
        for (int i = 0; i < mImgs.length; i++) {
            People info = new People();
            info.setPortraitId(mImgs[i]);
            info.setAge(((int) Math.random() * 25 + 16) + "岁");
            info.setName(mNames[i]);
            info.setSex(i % 3 == 0 ? false : true);
            info.setDistance(Math.round((Math.random() * 10) * 100) / 100);
            mDatas.put(i, info);
            mlist.add(info);
        }
    }
}

大功造成!!!

看看效果吧!


这里附上github链接:https://github.com/dalong982242260/QQNearbyPeople

相关文章
|
1月前
|
Android开发 开发者
安卓应用开发中的自定义视图
【9月更文挑战第37天】在安卓开发的海洋中,自定义视图犹如一座座小岛,等待着勇敢的探索者去发现其独特之处。本文将带领你踏上这段旅程,从浅滩走向深海,逐步揭开自定义视图的神秘面纱。
41 3
|
1月前
|
数据可视化 Android开发 开发者
安卓应用开发中的自定义View组件
【10月更文挑战第5天】在安卓应用开发中,自定义View组件是提升用户交互体验的利器。本篇将深入探讨如何从零开始创建自定义View,包括设计理念、实现步骤以及性能优化技巧,帮助开发者打造流畅且富有创意的用户界面。
86 0
|
20天前
|
搜索推荐 前端开发 Android开发
安卓应用开发中的自定义视图实现
【10月更文挑战第30天】在安卓开发的海洋中,自定义视图是那抹不可或缺的亮色,它为应用界面的个性化和交互体验的提升提供了无限可能。本文将深入探讨如何在安卓平台创建自定义视图,并展示如何通过代码实现这一过程。我们将从基础出发,逐步引导你理解自定义视图的核心概念,然后通过一个实际的代码示例,详细讲解如何将理论应用于实践,最终实现一个美观且具有良好用户体验的自定义控件。无论你是想提高自己的开发技能,还是仅仅出于对安卓开发的兴趣,这篇文章都将为你提供价值。
|
22天前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
30 5
|
1月前
|
XML 前端开发 Java
安卓应用开发中的自定义View组件
【10月更文挑战第5天】自定义View是安卓应用开发的一块基石,它为开发者提供了无限的可能。通过掌握其原理和实现方法,可以创造出既美观又实用的用户界面。本文将引导你了解自定义View的创建过程,包括绘制技巧、事件处理以及性能优化等关键步骤。
|
2月前
|
Android开发 开发者
安卓开发中的自定义视图:从入门到精通
【9月更文挑战第19天】在安卓开发的广阔天地中,自定义视图是一块充满魔力的土地。它不仅仅是代码的堆砌,更是艺术与科技的完美结合。通过掌握自定义视图,开发者能够打破常规,创造出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战应用,一步步展示如何用代码绘出心中的蓝图。无论你是初学者还是有经验的开发者,这篇文章都将为你打开一扇通往创意和效率的大门。让我们一起探索自定义视图的秘密,将你的应用打造成一件艺术品吧!
62 10
|
2月前
|
XML 编解码 Android开发
安卓开发中的自定义视图控件
【9月更文挑战第14天】在安卓开发中,自定义视图控件是一种高级技巧,它可以让开发者根据项目需求创建出独特的用户界面元素。本文将通过一个简单示例,引导你了解如何在安卓项目中实现自定义视图控件,包括创建自定义控件类、处理绘制逻辑以及响应用户交互。无论你是初学者还是有经验的开发者,这篇文章都会为你提供有价值的见解和技巧。
47 3
|
2月前
|
前端开发 Android开发 开发者
安卓应用开发中的自定义视图基础
【9月更文挑战第13天】在安卓开发的广阔天地中,自定义视图是一块神奇的画布,它允许开发者将想象力转化为用户界面的创新元素。本文将带你一探究竟,了解如何从零开始构建自定义视图,包括绘图基础、触摸事件处理,以及性能优化的实用技巧。无论你是想提升应用的视觉吸引力,还是追求更流畅的交互体验,这里都有你需要的金钥匙。
|
2月前
|
缓存 搜索推荐 Android开发
安卓应用开发中的自定义View组件实践
【9月更文挑战第10天】在安卓开发领域,自定义View是提升用户体验和实现界面个性化的重要手段。本文将通过一个实际案例,展示如何在安卓项目中创建和使用自定义View组件,包括设计思路、实现步骤以及可能遇到的问题和解决方案。文章不仅提供了代码示例,还深入探讨了自定义View的性能优化技巧,旨在帮助开发者更好地掌握这一技能。
|
2月前
|
前端开发 搜索推荐 Android开发
探索安卓开发中的自定义视图##
【9月更文挑战第6天】 在安卓应用开发的世界里,自定义视图如同绘画艺术中的色彩,它们为界面设计增添了无限可能。通过掌握自定义视图的绘制技巧,开发者能够创造出既符合品牌形象又提升用户体验的独特界面元素。本文将深入浅出地介绍如何从零开始构建一个自定义视图,包括基础框架搭建、关键绘图方法实现、事件处理机制以及性能优化策略。准备好让你的安卓应用与众不同了吗?让我们开始吧! ##