概述
本文主要是自定义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