Android自定義半圓形圓盤滾動選擇器
前段時間公司項目要求做一個特效的滑動選擇器,效果如下圖的樣子:

功能要求:兩邊的半圓形轉盤可以轉動,轉盤上的圖標也一起滾動,藍紅色圖標指著的小圖標變成高亮選中狀態(tài)。
第一眼看到這個需求就想到這個必須要用自定義控件來做才行,于是產(chǎn)生了這樣的思路:
半圓形的滾動的轉盤自定義view繼承viewgroup,重寫滑動事件,自定義圓盤上圖片的擺放角度,至于藍色和紅色箭頭圖標指向的選中狀態(tài)可以用坐標數(shù)組繪制一個區(qū)域來判斷是否有符合條件的圖標滾動到了這個位置,如果有的話就將這個圖標所在的控件透明度設置為1,如果沒到這個位置就設置為非選中狀態(tài)0.5透明度 ,思路這樣定下來了,預計可以行得通,于是開始進行實際的嘗試寫代碼實現(xiàn)這個自定義的控件和功能。
下面我直接把核心代碼附上,注釋比較清晰:
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 2018/6/8
* @author JunJieW
* @since 1376881525@qq.com
* @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;
/**
* 圖片內(nèi)容偏移角度
*/
private int mImageAngle;
/**
* 是否初始化布局
*/
private boolean isChekc = false;
/**
* 布局view
*/
private List<Integer> mImageList = new ArrayList<>();
/**
* 是否可點擊
*/
private boolean isCanClick = true;
/**
* 圖片與環(huán)之間的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();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (!isChekc) {
initView();
mRadius = getWidth();
isChekc = true;
}
}
/**
* 測量
*/
@Override
protected 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++) {
//新建imageview
final 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為選中狀態(tài)
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為選中狀態(tài)
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() {
@Override
public void onClick(View view) {
if (isCanClick) {
// Toast.makeText(getContext(),finalI + " ---", Toast.LENGTH_SHORT).show();
if (mOnLogoItemClick != null)
mOnLogoItemClick.onItemClick(view, finalI - 1);
}
}
});
//添加view
addView(mImageView);
}
//添加view點擊事件
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
if (isCanClick) {
}
}
});
}
/**
* 觸摸監(jiān)聽
*/
@Override
public 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;
// 如果當前已經(jīng)在快速滾動
if (isMove) {
// 移除快速滾動的回調(diào)
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);
}
/**
* 根據(jù)當前位置計算象限
*/
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函數(shù)中獲取即可,這時view都已經(jīng)加載完成
* 但這里特別注意一點要:如果是fragment種使用該自定義view的話,這里的方法就應該注釋掉
* 因為不但獲取到的矩形的值是空的,而且當你的fragment執(zhí)行了跳轉的邏輯后,再返回后會發(fā)
* 一種特別惡心的異常,你獲取到判斷選中位置的矩形的left,top,right,bottom的值會和
* 初始化的時候不一樣,導致你選中時候的狀態(tài)出現(xiàn)異常情況,本人已經(jīng)被坑過,希望后面的同學
* 一定注意吸取教訓
*/
@Override
public 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動態(tài)設置寬高為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動態(tài)設置寬高為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的狀態(tài)
setSelectedIcon();
}
//改變選中的icon的狀態(tài)
private void setSelectedIcon() {
if (select_icon_rect.left==0&&select_icon_rect.top==0){
//fragment中onWindowFocusChanged會出現(xiàn)計算select_icon_rect.left和select_icon_rect.top等于0的情況,
// 所以做下判斷,如果為0則重新調(diào)用下計算方法
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是選中狀態(tài)的時候
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是選中狀態(tài)的時候
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中根據(jù)回調(diào)方法獲取選中的對象:
//左右側方法相同,這里列出左側圓盤獲取方法:
view.ringView_half_left.addOnIconSelectedListener { position ->
// ToDo 根據(jù)postion從你的list中獲取對應的選中的對象的bean類屬性即可
}
最后貼下布局文件:
<RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <RelativeLayout android: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.RingViewHalf android: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" /> <ImageView android: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> <RelativeLayout android: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.RingViewHalf android: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" /> <ImageView android: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> <Button android: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靜態(tài)傳進去的,也可以改為動態(tài)方式傳遞
app:list_rh="@array/zodiac_list"
然后在values下面創(chuàng)建一個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>
到此就可以了。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
Android Studio實現(xiàn)簡易計算器(表格布局TableLayout)
這篇文章主要為大家詳細介紹了Android Studio實現(xiàn)簡易計算器,表格布局TableLayout,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-03-03
Android中okhttp3.4.1+retrofit2.1.0實現(xiàn)離線緩存
這篇文章主要介紹了Android中okhttp3.4.1結合retrofit2.1.0實現(xiàn)離線緩存,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10
GridView基于pulltorefresh實現(xiàn)下拉刷新 上拉加載更多功能(推薦)
原理和listview一樣 ,都是重寫Android原生控件。下面小編通過實例代碼給大家分享GridView基于pulltorefresh實現(xiàn)下拉刷新 上拉加載更多功能,非常不錯,一起看看吧2016-11-11
Android實現(xiàn)音量調(diào)節(jié)的方法
這篇文章主要介紹了Android實現(xiàn)音量調(diào)節(jié)的方法,涉及Android頁面布局及多媒體播放的設置技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-09-09
Android自定義View實現(xiàn)數(shù)獨游戲
這篇文章主要為大家詳細介紹了Android自定義View實現(xiàn)數(shù)獨游戲,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-12-12
Android入門之RelativeLayout、FrameLayout用法分析
這篇文章主要介紹了Android入門之RelativeLayout、FrameLayout用法分析,需要的朋友可以參考下2014-08-08

