前言

本自定义控件参考自鸿洋大神的自定义控件,基于原来的控件效果进行修改,着重实现了以下效果:位置自动修正以及滑动结束的回调。我们先来看看效果图:

上面的图片是一个ImageView,与控件无关,是为了验证回调功能。接着是位置自动修正:

位置自动修正的意思是说,每个item view经过滑动后,停留的位置不是随意的,而是固定在某个区域之内,就如每个item view装在一个个格子里面。喎�"/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPjxpbWcgYWx0PQ=="滑动结束回调" src="/uploadfile/Collfiles/20160730/20160730092538750.gif" title="" />

而滑动结束的回调是说,当滑动结束后,滑动到中央的item view会触发一次回调,用户可以利用该回调来进行别的逻辑处理,与别的控件进行交互,比如:某个item view滑动到中央,触发回调,让别的TextView或者ImageView来具体显示该item项的具体信息。

用法

只要在Activity中写上如下几行代码即可:

SemicircleMenu mSemicircleMenu = (SemicircleMenu) findViewById(R.id.circlemenu);

mSemicircleMenu.setMenuItemIconsAndTexts(mItemImgs, mItemTexts);

mSemicircleMenu.setOnMenuItemClickListener(new SemicircleMenu.OnMenuItemClickListener() {

@Override

public void itemClick(View view, int pos) {

Toast.makeText(MainActivity.this, mItemTexts[pos], Toast.LENGTH_SHORT).show();

}

});

mSemicircleMenu.setOnCentralItemCallback(new SemicircleMenu.OnCentralItemCallback() {

@Override

public void centralItemOperate(int pos) {

imageView.setImageResource(mItemImgs[pos]);

}

});

布局文件:

(注意:clickable应该为true。)

实现原理

其实关于测量、布局、甚至事件分发的实现原理在原文章都有很详细的说明了,有兴趣的读者可以先阅读原文,这里会作简要的说明,本文重点在于讲述位置修正即滑动结束回调的实现,本文所有代码均作了删减,读者可直接到GitHub处阅读源码。

Part 1 设置itemView的内容及加载itemView

/**

* 每个Item之间相距的角度

*/

private float mAngleDelay;

/**

* 设置菜单的文本信息

*/

public void setMenuItemIconsAndTexts(int[] resIds,String[] texts)

{

mItemIcons = resIds;

mItemTexts = texts;

if(resIds == null && texts == null)

{

throw new IllegalArgumentException("菜单文本和图片必须设置其一");

}

//初始化mMenuItemCount

mMenuItemCount = resIds == null ? texts.length : resIds.length;

if(resIds != null && texts != null)

{

mMenuItemCount = Math.min(resIds.length,texts.length);

}

//计算每个Item之间相差的度数,该值直接影响后面的布局、滑动

mAngleDelay = 360 / mMenuItemCount;

addMenuItems();

}

private void addMenuItems() {

LayoutInflater mInflater = LayoutInflater.from(getContext());

/**

* 初始化item view

*/

for(int i = 0; i < mMenuItemCount; i++)

{

final int j = i;

View view = mInflater.inflate(R.layout.circle_menu_item,this,false);

view.setTag(i); //为每个item view打上Tag

ImageView iv = (ImageView) view.findViewById(R.id.id_circle_menu_item_image);

TextView tv = (TextView) view.findViewById(R.id.id_circle_menu_item_text);

//...

addView(view);

}

}

从以上代码来看,暴露了setMenuItemIconsAndTexts方法,用户可以通过该方法为该控件设置不同的Item的图像及其文本信息。接着根据设置item的数量,来计算每个item之间应相隔多少度,即mAngleDelay值,例如,如果是6个Item,那么mAngleDelay值就是60度,以此类推。接着,在addMenuItem方法内,是不断加载Item View,并且添加至当前ViewGroup内。(注意:形如R.id.id_circle_menu_item_image的id是定义在values文件夹下的id文件内的)。

Part 2 测量和布局

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

{

//...

//我们只需要半圆区域,因此把高度限制为一半

setMeasuredDimension(resWidth,resHeight /2);

mRadius = Math.max(getMeasuredWidth(),getMeasuredHeight());

final int count = getChildCount();

int childSize = (int) (mRadius * DEFAULT_CHILD_DIMENSION);

int childMode = MeasureSpec.EXACTLY;

//遍历所有子View,对其进行测量

for(int i = 0; i < count;i++)

{

final View child = getChildAt(i);

if(child.getVisibility() == GONE)

{

continue;

}

int makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize,childMode);

child.measure(makeMeasureSpec,makeMeasureSpec);

}

mPadding = PADDING_LAYOUT * mRadius;

}

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b)

{

int layoutRadius = mRadius;

final int childCount = getChildCount();

int left,top;

//每个item view的宽度

int cWidth = (int) (layoutRadius * DEFAULT_CHILD_DIMENSION);

//坐标原点到item view中心的距离

float tmp = layoutRadius / 2f - cWidth / 2 - mPadding;

for(int i =0;i

在加载完item view后,我们便对所有item view进行测量、布局,其中布局流程中,我们根据mStartAngle这个角度来进行布局,通过计算三角关系把所有view的left、top坐标计算出来,然后布局,这样每个Item view就一一形成了。顺带一说,这里mStartAngle一开始等于90度,也就是说第一个选项出现的位置是正中央。在布局的最后,存在一个判断语句,判断我们在滑动结束后是否要进行回调,这个下面会详细说明。Part 3 事件分发

在初始化布局完毕后,一个半圆的菜单便显示出来了,接下来我们需要对触摸事件进行处理,以便能进行滑动。我们先看看代码:

@Override

public boolean dispatchTouchEvent(MotionEvent ev)

{

float x = ev.getX();

float y = ev.getY();

switch (ev.getAction())

{

case MotionEvent.ACTION_DOWN:

mLastX = x;

mLastY = y;

mDownTime = System.currentTimeMillis();

mTmpAngle = 0;

mTouchFlag = true;

//如果按下的时候,正在自动滚动状态,那么取消滚动,并且进行位置矫正

if(isFling)

{

removeCallbacks(mFlingRunnable);

isFling = false;

mCorrectPositionFlag = true;

post(mFlingRunnable = new AutoFlingRunnable(getCorrectAngle(mAutoFlingAngle % mAngleDelay)));

return true;

}

break;

case MotionEvent.ACTION_MOVE:

float start = getAngle(mLastX,mLastY);

float end = getAngle(x,y);

if(getQuadrant(x,y) == 4)

{

mStartAngle += end - start;

mTmpAngle += end - start;

}else{

mStartAngle += start -end;

mTmpAngle += start -end;

}

requestLayout();

mLastX = x;

mLastY = y;

break;

case MotionEvent.ACTION_UP:

mTouchFlag = false;

float anglePerSecond = mTmpAngle * 1000 / (System.currentTimeMillis() - mDownTime);

//如果角速度超过规定的值,那么认为是快速滚动,开启快速滚动任务

//否则,直接进行位置矫正

if(Math.abs(anglePerSecond) >= mFlingableValue && !isFling)

{

mAutoFlingAngle = mTmpAngle;

post(mFlingRunnable = new AutoFlingRunnable(anglePerSecond));

return true;

}else if(Math.abs(anglePerSecond) < mFlingableValue)

{

float mDeltaAngle = mTmpAngle % mAngleDelay ;

if(mDeltaAngle != 0)

{

post(mFlingRunnable = new AutoFlingRunnable(getCorrectAngle(mDeltaAngle)));

return true;

}

}

// 如果当前旋转角度超过NOCLICK_VALUE屏蔽点击

if (Math.abs(mTmpAngle) > NOCLICK_VALUE)

{

return true;

}

break;

}

return super.dispatchTouchEvent(ev);

}

大体思路是这样的,当检测到ACTION_DOWN事件时,记录当前的触摸坐标,接着在ACTION_MOVE事件中,不断获取实时的触摸坐标,并计算角度,通过新的角度来累加到mStartAngle中,接着调用requestLayout()方法来重新布局,这样便实现了该菜单跟随手指而转动的效果,这是最基础的。但如果是快速滑动呢?那么我们在ACTION_UP事件中,通过判断滑动的距离与从触摸到松开手指的时间的比值来判断是否是快速滑动。如果是快速滑动,那么启动一个Runnable来完成快速滚动的事件,其实这也很简单,在Runnable中不断调用requestLayout()就可以实现快速滚动的效果了,这些在原文章都有详细说明。

Part 4 位置自动修正

但是,由于随手指滑动,或者快速滑动完毕后,其最后的滑动角度一般是一个随机的数值,这样就会造成item view出现在不应该出现的位置,比如正中央恰好没有item view出现,都出现了一定的偏移,这样对于菜单来说是非常不理想的,所以我们需要进行位置的矫正,使得每一个的滑动完成后,其Item View都能在正确的位置出现,而解决这个问题,我们可以从以下思路来解决:首先把总的滚动角度先算出来,那么这个总的滚动角度便直接决定了滚动完毕后各item的位置,既然我们不能影响其滚动过程,那么我们可以在滚动结束后,通过对总滚动角度进行一系列的判断来对最后的位置进行调整,并再一次requestLayout,使得位置得以矫正。

那么这个总的滚动角度是怎样与位置矫正联系起来的呢?在代码里面,总滚动角度用mTmpAngle或者mAutoFlingAngle来记录,我们可以先用它对mAngleDelay(该值上面提及,表示每个Item之间相隔的角度)求余,这样得出的结果是任一item的偏移量。举个例子:item view有6个,那么每个Item相隔60°,我们转动了80°,那么我们可以这样分解:先转了60°,此时每个Item的位置一定是正确的,再转20°,那么此时item就会留在不正确的位置了,我们所要做的就是对这个“20°”进行处理,那么以30°为分割线,没到30°的,让itemview往回转到正确的位置;如果超过了30°的,让Itemview转动到下一个位置,那么我们的问题便得以解决了。

下面的方法是计算还需要多少角度才能转到正确的位置的:

/**

* 获取位置矫正所需的角度

* @param angle 对mAngleDelay求余后的角度

* @return

*/

private float getCorrectAngle(float angle)

{

if(angle > 0 && angle <= mAngleDelay/2)

{

mCorrectPositionFlag = true;

return -angle;

}else if(angle >mAngleDelay/2)

{

mCorrectPositionFlag = true;

return (mAngleDelay -angle);

}else if(angle < 0 && Math.abs(angle) <= mAngleDelay/2)

{

mCorrectPositionFlag = true;

return -angle;

}else if(angle < 0 && Math.abs(angle) > mAngleDelay/2){

mCorrectPositionFlag = true;

return -(mAngleDelay -Math.abs(angle));

}

return 0;

}

在获取到需要修正的角度后,我们可以直接通过Runnable来重新布局一下,把该值作为需要转动的角度即可。

如下所示:

private class AutoFlingRunnable implements Runnable

{

//...

public void run()

{

if(mCorrectPositionFlag)

{

float angle = angelPerSecond;

mStartAngle += angle;

requestLayout();

mCorrectPositionFlag = false;

}else {

// 如果小于20,则停止,同时进行位置矫正

if ((int) Math.abs(angelPerSecond) < 20) {

isFling = false;

mCorrectPositionFlag = true;

this.angelPerSecond = getCorrectAngle(mAutoFlingAngle % mAngleDelay);

postDelayed(this,30);

return;

}

isFling = true;

// 不断改变mStartAngle,让其滚动,/30为了避免滚动太快

mStartAngle += (angelPerSecond / 30);

mAutoFlingAngle += (angelPerSecond / 30);

// 逐渐减小这个值

angelPerSecond /= 1.0666F;

postDelayed(this, 30);

// 重新布局

requestLayout();

}

}

}

Part 5 滑动结束的回调

在滑动结束并且位置修正完毕后,在中央会有一个item view,有时候我们需要对该Item view进行交互操作,比如上面的演示图内,每滑动完毕,便把中央的item view的图片显示到上面ImageView中,那么我们就需要在滑动完毕的时候,判断出居于中央的item View到底是哪一个。以下是实现思路:首先通过一个方法findChildViewUnder来获取某个坐标点上的itemView的实例,如下所示:

/**

* 获取某个坐标上的子View

* @param x

* @param y

* @return View

*/

private View findChildViewUnder(float x,float y)

{

final int count = getChildCount();

for(int i = count - 1; i >= 0; i--)

{

final View child = getChildAt(i);

if(x >= child.getLeft() && x <= child.getRight() && y>= child.getTop() && y <= child.getBottom())

return child;

}

return null;

}

实现原理很简单,就是遍历所有的item View,来判断给定的x、y坐标在哪个Item View之内,提取到item View的实例后,我们再拿出该itemView的Tag(因为加载itemView的时候,给每个View都打上了不同的Tag),有了Tag,就知道了是哪一个itemView滑动到了最中央的位置,最后再利用回调的实现方法,来实现交互式操作。

上面提到,在onLayout方法的最后有如下语句:

//布局结束的时候,如果不在滚动同时也不在被触摸的时候,触发滚动结束回调

if(!isFling && !mTouchFlag ) {

mOnCentralItemCallback.centralItemOperate((Integer) findChildViewUnder(layoutRadius/2,tmp).getTag());

}

其中,OnCentralItemCallback是一个接口,类似于监听器接口,centralItemOperate是一个回调方法。在Activity中调用该方法能实时获取到滑动结束后中央位置的itemView的类型。

GitHub地址:https://github.com/chenyua1995/SemicircleMenu

欢迎各位star和fork,谢谢阅读!

android 半圆滚动菜单,自定义控件:实现半圆滚动菜单效果相关推荐

  1. Android自定义控件----继承ViewGroup侧滑菜单5,抽屉式侧滑,QQ5.0效果(完结)

    效果图: 项目结构: QQ5.0的侧滑 和抽屉菜单的区别 简单的说就是在onScrollChanged方法中加入了缩放,偏移,渐变属性动画效果 区别1:内容1:内容区域 1.0到0.7 缩放效果 sc ...

  2. Android自定义半圆进度条 半圆渐变色进度条带指示 半圆开口大小可自由修改

    Android自定义半圆进度条 半圆渐变色进度条带指示 半圆开口大小可自由修改 首先我们来看下效果图 不同的开口大小只需要修改一个参数即可 半圆1: 半圆2: 半圆3: 如果是你想要的效果,就直接滑动 ...

  3. 导航菜单:jQuery粘性滚动导航栏效果

    粘性滚动是当导航在滚动过程中会占粘于浏览器上,达到方便网站页面浏览的效果,也是一种用户体验,下面我们看一下是怎么实现的: jQuery的 smint插件,也是一个导航菜单固定插件.当页滚动时,导航菜单 ...

  4. Android 第十八课 强大的滚动控件 RecyclerView

    步骤: 一.添加依赖库 compile'com.android.support:recyclerview-v7:26.1.0' 二.在activity_mian.xml中,添加RecyclerView ...

  5. Android应用开发--MP3音乐播放器滚动歌词实现

    Android应用开发--MP3音乐播放器滚动歌词实现 2013年6月2日  简.美音乐播放器开发记录 -----前话 有网友给我博客评论说,让我借鉴好的Android代码,代码贴出来的时候最好整体先 ...

  6. android view上下滚动条,Android自定义View六(ViewGroup水平垂直滚动实现类似支付宝年度账单的效果)...

    先看两张效果图 1.垂直滑动 onegif.gif 2.水平滑动 twoGIF.gif 先看使用方法 1.AndroidStudio 引入 Project.gradle repositories { ...

  7. Android App开发动画特效之利用滚动器实现平滑翻页(附源码和演示 简单易懂)

    需要图片集请点赞关注收藏后评论区留言~~~ 一.利用滚动器实现平滑翻页 在日常生活中,平移动画比较常见,有时也被称为位移动画,左右翻页和上下滚动其实都用到了平移动画,譬如平滑翻书的动画效果,就是位移动 ...

  8. Android开发——自定义炫酷PickerView惯性滚动魔改

    Android开发--自定义炫酷PickerView快速滚动魔改 最近由于课内压力的增加和安卓课设项目,故没有怎么刷acm题,基本上学校要训练也就去水一波,程序设计相关内容也鸽了. 由于从来没有做过开 ...

  9. 【android UI学习】LinearGradient实现歌词滚动

    Shader着色器的讲解,可以参考 [android UI学习]Shader着色器 这里LinearGradient的实际运用 先看运行效果 具体思路就是, 用LinearGradient设置一个三个 ...

最新文章

  1. CodeforcesBeta Round #19 D. Points 离线线段树 单点更新 离散化
  2. [持续更新]UnsatisfiedLinkError常见问题及解决方案
  3. 会员制的cd出租销售java_模式|酒店改造养老会员制盈利模式及交易结构深度探讨...
  4. eda可视化_5用于探索性数据分析(EDA)的高级可视化
  5. android支付宝支付开发过程
  6. git clone 分支_Git 小团队的协作 (二)
  7. EBB-23、DNS服务
  8. MongoDB 学习-MongoDB 的基本操作(二)
  9. python filter过滤器的使用_Python filter过滤器原理及实例应用
  10. Neural Style Transfer 神经风格迁移详解
  11. Enterprise Library 2.0 Hands On Lab 翻译(2):数据访问程序块(二)
  12. redis的持久化相关操纵
  13. WPS EXCEL 处理字符串转换为日期格式
  14. 装office2010时,装了msxml6仍提示要装msxml6
  15. 金蝶K3Cloud中DBUtils的用法
  16. XYplorer 20.90.0900中文版 — 资源管理器
  17. 前端程序员应该去哪个城市发展?
  18. C++学习日记7——容器
  19. 认真对待每一道算法题 之 找明星问题 - 淘宇瀚
  20. Layui时间插件laydate只选择时分

热门文章

  1. 这个 Python 代码自动补全神器搞得我卧槽卧槽的
  2. working space和working set关系
  3. LDA (Linear Discriminate Analysis)Fisher Criteria
  4. 漫步最优化四十一——Powell法(下)
  5. 漫步最优化十三——驻点
  6. leetcode —— 654. 最大二叉树
  7. php 文件上传mime 类型,PHP JAVA C上传文件如何准确判断文件类型-mime知识普及
  8. ug中模型不见了怎么办_关于UG参数化建模的定义
  9. mysql大小写敏感_MySQL数据库大小写敏感的问题
  10. python staticmethod规范_Python Staticmethod可以调用另一个本地方法吗?