本文为原创作品,转载请注明出处:/wjj1996825/article/details/80646526
前段时间公司项目要求做一个特效的滑动选择器,效果如下图的样子:
功能要求:两边的半圆形转盘可以转动,转盘上的图标也一起滚动,蓝红色图标指着的小图标变成高亮选中状态。
第一眼看到这个需求就想到这个必须要用自定义控件来做才行,于是产生了这样的思路:
半圆形的滚动的转盘自定义view继承viewgroup,重写滑动事件,自定义圆盘上图片的摆放角度,至于蓝色和红色箭头图标指向的选中状态可以用坐标数组绘制一个区域来判断是否有符合条件的图标滚动到了这个位置,如果有的话就将这个图标所在的控件透明度设置为1,如果没到这个位置就设置为非选中状态0.5透明度 ,思路这样定下来了,预计可以行得通,于是开始进行实际的尝试写代码实现这个自定义的控件和功能。
下面我直接把核心代码附上,注释比较清晰:
attrs.xml文件代码:
<!--自定义半圆形展示效果转盘选择器控件--><declare-styleable name="ringview_half"><attr name="image_angle_rh" format="integer" /><attr name="image_padding_rh" format="integer" /><attr name="max_speed_rh" format="integer" /><attr name="min_speed_rh" format="integer" /><attr name="list_rh" format="integer" /><attr name="can_scroll_rh" format="boolean" /><attr name="is_right_select_icon_rh" format="boolean" /></declare-styleable>
自定义控件的类代码:
import android.content.Context;import android.content.res.TypedArray;import android.graphics.Rect;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.view.ViewGroup;import android.widget.ImageView;import com.wj.R;import com.wj.utils.DensityUtil;import com.wj.utils.ScreenUtils;import java.util.ArrayList;import java.util.List;/*** @time /6/8* @author JunJieW* @since 1376881525@* @description 自定义半圆形展示效果转盘选择器控件*/public class RingViewHalf extends ViewGroup {/*** 上一次滑动的坐标*/private float mLastX;private float mLastY;/*** 检测按下到抬起时使用的时间*/private long mDownTime;/*** 自动滚动线程*/private ScrollResetRunnable mScrollResetRunnable;/*** 检测按下到抬起时旋转的角度*/private float mTmpAngle;/*** 每秒最大移动角度*/private int mMax_Speed;/*** 如果移动角度达到该值,则屏蔽点击*/private int mMin_Speed;/*** 圆的直径*/private int mRadius;/*** 判断是否正在自动滚动*/private boolean isMove;/*** 布局滚动角度*/private int mStartAngle = 0;/*** 中间条的宽度*/private int mCircleLineStrokeWidth;/*** 图片内容偏移角度*/private int mImageAngle;/*** 是否初始化布局*/private boolean isChekc = false;/*** 布局view*/private List<Integer> mImageList = new ArrayList<>();/*** 是否可点击*/private boolean isCanClick = true;/*** 图片与环之间的padding*/private int mPadding;/*** 是否是右边居中的图标为选中图标*/private boolean is_right_select_icon = true;/*** 是否是右边居中的图标为选中图标*/private Rect select_icon_rect = new Rect();//是否能转动private boolean mCanScrool;public RingViewHalf(Context context) {this(context, null, 0);}public RingViewHalf(Context context, AttributeSet attrs) {this(context, attrs, 0);}public RingViewHalf(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);//获取自定义控件设置的值TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ringview_half, 0, 0);mMax_Speed = array.getInteger(R.styleable.ringview_half_max_speed_rh, 300);mMin_Speed = array.getInteger(R.styleable.ringview_half_min_speed_rh, 3);mImageAngle = array.getInteger(R.styleable.ringview_half_image_angle_rh, 0);mPadding = array.getInteger(R.styleable.ringview_half_image_padding_rh, 0);mCanScrool = array.getBoolean(R.styleable.ringview_half_can_scroll_rh, true);is_right_select_icon = array.getBoolean(R.styleable.ringview_half_is_right_select_icon_rh, true);//获取xml定义的资源文件TypedArray mList = context.getResources().obtainTypedArray(array.getResourceId(R.styleable.ringview_half_list_rh, 0));int len = mList.length();if (len > 0) {for (int i = 0; i < len; i++)mImageList.add(mList.getResourceId(i, 0));} else {mImageList.add(R.mipmap.icon);mImageList.add(R.mipmap.icon);mImageList.add(R.mipmap.icon);}mList.recycle();array.recycle();int [] location =new int [2];getLocationInWindow(location);Log.d("locationInWindow",">>>>X=="+location[0]+"y=="+location[1]);addImgIcon();}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {if (!isChekc) {initView();mRadius = getWidth();isChekc = true;}}/*** 测量*/@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);super.onMeasure(widthMeasureSpec, heightMeasureSpec);int childCount = this.getChildCount();for (int i = 0; i < childCount; i++) {View child = this.getChildAt(i);this.measureChild(child, widthMeasureSpec, heightMeasureSpec);child.getMeasuredWidth();}}/*** 排版布局*/private void initView() {int width = this.getWidth();int height = this.getHeight();if (width != height) {int min = Math.min(width, height);width = min;height = min;}//不同屏幕分辨率下做不同的处理float instPadding = 70f;if (ScreenUtils.getScreenWidth(getContext())<=720){instPadding = 55f;}//图片摆放的圆弧半径mCircleLineStrokeWidth = getChildAt(0).getMeasuredHeight() + DensityUtil.dip2px(getContext(),instPadding) + mPadding;//计算图片圆的半径final int mContent = width / 2 - mCircleLineStrokeWidth / 2;for (int i = 0; i < getChildCount(); i++) {View child = this.getChildAt(i);//计算每个图片摆放的角度int mAnGle = 360 / mImageList.size() * (i + 1) + mImageAngle;//获取每个图片摆放的左上角的x和y坐标float left = (float) (width / 2 + mContent * Math.cos(mAnGle * Math.PI / 180)) - child.getMeasuredWidth() / 2;float top = (float) (height / 2 + mContent * Math.sin(mAnGle * Math.PI / 180)) - child.getMeasuredHeight() / 2;/*** 一四象限*/if (getQuadrantByAngle(mAnGle) == 1 || getQuadrantByAngle(mAnGle) == 4) {//child.setRotation(mAnGle - 270);/*** 二三象限*/} else {//child.setRotation(mAnGle + 90);}child.layout((int) left, (int) top, (int) left + child.getMeasuredWidth(), (int) top + child.getMeasuredHeight());}}/*** 添加子控件*/private void addImgIcon() {for (int i = 1; i < mImageList.size() + 1; i++) {//新建imageviewfinal ImageView mImageView = new ImageView(getContext());mImageView.setImageResource(mImageList.get(i - 1));LayoutParams layoutParams = null;mImageView.setScaleType(ImageView.ScaleType.FIT_XY);if (is_right_select_icon){//右侧icon为选中状态if (i==mImageList.size()){mImageView.setAlpha(1f);layoutParams = new LayoutParams(DensityUtil.dip2px(getContext(),40f), DensityUtil.dip2px(getContext(),40f));}else {mImageView.setAlpha(0.5f);layoutParams = new LayoutParams(DensityUtil.dip2px(getContext(),40f), DensityUtil.dip2px(getContext(),40f));}}else {// 左侧icon为选中状态if (i==5){mImageView.setAlpha(1f);layoutParams = new LayoutParams(DensityUtil.dip2px(getContext(),40f), DensityUtil.dip2px(getContext(),40f));}else {mImageView.setAlpha(0.5f);layoutParams = new LayoutParams(DensityUtil.dip2px(getContext(),40f), DensityUtil.dip2px(getContext(),40f));}}mImageView.setLayoutParams(layoutParams);final int finalI = i;//添加点击事件mImageView.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View view) {if (isCanClick) {// Toast.makeText(getContext(),finalI + " ---", Toast.LENGTH_SHORT).show();if (mOnLogoItemClick != null)mOnLogoItemClick.onItemClick(view, finalI - 1);}}});//添加viewaddView(mImageView);}//添加view点击事件setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View view) {if (isCanClick) {}}});}/*** 触摸监听*/@Overridepublic boolean dispatchTouchEvent(MotionEvent event) {if (mCanScrool) {float x = event.getX();float y = event.getY();switch (event.getAction()) {case MotionEvent.ACTION_DOWN:mLastX = x;mLastY = y;mDownTime = System.currentTimeMillis();mTmpAngle = 0;// 如果当前已经在快速滚动if (isMove) {// 移除快速滚动的回调removeCallbacks(mScrollResetRunnable);isMove = false;return true;}break;case MotionEvent.ACTION_MOVE:/*** 获得开始的角度*/float start = getAngle(mLastX, mLastY);/*** 获得当前的角度*/float end = getAngle(x, y);Log.e("TAG", "start = " + start + " , end =" + end);// 一四象限if (getQuadrant(x, y) == 1 || getQuadrant(x, y) == 4) {mStartAngle += end - start;mTmpAngle += end - start;//二三象限} else {mStartAngle += start - end;mTmpAngle += start - end;}// 重新布局getCheck();break;case MotionEvent.ACTION_UP:// 获取每秒移动的角度float anglePerSecond = mTmpAngle * 1000/ (System.currentTimeMillis() - mDownTime);// 如果达到最大速度if (Math.abs(anglePerSecond) > mMax_Speed && !isMove) {// 惯性滚动post(mScrollResetRunnable = new ScrollResetRunnable(anglePerSecond));return true;}// 如果当前旋转角度超过minSpeed屏蔽点击if (Math.abs(mTmpAngle) > mMin_Speed) {return true;}break;}}return super.dispatchTouchEvent(event);}/*** 获取移动的角度*/private float getAngle(float xTouch, float yTouch) {double x = xTouch - (mRadius / 2d);double y = yTouch - (mRadius / 2d);return (float) (Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI);}/*** 根据当前位置计算象限*/private int getQuadrant(float x, float y) {int tmpX = (int) (x - mRadius / 2);int tmpY = (int) (y - mRadius / 2);if (tmpX >= 0) {return tmpY >= 0 ? 4 : 1;} else {return tmpY >= 0 ? 3 : 2;}}/*** 在activity的onCreate方法中获取当前自定义view中在屏幕中的绝对坐标始终为0,* 改成在onWindowFocusChanged函数中获取即可,这时view都已经加载完成* 但这里特别注意一点要:如果是fragment种使用该自定义view的话,这里的方法就应该注释掉* 因为不但获取到的矩形的值是空的,而且当你的fragment执行了跳转的逻辑后,再返回后会发* 一种特别恶心的异常,你获取到判断选中位置的矩形的left,top,right,bottom的值会和* 初始化的时候不一样,导致你选中时候的状态出现异常情况,本人已经被坑过,希望后面的同学* 一定注意吸取教训*/@Overridepublic void onWindowFocusChanged(boolean hasWindowFocus) {super.onWindowFocusChanged(hasWindowFocus);getSelectIconReft();}//获取选中icon位置的矩形范围private void getSelectIconReft() {int [] location = new int [2];getLocationOnScreen(location);//计算出右侧选中时图标的位置if (is_right_select_icon){//选中的icon动态设置宽高为60,没选中宽高55,这里60/2为选中按钮的宽度或者高度的一半,即中心点select_icon_rect.left = location[0]+getWidth()-mCircleLineStrokeWidth/2-DensityUtil.dip2px(getContext(),40f)/2;select_icon_rect.top =(location[1]+getHeight()/2)-DensityUtil.dip2px(getContext(),40f)/2;select_icon_rect.right = location[0]+getWidth()-mCircleLineStrokeWidth/2+DensityUtil.dip2px(getContext(),40f)/2;select_icon_rect.bottom = (location[1]+getHeight()/2)+DensityUtil.dip2px(getContext(),40f)/2;}else {//计算出左侧选中时图标的位置//选中的icon动态设置宽高为60,没选中宽高55,这里60/2为选中按钮的宽度或者高度的一半,即中心点select_icon_rect.left = location[0]+mCircleLineStrokeWidth/2-DensityUtil.dip2px(getContext(),40f)/2;select_icon_rect.top = (location[1]+getHeight()/2)-DensityUtil.dip2px(getContext(),40f)/2;select_icon_rect.right = location[0]+mCircleLineStrokeWidth/2+DensityUtil.dip2px(getContext(),40f)/2;select_icon_rect.bottom = (location[1]+getHeight()/2)+DensityUtil.dip2px(getContext(),40f)/2;}Log.d("onFocusChanged","-----getHeight=="+getChildAt(0).getHeight()+";getWidth=="+getChildAt(0).getWidth());}/*** 通过角度判断象限*/private int getQuadrantByAngle(int angle) {if (angle <= 90) {return 4;} else if (angle <= 180) {return 3;} else if (angle <= 270) {return 2;} else {return 1;}}/*** 惯性滚动*/private class ScrollResetRunnable implements Runnable {private float angelPerSecond;public ScrollResetRunnable(float velocity) {this.angelPerSecond = velocity;}public void run() {//小于20停止if ((int) Math.abs(angelPerSecond) < 20) {isMove = false;return;}isMove = true;// 滚动时候不断修改滚动角度大小// mStartAngle += (angelPerSecond / 30);mStartAngle += (angelPerSecond / 40);//逐渐减小这个值angelPerSecond /= 1.0666F;postDelayed(this, 30);// 重新布局getCheck();}}/*** 点击事件接口*/public interface OnLogoItemClick {void onItemClick(View view, int pos);}private OnLogoItemClick mOnLogoItemClick;private OnIconSelectedListener mOnIconSelectedListener;/*** 设置点击事件* @param mOnLogoItemClick*/public void addOnItemClick(OnLogoItemClick mOnLogoItemClick) {this.mOnLogoItemClick = mOnLogoItemClick;}/*** 到选中位置后选中事件接口*/public interface OnIconSelectedListener{void onIconSelected( int pos);}/*** 设置点击事件* @param mOnIconSelectedListener*/public void addOnIconSelectedListener(OnIconSelectedListener mOnIconSelectedListener) {this.mOnIconSelectedListener = mOnIconSelectedListener;}/*** 旋转圆盘*/private void getCheck() {mStartAngle %= 360;setRotation(mStartAngle);//改变选中的icon的状态setSelectedIcon();}//改变选中的icon的状态private void setSelectedIcon() {if (select_icon_rect.left==0&&select_icon_rect.top==0){//fragment中onWindowFocusChanged会出现计算select_icon_rect.left和select_icon_rect.top等于0的情况,// 所以做下判断,如果为0则重新调用下计算方法getSelectIconReft();}for (int j =0;j<getChildCount();j++){LayoutParams layoutParams = null;int [] location = new int [2];getChildAt(j).getLocationOnScreen(location);Log.d("getCheck","location[0]=="+location[0]+";select_icon_rect.left=="+select_icon_rect.left+"location[1]=="+location[1]+";select_icon_rect.top=="+select_icon_rect.top);if (is_right_select_icon){//右边icon是选中状态的时候if (select_icon_rect.left-22<=location[0]&&location[0]<=select_icon_rect.right+22){if (select_icon_rect.top-22<=location[1]&&location[1]<=select_icon_rect.bottom+22){getChildAt(j).setAlpha(1);layoutParams = new LayoutParams(DensityUtil.dip2px(getContext(),40f), DensityUtil.dip2px(getContext(),40f));//把选中的icon所在list中的position通过接口回传过去if (mOnIconSelectedListener!=null){mOnIconSelectedListener.onIconSelected(j);}}else {getChildAt(j).setAlpha(0.5f);layoutParams = new LayoutParams(DensityUtil.dip2px(getContext(),40f), DensityUtil.dip2px(getContext(),40f));}}else {getChildAt(j).setAlpha(0.5f);layoutParams = new LayoutParams(DensityUtil.dip2px(getContext(),40f), DensityUtil.dip2px(getContext(),40f));}}else {//左边icon是选中状态的时候if (select_icon_rect.left-22<=location[0]&&location[0]<=select_icon_rect.right+22){if (select_icon_rect.top-22<=location[1]&&location[1]<=select_icon_rect.bottom+22){getChildAt(j).setAlpha(1);layoutParams = new LayoutParams(DensityUtil.dip2px(getContext(),40f), DensityUtil.dip2px(getContext(),40f));//把选中的icon所在list中的position通过接口回传过去if (mOnIconSelectedListener!=null){mOnIconSelectedListener.onIconSelected(j);}}else {getChildAt(j).setAlpha(0.5f);layoutParams = new LayoutParams(DensityUtil.dip2px(getContext(),40f), DensityUtil.dip2px(getContext(),40f));}}else {getChildAt(j).setAlpha(0.5f);layoutParams = new LayoutParams(DensityUtil.dip2px(getContext(),40f), DensityUtil.dip2px(getContext(),40f));}}getChildAt(j).setLayoutParams(layoutParams);getChildAt(j).invalidate();Log.d("getChildCount","=="+j+";class=="+getChildAt(j).getClass()+";left=="+getChildAt(j).getLeft()+";top=="+getChildAt(j).getTop()+";right=="+getChildAt(j).getRight()+";bottom=="+getChildAt(j).getBottom()+";getLocationOnScreen:x="+location[0]+"y="+location[1]+";getRotationX=="+getRotationX()+";getRotationY=="+getRotationY());}}}
然后就是你在activity中根据回调方法获取选中的对象:
//左右侧方法相同,这里列出左侧圆盘获取方法:
view.ringView_half_left.addOnIconSelectedListener { position ->// ToDo根据postion从你的list中获取对应的选中的对象的bean类属性即可}
最后贴下布局文件:
<RelativeLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"><RelativeLayoutandroid:id="@+id/ring_left_outside_rl"android:layout_width="@dimen/dp_350"android:layout_height="@dimen/dp_350"android:layout_alignParentLeft="true"android:layout_marginLeft="-240dp"><com.wj.views.RingViewHalfandroid:id="@+id/ringView_half_left"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@drawable/bg_compatibility"app:image_angle_rh="15"app:image_padding_rh="20"app:is_right_select_icon_rh="true"app:list_rh="@array/zodiac_list" /><ImageViewandroid:layout_width="70dp"android:layout_height="70dp"android:layout_alignParentLeft="true"android:layout_centerVertical="true"android:layout_marginLeft="@dimen/dp_220"android:src="@drawable/icon_match_boy" /></RelativeLayout><RelativeLayoutandroid:id="@+id/ring_right_outside_rl"android:layout_width="@dimen/dp_350"android:layout_height="@dimen/dp_350"android:layout_alignParentRight="true"android:layout_marginRight="-240dp"><com.wj.views.RingViewHalfandroid:id="@+id/ringView_half_right"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_alignParentRight="true"android:layout_centerVertical="true"android:background="@drawable/bg_compatibility"app:image_angle_rh="15"app:image_padding_rh="20"app:is_right_select_icon_rh="false"app:list_rh="@array/zodiac_list" /><ImageViewandroid:layout_width="70dp"android:layout_height="70dp"android:layout_alignParentRight="true"android:layout_centerVertical="true"android:layout_marginRight="@dimen/dp_220"android:src="@drawable/icon_match_girl" /></RelativeLayout><Buttonandroid:id="@+id/check_btn"android:layout_width="@dimen/dp_265"android:layout_height="@dimen/dp_46"android:layout_alignParentBottom="true"android:layout_marginBottom="@dimen/dp_25"android:layout_marginTop="@dimen/dp_25"android:layout_centerHorizontal="true"android:alpha="0.5"android:enabled="true"android:clickable="true"android:background="@drawable/check" /></RelativeLayout>
//这里是放半圆形转盘选择器上显示的图片list,我这里是用的xml静态传进去的,也可以改为动态方式传递
app:list_rh="@array/zodiac_list"
然后在values下面创建一个arrays.xml文件
<?xml version="1.0" encoding="utf-8"?><resources><string-array name="zodiac_list"><item>@drawable/zodiac_1</item><item>@drawable/zodiac_2</item><item>@drawable/zodiac_3</item><item>@drawable/zodiac_4</item><item>@drawable/zodiac_5</item><item>@drawable/zodiac_6</item><item>@drawable/zodiac_7</item><item>@drawable/zodiac_8</item><item>@drawable/zodiac_9</item><item>@drawable/zodiac_10</item><item>@drawable/zodiac_11</item><item>@drawable/zodiac_12</item></string-array></resources>
到此就可以了~,喜欢的可以在文章底部留言交流或者探讨,最后附上GitHub源码地址,喜欢的记得点亮星星。
/15115134369/RingviewHalfDemo