序言(扯)
由于现在大公司放出的实习offer大多为暑期,本学期课程不多,所以找了一家公司可以立刻入职的去实习下,相比大公司,小公司多采取硬上的策略,对于解决编程问题能力的提升是相当大的,在拉勾投了一家,前天去面试,然后一个自定义view把我问跪了,讲到复用性,讲了很不优雅的一种实现方式,不过最终学习了下,实现思路和我当时讲的差不多,但这确实是我开发中一个短板,但是其他问题回答的还是蛮不错的,还是给了offer,产品真心赞,团队也超赞,CEO=CMU(CS),听COO讲了下产品,分析的真是让我瞠目结舌。下周入职,搬,搬,搬。
今天来写自定义View了,之前看任主席的书,看了View的绘制和事件响应机制,对view底层有了较深理解,但是说到自定义一个View,下不去手。写个卫星菜单,后续将根据这个项目中出现的问题,然后在后续对自定义view的问题和view底层的一些东西进行讲解下。
自定义控件过程
自定义控件的流程大致分为以下几步
定义属性
定义xml文件
自定义View获取属性
onMeasure()
onLayout()
自定义view的目的是为了提升我们的视觉体验,所以一般我们会辅助一些动画来提升体验,为了增强交互,我们也要为其增加一些交互,所以我么需要对其中进行一些事件的监听,自定义监听器,然后在我们需要的地方进行回调,考虑完这些,我们需要再考虑的是如何提高这些view的复用性。还有当我们多个空
View的绘制流程
OnMeasure
OnLayout
OnDraw
对于自定义View,我们通常会重写这三个方法,重写那些,取决于我们的自定义View从哪里继承,然后要实现什么样的功能。大致归纳有以下几点。
继承View
实现一些不规则的图形,需要重写onDraw方法进行绘制继承ViewGroup
需要实现对于子控件的测量和布局继承特定View
较容易实现继承特定ViewGroup
无需处理测量和布局
实现卫星菜单
有了前面的一点小储备,接下里要动手实现我们的小demo了,当然我这个demo的实现思路也是参考网上的思路和实现,首先明确我们要实现的样式是如何的。上一张图
如上图所示,我们需要一个按钮,触发后向发生卫星弹射出5个子View,然后点击之后会缩回,根据上面的自定义View的分类,我们不难发现,我们需要的是通过继承ViewGroup来实现,继承自ViewGroup,那么我们要对其进行一个测量,然后是布局,难点就是在布局上,如何布局呢?这里不难发现,我们可以通过为其设定半径,然后将这个几个卫星平均分布在中心的圆周围。然后对于我们中间的View进行事件的监听,还有对于分布在周围的View的监听。上述即为实现的核心环节。接下来按照我们的思路,贴出代码,供以参考,熟悉整个流程。
属性设置
这个之前真的没有用到过,又涨姿势了)喜悦脸,之前在使用一些自定义控件中,我们不难发现会有一些特殊的属性,我们可以为其设置值,然后我们使用的自定义View就可以根据这个属性制定的值进行显示,如何设置这些值,然后在View的内部又是如何获得的这些值呢?
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="postion">
<enum name="left_top" value="0"/>
<enum name="left_bottom" value="1"/>
<enum name="right_top" value="2"/>
<enum name="right_bottom" value="3"/>
</attr>
<attr name="radius" format="dimension"/>
<declare-styleable name="ArcMenu">
<attr name="postion"/>
<attr name="radius"/>
</declare-styleable>
</resources>
在values下,设置我们的资源属性文件,然后声明我们的自定义View所需要的属性,然后在我们自定义View添加命名空间之后,我们就可以使用这些属性了,这都是很次要的了,然后完成我们的布局。
设置布局
<com.example.chenjensen.viewset.arcmenuview.ArcMenu
android:id="@+id/arc_menu"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:postion="left_bottom"
app:radius="100dp"
>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@mipmap/arcmenu_composer_button">
<ImageView
android:id="@+id/arcmenu_iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@mipmap/arcmenu_composer_icn_plus"/>
</RelativeLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/arcmenu_composer_camera"
android:tag="camera"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/arcmenu_composer_music"
android:tag="music"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/arcmenu_composer_place"
android:tag="place"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/arcmenu_composer_sleep"
android:tag="sleep"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/arcmenu_composer_thought"
android:tag="thought"/>
</com.example.chenjensen.viewset.arcmenuview.ArcMenu>
在View中获取这些属性
TypedArray a = context.getTheme().obtainStyledAttributes(attributeSet, R.styleable.ArcMenu,defStyle,0);
int pos = a.getInt(R.styleable.ArcMenu_postion, POS_RIGHT_BOTTOM);
switch (pos)
{
case POS_LEFT_BOTTOM:
mPostion = Position.LEFT_BOTTOM;
break;
case POS_LEFT_TOP:
mPostion = Position.LEFT_TOP;
break;
case POS_RIGHT_BOTTOM:
mPostion = Position.RIGHT_BOTTOM;
break;
case POS_RIGHT_TOP:
mPostion = Position.RIGHT_TOP;
break;
default:
break;
}
mRadius = (int)a.getDimension(R.styleable.ArcMenu_radius,(int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,100
,getResources().getDisplayMetrics()));
测量子View
因为我们继承的是一个ViewGroup, 所以我们需要进行子View的测量。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
for(int i=0; i<count; i++){
measureChild(getChildAt(i),widthMeasureSpec,heightMeasureSpec);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
View布局
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if(changed){
layoutCButton();
int count = getChildCount();
for(int i=0; i<count-1; i++){
View child = getChildAt(i+1);
child.setVisibility(GONE);
int cl = (int)(mRadius*Math.sin(Math.PI/2/(count-2)*i));
int ct = (int)(mRadius*Math.cos(Math.PI/2/(count-2)*i));
int cWidth = child.getMeasuredWidth();
int cHeight = child.getMeasuredHeight();
if(mPostion==Position.LEFT_BOTTOM||mPostion==Position.RIGHT_BOTTOM){
ct = getMeasuredHeight()-cHeight-ct;
}
if(mPostion==Position.RIGHT_BOTTOM||mPostion==Position.RIGHT_TOP){
cl = getMeasuredWidth()-cWidth-cl;
}
child.layout(cl,ct,cl+cWidth,ct+cHeight);
}
}
}
这里涉及到我们在布局上的数学知识了。因为当我们的卫星菜单处在四个不同的角的时候,我们在坐标的设置上就会出现问题了,首先是将我们的恒星按钮进行布局,然后是卫星按钮围绕其周围进行布局,对于恒星的布局,也需要我们根据四个不同的位置进行选择。
private void layoutCButton(){
mCButton = getChildAt(0);
mCButton.setOnClickListener(this);
int left = 0;
int top = 0;
int width = mCButton.getMeasuredWidth();
int height = mCButton.getMeasuredHeight();
switch (mPostion){
case LEFT_BOTTOM:
left = 0;
top = getMeasuredHeight()-height;
break;
case RIGHT_BOTTOM:
left = getMeasuredWidth()-width;
top = getMeasuredHeight()-height;
break;
case LEFT_TOP:
left = 0;
top = 0;
break;
case RIGHT_TOP:
left = getMeasuredWidth()-width;
top = 0;
break;
default:break;
}
mCButton.layout(left, top, left + width, top + height);
}
这样我们的基本完成了界面的布局了,然后是对于
点击事件的监听和响应
@Override
public void onClick(View v) {
rotateButton(v, 0f, 360f, 300);
toggleMenu(300);
}
这里为了增强体验感,对于按钮,在点击的时候添加了一个旋转动画,然后是控制卫星的弹射,可能会有点疑问哈,我们之前写代码对于按钮事件的监听,我们不都是要将其和具体的控件绑定吗,但是这里为什么不需要,原因是,在这里只有它可以响应这个事件,其它的都是不可见,在可见的时候,我们又为其设置了监听器,一会演示,因此事件会被子view自己的监听事件拦截掉,而不会在其父view级的事件处理机制中出现。这里涉及到View的事件传递机制,是从父View向子View进行传递,如果事件被消耗了,则父View没有响应,当然可以在父View中设置拦截机制。这里的toggleMenu的实现是响应我们的卫星向周边弹射的核心。
public void toggleMenu(int duration){
int count = getChildCount();
for(int i=0; i<count-1; i++){
final View view = getChildAt(i+1);
view.setVisibility(VISIBLE);
int cl = (int)(mRadius*Math.sin(Math.PI/2/(count-2)*i));
int ct = (int)(mRadius*Math.cos(Math.PI / 2 / (count - 2) * i));
int xFlag = 1;
int yFlag = 1;
if(mPostion==Position.LEFT_BOTTOM||mPostion==Position.LEFT_TOP)
xFlag = -1;
if(mPostion==Position.LEFT_TOP||mPostion==Position.RIGHT_TOP)
yFlag = -1;
AnimationSet set = new AnimationSet(true);
Animation transAnim = null;
//添加平移动画
if(mCurrentStatus == Status.CLOSE){
transAnim = new TranslateAnimation(xFlag*cl,0,yFlag*ct,0);
view.setClickable(true);
view.setFocusable(true);
}else{
transAnim = new TranslateAnimation(0,xFlag*cl,0,yFlag*ct);
view.setClickable(false);
view.setFocusable(false);
}
transAnim.setFillAfter(true);
transAnim.setDuration(duration);
transAnim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if(mCurrentStatus==Status.CLOSE){
view.clearAnimation();
view.setVisibility(View.INVISIBLE);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
//添加旋转动画
RotateAnimation animation = new RotateAnimation(0,720, Animation.RELATIVE_TO_SELF,0.5f
,Animation.RELATIVE_TO_SELF,0.5f);
animation.setDuration(duration);
animation.setFillAfter(true);
//先添加旋转,后添加平移动画
set.addAnimation(animation);
set.addAnimation(transAnim);
view.startAnimation(transAnim);
final int pos = i;
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if(mMenuItemClickListener!=null){
mMenuItemClickListener.onClick(v,pos);
}
menuItemAnim(pos);
changeState();
}
});
}
changeState();
}
说下实现原理吧,因为这里我们使用的不是属性动画,通过的是平移,其特点是当其移动了之后,其实际位置还是在原处,所以是不可以点击的,解决这个问题的方式是将其固定在我们要点击的位置,然后设置为不可见,移动的时候,让其从一个负位置移动到当前这个位置,产生一种发射的特效,同时给其添加了一个旋转特效,让其显得更加的自然圆滑,之后,我们对这些子View设置监听事件,监听事件是让点击的按钮变大,未点击的按钮消失,然后变化这些状态。
private void menuItemAnim(int pos){
for(int i=0; i<getChildCount()-1; i++){
View childView = getChildAt(i+1);
if(i==pos){
childView.startAnimation(scaleBigAnimation(300));
}else{
childView.startAnimation(scaleSmallAnimation(300));
}
childView.setFocusable(false);
childView.setClickable(false);
}
}
遇到的问题
在对View进行设置可见或者不可见的时候,要清除掉当前的动画
结束语
具体动画代码不再贴出,可以去我的Github上查看具体代码,同时本人想实现一个自定义View的集合篇,将写一些自定义View的东西具体的实现贴一下,然后在Github上挂出代码, 大家有什么推荐的吗?这样我们可以一起学习,共同进步。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。