Android-教你写小米系统应用--"我的小米"

我承认我有点标题党了,我不可能完整的介绍怎么写小米应用,我这篇要说的其实是模仿MIUI6系统应用“我的小米”的首页,主要实现的UI是一个圆形的头像,下面是用户名,再下面是一些功能的cell,然后向上滑动功能cell,可以将头像渐隐,然后用户名放大放到页面顶部,向下滑动,恢复页面初始样貌,大家如果手头有小米手机的可以自己感受下(我自己觉得小米的一些系统应用做的还是不错的)。

构思

前面的文章中,我们已经了解了如何去自定义一个ViewGroup,可以在onLayout中自由的对子View进行位置设定,我们今天这里刚好需要对上面需求提到的三部分子View(头像ImageView,姓名TextView,功能Cell布局)在滑动过程中进行位置设定,重绘,所以我们就可以自定义一个ViewGroup去实现。

public class MineMiView extends ViewGroup {
    public MineMiView(Context context) {
    super(context, null, 0);
  }

  public MineMiView(Context context, AttributeSet attrs) {
    super(context, attrs, 0);
  }

  public MineMiView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mContext = context;
  }

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

  }
}

我们接着将主布局文件画一下,现在我们为了简单起见,布局的第三部分-功能cell部分暂时先用一个空的天蓝色LinearLayout布局代替,后面我们会替换回去。

 <com.example.aliouswang.myapplication.widget.MineMiView
    android:id="@+id/mineMiView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <ImageView
        android:id="@+id/head_imageview"
        android:layout_width="70dp"
        android:layout_height="70dp"
        android:background="@drawable/default_head"
        ></ImageView>

    <LinearLayout
        android:id="@+id/username_rootview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:gravity="center_horizontal"
        >
        <TextView
            android:id="@+id/name_tv1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Joe少"
            android:textSize="16sp"
            android:textColor="@android:color/holo_orange_dark"
            ></TextView>

        <TextView
            android:id="@+id/name_tv2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="stay hungry, stay foolish"
            android:textSize="12sp"
android:textColor="@android:color/holo_orange_light"
            ></TextView>
        </LinearLayout>
            <LinearLayout
                android:id="@+id/content_rootview"
                android:orientation="vertical"
   android:background="@android:color/holo_blue_light"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

            </LinearLayout>
 </com.example.aliouswang.myapplication.widget.MineMiView>

//初始化我们即将使用的三个子View
@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    mHeadImageView = (ImageView)findViewById(R.id.head_imageview);
    mUserNameRootView = (LinearLayout)findViewById(R.id.username_rootview);
    mContentRootView = (MiScrollView)findViewById(R.id.content_rootview);
}

    @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    sWidth = MeasureSpec.getSize(widthMeasureSpec);
    sHeight = MeasureSpec.getSize(heightMeasureSpec);
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    //measure头像控件的大小
    headImageWidth = mHeadImageView.getMeasuredWidth();
    headImageHeight = mHeadImageView.getMeasuredHeight();
    //measure姓名控件的大小
    userNameWidth = mUserNameRootView.getMeasuredWidth();
    userNameHeight = mUserNameRootView.getMeasuredHeight();
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    mHeadImageView.layout((sWidth - headImageWidth)/2, mMarginTop
    ,(sWidth + headImageWidth)/2, mMarginTop + headImageHeight
            );
    //计算第三部分content的高度,其他控件的位置根据它的高度来设置
    if (contentHeight == -1) {
        contentHeight = sHeight - (mMarginTop + headImageHeight +
                50 + userNameHeight);
    }

    mUserNameRootView.layout((sWidth - userNameWidth)/2,
            sHeight - contentHeight - 50 - userNameHeight
            ,(sWidth + userNameWidth)/2,
            sHeight - contentHeight - 50
    );

    mContentRootView.layout(0, sHeight - contentHeight
            ,sWidth, sHeight
    );
    
   //初始化MaxTop,MinTop, currentTop.
    if (maxTop == 0) {
        maxTop = mMarginTop + headImageHeight + 80 + userNameHeight;
        minTop = mMarginTop + 30;
        currentTop = maxTop;
    }

}

实现滑动-ViewDragHelper

滑动子View可以通过监听ACTION_MOVE事件然后不断layout子View在ViewGroup中的位置实现滚动,但是如果完全自己写,逻辑就比较复杂了。其实Android support V4架包已经为我们提供了ViewDragHelper类,来辅助我们在自定义ViewGroup时,来处理子View的滑动需求。下面我们就简单介绍下ViewDragHelper的用法。

  • 1.我们先实例化一个ViewDragHelper对象mDragHelper,可以在onAttachedToWindow()方法中初始化,也可以在ViewGroup的构造器中初始化。It's depend on you.

    @Override
    protected void onAttachedToWindow() {
      super.onAttachedToWindow();
      mDragHelper = ViewDragHelper.create(this, 1.0f, new MiDragHelperCallback());
      mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);
    }
    
    private class MiDragHelperCallback extends ViewDragHelper.Callback {
      @Override
      public boolean tryCaptureView(View child, int pointerId) {
          return child == mContentRootView;
      }
    }
    

ViewDragHelper.Callback是一个抽象类,里面定义了很多回调方法,我们这里只说明我们用到的方法,其他的留给大家自己深入了解学习。

//是否允许抓取View,即你手指在屏幕上触摸拖动的View child是否允许被拖动
//我们这里只允许子View mContentRootView拖动。
@Override
public boolean tryCaptureView(View child, int pointerId) {
  return child == mContentRootView;
}

//拖动的子View水平方向的位置,这里其实给我们一次修改被拖动的子View水平位置的机会,我们根据需求返回值
//因为我们这里只处理Vertical方向拖动,Horizontal方向的返回0即可。
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
        return 0;
}

//与上面Horizontal类似,我们对Vertical方向的滑动位置控制在minTop和maxTop之间,这二个参数可以根据需求设置
//另外有一个topBounusFator用来表示上下滑动的弹性系数,滑动超出后弹回正确的位置,
@Override
  public int clampViewPositionVertical(View child, int top, int dy) {
      int resultTop = top;
      if (resultTop > maxTop + topBonunsFator) {
          resultTop = maxTop + topBonunsFator;
      }
      if (resultTop < minTop - topBonunsFator){
          resultTop = minTop - topBonunsFator;
      }
      return resultTop;
  }

//当子View的位置即将发生改变时,这里给了我们修改layout子View 的位置的机会,
//同时我们根据滑动的位置,还设置了mHeadImageView的透明度和 mUserNameRootView的缩放系数
//最后调用requestLayout
  @Override
  public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
        if (changedView == mContentRootView) {
            if (currentTop == top) {
                bMoved = false;
                return;
            }else {
                bMoved = true;
            }
            contentHeight = sHeight - top;
            contentScale = ((float)(maxTop - top)/(float)(maxTop - minTop) * scaleFator) + 1;
            headImageAlpha = 1 - (float)(maxTop - top) * alphaFator/(float)(maxTop - minTop);
            if (headImageAlpha < 0) {
                headImageAlpha = 0;
            }
            currentTop = top;
            mUserNameRootView.setScaleX(contentScale);
            mUserNameRootView.setScaleY(contentScale);
            mHeadImageView.setAlpha(headImageAlpha);
            requestLayout();
        }
  }    

  //当拖动的子View释放后,即手指离开屏幕后,这里我们对滑动的速度和手指的最后位置进行判断,
  //通过判断最后滑动到Top或者Bottom,通过调用mDragHelper.settleCapturedViewAt(0, maxTop);
  //注意最后需要手动刷新ViewCompat.postInvalidateOnAnimation(MineMiView.this);
    @Override
    public void onViewReleased(View releasedChild, float xvel, float yvel) {
        super.onViewReleased(releasedChild, xvel, yvel);
        if (yvel < -defaultVotical) {
            mDragHelper.settleCapturedViewAt(0, minTop);
        }else if(yvel > defaultVotical){
            mDragHelper.settleCapturedViewAt(0, maxTop);
        }else if(currentTop < (minTop + maxTop) /2) {
            mDragHelper.settleCapturedViewAt(0, minTop);
        }else {
            mDragHelper.settleCapturedViewAt(0, maxTop);

        }
        ViewCompat.postInvalidateOnAnimation(MineMiView.this);
    }

传递MotionEvent给ViewDragHelper

上面已经将ViewDragHelper初始化完毕,那么现在就需要将MineMiView的触摸事件传递给mDragHelper,其实跟利用GestureDetector来处理手势事件是类似的。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_CANCEL
            || ev.getAction() == MotionEvent.ACTION_UP) {
        mDragHelper.cancel();
        return super.onInterceptTouchEvent(ev);
    }
    boolean val = mDragHelper.shouldInterceptTouchEvent(ev);
    return val || super.onInterceptTouchEvent(ev);
} 

@Override
public boolean onTouchEvent(MotionEvent event) {
    mDragHelper.processTouchEvent(event);
    return true;
}

最后还有一点工作要做,因为我们知道手指在屏幕上滑动时,会有一个加速度,我们希望做一个减速过程来结束ViewDragHelper的settle,我们可以在computeScroll方法中做处理。

  @Override
  public void computeScroll() {
   //判断mDragHelper的settle是否结束,未结束,继续刷新ViewGroup
    if (mDragHelper.continueSettling(true)) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
  }

其实我们的骨干代码就基本完成了,我们来看下运行效果。

mine_mi_gif.gif

总结

我们利用ViewDragHelper简化了我们处理View拖动的逻辑,但是我们现在还不完善,因为,如果我们蓝色的contentView与它的子View被设置了点击事件,那么MotionEvent就不会传递到我们的MineMiView,也就不会传给我们的ViewDragHelper来处理,这时我们上面的代码肯定不能正常工作,所以我们需要额外做触摸事件的拦截,即只有当contentView符合drag的条件时,我们认为应该让MineMiView拦截事件(onInterceptTouchEvent 返回true即可),然后传递给ViewDragHelper处理来实现滑动,否则触摸事件交给子View消费。这一块具体的实现过程,我后面再出一篇详细介绍。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,050评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,945评论 4 60
  • 如何让大脑快速起跑 有几件你现在就可以做的事情,能瞬间提升大脑的性能。现在,你可以按照这些简单的建议做一条,让你的...
    Pampam阅读 728评论 0 4
  • 不是我们不努力,而是这个时代发展得太快,快到我们难以想象! 什么事好像就在那一瞬间,突然间横空出世,让人猝不及防。...
    陈娅希阅读 170评论 0 0
  • 《Pitch Perfect 2》 ——或者说《完美音调2》 已经不知道怎么形容我现在激动的心情,噼里啪啦,打字的...
    M怎么读阅读 289评论 0 2