Android 获取View在屏幕中的位置

Android里面提供了一些方法可以获取View在屏幕中的位置。

getLocationOnScreen,计算该视图在全局坐标系中的x,y值,获取在当前屏幕内的绝对坐标(该值从屏幕顶端算起,包括了通知栏高度)。

getLocationInWindow,计算该视图在它所在的widnow的坐标x,y值。

getLeft, getTop, getBottom, getRight, 这一组是获取相对在它父亲布局里的坐标。

注意:如果在Activity的OnCreate()事件输出那些参数,是全为0,要等UI控件都加载完了才能获取到这些数据。

button2.setOnClickListener(new OnClickListener() {
 
            @Override
            public void onClick(View v) {
                int[] location = new int[2];
                v.getLocationOnScreen(location);
                x = location[0];
                y = location[1];
                Log.d("test", "Screenx--->" + x + "  " + "Screeny--->" + y);
                v.getLocationInWindow(location);
                x = location[0];
                y = location[1];
                Log.d("test", "Window--->" + x + "  " + "Window--->" + y);
                Log.d("test", "left:" + v.getLeft());
                Log.d("test", "right:" + v.getRight());
                Log.d("test", "Top:" + v.getTop());
                Log.d("test", "Bottom:" + v.getBottom());
                Log.d("test", "Width:"+v.getWidth());
                Log.d("test", "Height:"+v.getHeight());
            }

getDimension()、getDimensionPixelSize()和getDimensionPixelOffset()的区别

今天写代码的时候,需要从资源文件中读取一个长度值,用来设置控件的宽高,误以为getDimension()返回的就是资源里定义的dp值,后来发现不是我理解的那样。下面介绍一下getDimension()、getDimensionPixelSize()和getDimensionPixelOffset()的区别;

先看我在资源中的定义:

<dimen name="guide_first_cover_image_width">171dp</dimen>

然后打印出它们的值看看

QLog.i(TAG,String.format("getDimension: %f",mContext.getResources().getDimension(R.dimen.guide_first_cover_image_width)) );

QLog.i(TAG,String.format("getDimensionPixelSize: %d",mContext.getResources().getDimensionPixelSize(R.dimen.guide_first_cover_image_width)));

QLog.i(TAG,String.format("getDimensionPixelOffset:%d",mContext.getResources().getDimensionPixelOffset(R.dimen.guide_first_cover_image_width)));

结果如下:

getDimension: 256.500000

getDimensionPixelSize: 257

getDimensionPixelOffset:256

我的模拟器是480*800的,屏幕密度是1.5,从打印结果就可以推知,getDimension()、getDimensionPixelSize()和getDimenPixelOffset()的结果值都是将资源文件中定义的dip值乘以屏幕密度,即171*1.5=256.5,只是getDimension()返回的是float,其余两个返回的是int, 其中getDimensionPixelSize()返回的是实际数值的四舍五入,而getDimensionPixelOffset返回的是实际数值去掉后面的小数点; 再跟踪代码查看这三个函数的具体实现,可以了解得更具体。总然而之,这三个函数返回的都是dip值乘以屏幕密度,如果你在资源文件中定义的长度单位不是dip,而是px的话,程序会直接抛出异常。

参考链接


getDimension()、getDimensionPixelSize()和getDimensionPixelOffset()的区别

Android Scroller实现View弹性滑动完全解析

说起View的滑动效果,实现的方法有多种,例如使用动画,或者通过改变View的布局参数,其实除了这两种外,在AndroidView已经为我们提供了scrollTo()scrollBy()方法来实现滑动效果,这两个方法也是我们接下来要重点讨论的...

但是呢,这两个方法实现的滑动效果都是瞬时完成的,并不能带来良好的体验哦,所以此时就需要我们的重量级嘉宾Scroller登场了,通过Scroller可以实现View在一定时间间隔内的弹性滑动,以带来更好的视觉体验,这么说可能有点抽象,举个例子,当我们使用ViewPager时,手指滑动一段距离松开后ViewPager会自动的滑动一段距离后停止,这个效果就是通过Scroller实现的,这样说应该能更好的理解Scroller的用途了。

本着从理论到实践的原则,我们来一步步的揭秘Scroller...

1. 你应该知道的scrollTo()和scrollBy()

首先看一下这两个方法的源码:

   /**
     * Move the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the amount of pixels to scroll by horizontally
     * @param y the amount of pixels to scroll by vertically
     */
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }
   /**
     * Set the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

两个方法第一个参数x表示相对于当前位置横向移动的距离,正值向左移动,负值向右移动。第二个参数y表示相对于当前位置纵向移动的距离,正值向上移动,负值向下移动,单位都是像素。
不同的是,scrollTo()方法让View相对于初始的位置滚动某段距离,scrollBy()方法则是让View相对于当前的位置滚动某段距离。同时可以发现scrollBy()是通过scrollTo()方法实现的。

上边我们说过,两个方法第一个参数x表示相对于当前位置横向移动的距离,正值向左移动,负值向右移动。这是为什么呢???看下scrollTo()方法中的mScrollX = x;一行,我们传入的x值被赋值给了mScrollX ,mScroller又是什么东东呢?其实View有一个getScrollX(),继续走进源码的世界:

public final int getScrollX() {
        return mScrollX;
    }

可以看到通过getScrollX()方法可以返回mScrollX,也就是View在 水平方向移动的距离,其实关于mScrollX的值有个变化规律:mScrollX等于View左边缘和View内容左边缘的水平方向x坐标的差值,同样的道理mScrollY 等于View上边缘和View内容上边缘的垂直方向y坐标的差值

通过scrollTo()或scrollBy()实现View的移动,只是View的内容的移动,View本身的位置并不会发生改变。此时的View可以理解为一个ViewGroup,而内容可以理解成ViewGroup中的子View,假设View的坐标原点为(0, 0),如果View内容从左向右移动,则View的内容左边缘x值将大于0,而View的左边缘x值始终为0,所以View左边缘和View内容左边缘的水平方向x坐标的差值小于0,也就是mScrollX的值小于0,这也就解释了为什么当scrollTo()或scrollBy()的x参数为负值,看起来View是向右移动的,这样也就可以理解x参数为正值的情况了,同理纵向上的y参数值也是一个道理。

说了这么多scroll相关的方法,也该聊聊我们的Scroller了...

2. Scroller弹性滑动原理

首先看一下Scroller的典型用法:

Scroller mScroller = new Scroller(mContext);

private void smoothScroll(int destX, int destY) {
        int scrollX = getScrollX();
        int deltaX = destX - scrollX;
        mScroller.startScroll(scrollX, 0, deltaX, 0, 500);
        invalidate();
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

目测startScroll()方法和View的滑动有关,继续看源码:

public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;
        mFinalX = startX + dx;
        mFinalY = startY + dy;
        mDeltaX = dx;
        mDeltaY = dy;
        mDurationReciprocal = 1.0f / (float) mDuration;
    }

原来startScroll()方法只是进行相关参数的初始化,其中startX、startY代表滑动的起点,dx、dy代表需要滑动的距离,duration代表整个滑动需要的时间。骗子...这里并没有View滑动的相关逻辑。那么View如何通过Scroller实现弹性滑动呢?其实是通过startScroll()下面的invalidate();方法,略抽象,可能我们更多的知道invalidate()方法能够使得View重绘,其实奥秘就在这里,通过使View重绘,会间接的执行computeScroll()方法,继续看源码:

public void computeScroll() {
    }

原来是一个空方法,所以需要我们自行实现,so sad...
而在computeScroll()中,我们首先通过computeScrollOffset()方法判断滑动是否结束,这个方法如何工作呢?继续看源码:

public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }

        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    
        if (timePassed < mDuration) {
            switch (mMode) {
            case SCROLL_MODE:
                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                mCurrX = mStartX + Math.round(x * mDeltaX);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
            case FLING_MODE:
                final float t = (float) timePassed / mDuration;
                final int index = (int) (NB_SAMPLES * t);
                float distanceCoef = 1.f;
                float velocityCoef = 0.f;
                if (index < NB_SAMPLES) {
                    final float t_inf = (float) index / NB_SAMPLES;
                    final float t_sup = (float) (index + 1) / NB_SAMPLES;
                    final float d_inf = SPLINE_POSITION[index];
                    final float d_sup = SPLINE_POSITION[index + 1];
                    velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                    distanceCoef = d_inf + (t - t_inf) * velocityCoef;
                }

                mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
                
                mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
                // Pin to mMinX <= mCurrX <= mMaxX
                mCurrX = Math.min(mCurrX, mMaxX);
                mCurrX = Math.max(mCurrX, mMinX);
                
                mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
                // Pin to mMinY <= mCurrY <= mMaxY
                mCurrY = Math.min(mCurrY, mMaxY);
                mCurrY = Math.max(mCurrY, mMinY);

                if (mCurrX == mFinalX && mCurrY == mFinalY) {
                    mFinished = true;
                }

                break;
            }
        } else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }

看第一个if条件,如果滑动动画已经finish,则返回fasle,继续看第二个if条件,如果执行滑动动画已经花费的时间小于整个滑动动画需要的时间,则计算出mCurrXmCurrY的值,也就是当前View内容左边缘的x、y坐标的值,同时返回true,所以,如果滑动的动画未结束则返回true否则返回false。当返回true时,则调用scrollTo(mScroller.getCurrX(), mScroller.getCurrY())进行View的滑动,其中参数mScroller.getCurrX()、mScroller.getCurrY()得到的就是上边的mCurrX和mCurrY,scrollTo之后继续执行invalidate()方法,此时又会导致View重绘,又一次执行computeScroll()方法...,这样的反复执行,直到computeScrollOffset()方法返回flase,即完成View的滑动。

通过上边的分析可以看出,仅仅通过Scroller,并不能实现View的滑动效果,同时需要配合View的invalidate()、computeScroll()、scrollTo()方法才可以完成。

说了这么多的理论,也该实践一下了...

继续阅读Android Scroller实现View弹性滑动完全解析

Android 控件 RecyclerView

概述

RecyclerView是什么

从Android 5.0开始,谷歌公司推出了一个用于大量数据展示的新控件RecylerView,可以用来代替传统的ListView,更加强大和灵活。RecyclerView的官方定义如下:

A flexible view for providing a limited window into a large data set.

从定义可以看出,flexible(可扩展性)是RecyclerView的特点。

RecyclerView是support-v7包中的新组件,是一个强大的滑动组件,与经典的ListView相比,同样拥有item回收复用的功能,这一点从它的名字Recyclerview即回收view也可以看出。

RecyclerView的优点

RecyclerView并不会完全替代ListView(这点从ListView没有被标记为@Deprecated可以看出),两者的使用场景不一样。但是RecyclerView的出现会让很多开源项目被废弃,例如横向滚动的ListView, 横向滚动的GridView, 瀑布流控件,因为RecyclerView能够实现所有这些功能。

比如:有一个需求是屏幕竖着的时候的显示形式是ListView,屏幕横着的时候的显示形式是2列的GridView,此时如果用RecyclerView,则通过设置LayoutManager一行代码实现替换

RecylerView相对于ListView的优点罗列如下:

  • RecyclerView封装了viewholder的回收复用,也就是说RecyclerView标准化了ViewHolder编写Adapter面向的是ViewHolder而不再是View了,复用的逻辑被封装了,写起来更加简单。
    直接省去了listview中convertView.setTag(holder)和convertView.getTag()这些繁琐的步骤。
  • 提供了一种插拔式的体验高度的解耦,异常的灵活,针对一个Item的显示RecyclerView专门抽取出了相应的类,来控制Item的显示,使其的扩展性非常强。
  • 设置布局管理器以控制Item布局方式横向竖向以及瀑布流方式
    例如:你想控制横向或者纵向滑动列表效果可以通过LinearLayoutManager这个类来进行控制(与GridView效果对应的是GridLayoutManager,与瀑布流对应的还StaggeredGridLayoutManager等)。也就是说RecyclerView不再拘泥于ListView的线性展示方式,它也可以实现GridView的效果等多种效果。
  • 可设置Item的间隔样式(可绘制)
    通过继承RecyclerView的ItemDecoration这个类,然后针对自己的业务需求去书写代码。
  • 可以控制Item增删的动画,可以通过ItemAnimator这个类进行控制,当然针对增删的动画,RecyclerView有其自己默认的实现。

但是关于Item的点击和长按事件,需要用户自己去实现。

继续阅读Android 控件 RecyclerView

todo-mvvm-databinding源码分析

MVVM框架 todo-mvvm-databinding 项目例子。

MVP框架 todo-mvp 项目例子。

目的

分析和学习官方mvvm框架的设计模式和数据绑定在其中的具体用法,制作一套符合当前公司业务场景的mvvm框架。其中也分析一下数据源在项目中的设计以及框架中单元测试的实施。

设计模式

MVVM框架中的`ViewModel`相比MVP框架中的`Presenter`起着类似的作用。两种框架结构的不同之处在于View分别与`ViewModel`或`Presenter`进行通信:

  • 当MVVM框架中数据发生变化时会影响`ViewModel`的改变,`View`会自动更新库或框架。你不能直接通过`ViewModel`来修改`View`。
  • 在这个项目中,你可以使用布局文件将`ViewModel`中的变量绑定到特定的UI元素上(如`TextView`或`ImageView`)。数据绑定可以确保View和ViewModel保持双向同步,如下图所示。

继续阅读todo-mvvm-databinding源码分析

error: cannot find symbol import android.support.v7.widget.ListViewCompat;

项目中,当`com.android.support:appcompat-v7`升级到`28`之后,出现如下错误

~/Source/TimeFlow/lib-ui/src/main/java/com/mobibrw/lib/ui/SwipeListView/SwipeListView.java:194: 错误: 找不到符号
import android.support.v7.widget.ListViewCompat;
                                ^
  符号:   类 ListViewCompat
  位置: 程序包 android.support.v7.widget

这个原因是由于`com.android.support:appcompat-v7`升级到`28`之后

implementation 'com.android.support:appcompat-v7:28.+'

已经不包含 `android.support.v7.widget.ListViewCompat;`这个类了。

我们要么使用`android.support.v4.widget.ListViewCompat;`替代,要么直接使用`android.widget.ListView`。

目前的解决方法是直接使用`android.widget.ListView`

参考链接


加快Android Studio 3.6.3的编译速度

1. 加大给Android Studio 3.6.3分配的内存

打开`Android Studio`的安装目录,找到虚拟机的配置文件,进行修改,如下:

Windows
编辑安装目录下的`bin\studio64.exe.vmoptions`

macOS Catalina(10.15.4)

$ sudo vim /Applications/Android\ Studio.app/Contents/bin/studio.vmoptions

继续阅读加快Android Studio 3.6.3的编译速度

Android Studio 3.6.3/4.0/4.1/4.2配置Robolectric-3.8/4.3.1/4.5.1/4.6.1 Powermock-1.6.6单元测试环境

基础配置以及常见错误


目前版本的Android Studio 3.6.3/4.0/4.1/4.2使用Robolectric-3.8/4.3.1/4.5.1/4.6.1,只能配合Powermock-1.6.6,不能高于这个版本PowerMock ,尽管软件源中的版本已经更新升级到2.0.7版本了,但是Robolectric并没有及时更新依赖。尝试过使用最新版本的PowerMock,结果很多莫名的报错。

    // 单元测试
    testImplementation "com.android.support.test:runner:1.0.2"
    testImplementation "junit:junit:4.13.2"
    testImplementation "org.robolectric:robolectric:4.5.1"

    // 注意AndroidX环境下,不能引入shadows-supportv4,Jetifier转换的时候报错
    testImplementation "org.robolectric:shadows-supportv4:4.5.1"

    // MultiDex支持
    testImplementation "org.robolectric:shadows-multidex:4.5.1"
    
    // 此处版本更新需要特别小心,目前 robolectric 跟 powermock 存在兼容问题,
    // 需要固定的某些版本才能正常配合
    // 依赖关系参考 
    // https://github.com/robolectric/robolectric/wiki/Using-PowerMock
    testImplementation "org.powermock:powermock-module-junit4:1.6.6"
    testImplementation "org.powermock:powermock-module-junit4-rule:1.6.6"
    testImplementation "org.powermock:powermock-classloading-xstream:1.6.6"
    testImplementation "org.powermock:powermock-api-mockito:1.6.6"

如果 Windows 系统下出现如下提示,受限于Windows系统路径不能超过260个字符的限制

13:36 Error running 'xxxTest': Command line is too long. Shorten command line for xxxTest or also for Android JUnit default configuration.

貌似Windows 10 Build 14352版中已经可以通过修改注册表去除260字符的路径长度限制了。

在项目`/.idea/workspace.xml`文件中添加一行代码如下

<component name="PropertiesComponent">
    ...
    <property name="dynamic.classpath" value="true" />
</component>

对于使用MultiDex的应用,如果报告如下错误:

java.lang.RuntimeException: MultiDex installation failed (/var/folders/h6/4__l0pyn32j1mkkysynnxydr0000gn/T/robolectric-Method_xxxxxxxxxxxx_isCorrect4035485067136008322/com.xxxx.xxxx.app-sourceDir (Is a directory)).

	at android.support.multidex.MultiDex.install(MultiDex.java:121)
	at com.xxx.xx.app.xx.application.xxxxxx.attachBaseContext(MyApplication.java:212)
	at android.app.Application.attach(Application.java:212)
	at org.robolectric.util.ReflectionHelpers.callInstanceMethod(ReflectionHelpers.java:283)
	at org.robolectric.shadows.ShadowApplication.callAttach(ShadowApplication.java:79)
	at org.robolectric.android.internal.AndroidTestEnvironment.installAndCreateApplication(AndroidTestEnvironment.java:250)
	at org.robolectric.android.internal.AndroidTestEnvironment.setUpApplicationState(AndroidTestEnvironment.java:169)
	at org.robolectric.RobolectricTestRunner.beforeTest(RobolectricTestRunner.java:301)
	at org.robolectric.internal.SandboxTestRunner$2.lambda$evaluate$0(SandboxTestRunner.java:243)
	at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:89)
	at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
	at java.util.concurrent.FutureTask.run(FutureTask.java)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

需要在`build.gradle`中引入

 
    // MultiDex支持
    testImplementation "org.robolectric:shadows-multidex:4.5.1"

例子测试代码:

package com.mobibrw.example;

import org.junit.Test;
import static org.junit.Assert.*;

import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.multidex.ShadowMultiDex;

/**
 * Example local unit test, which will execute on the development machine (host).
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
@RunWith(RobolectricTestRunner.class)
@PowerMockIgnore("jdk.internal.reflect.*")
@Config(shadows = ShadowMultiDex.class)
public class ExampleUnitTest {
    @Test
    public void addition_isCorrect() {
        assertEquals(4, 2 + 2);
    }
}

注意,需要

@Config(shadows = ShadowMultiDex.class)

才能使得配置生效。

如果发生如下错误:

java.lang.VerifyError: Expecting a stackmap frame at branch target 43
Exception Details:
  Location:
    com/meizu/cloud/pushsdk/base/IntentReceiver.onReceive(Landroid/content/Context;Landroid/content/Intent;)V @6: ifne
  Reason:
    Expected stackmap frame at this location.
  Bytecode:
    0x0000000: b200 08b6 0013 9a00 25bb 0004 5912 0110
    0x0000010: 0ab7 000e 59b3 000a b600 10bb 0003 59b2
    0x0000020: 000a b600 0fb7 000c b300 09b2 0009 bb00
    0x0000030: 0659 2a2b 2cb7 0011 b600 0d57 b1       


	at java.lang.Class.getDeclaredConstructors0(Native Method)
	at java.lang.Class.privateGetDeclaredConstructors(Class.java:2671)
	at java.lang.Class.getConstructor0(Class.java:3075)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at org.robolectric.util.ReflectionHelpers.callConstructor(ReflectionHelpers.java:396)
	at org.robolectric.internal.bytecode.ShadowImpl.newInstanceOf(ShadowImpl.java:18)
	at org.robolectric.shadow.api.Shadow.newInstanceOf(Shadow.java:35)
	at org.robolectric.android.internal.AndroidTestEnvironment.registerBroadcastReceivers(AndroidTestEnvironment.java:541)
	at org.robolectric.android.internal.AndroidTestEnvironment.installAndCreateApplication(AndroidTestEnvironment.java:265)
	at org.robolectric.android.internal.AndroidTestEnvironment.setUpApplicationState(AndroidTestEnvironment.java:169)
	at org.robolectric.RobolectricTestRunner.beforeTest(RobolectricTestRunner.java:301)
	at org.robolectric.internal.SandboxTestRunner$2.lambda$evaluate$0(SandboxTestRunner.java:243)
	at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:89)
	at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
	at java.util.concurrent.FutureTask.run(FutureTask.java)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

继续阅读Android Studio 3.6.3/4.0/4.1/4.2配置Robolectric-3.8/4.3.1/4.5.1/4.6.1 Powermock-1.6.6单元测试环境

在macOS Catalina(10.15.4)安装ADB(Android调试桥)

什么是 ADB?

Android调试桥( adb )是一个开发工具,帮助安卓设备和个人计算机之间的通信。 这种通信大多是在USB电缆下进行,但是也支持Wi-Fi连接。 adb 还可被用来与电脑上运行的安卓模拟器交流通信。 adb 对于安卓开发来说就像一把“瑞士军刀”。

通过 Homebrew 安装

$ brew cask install android-platform-tools

测试是否正常安装

$ adb devices

手动安装

1、如果你以前安装过,请先删除老的文件

$ rm -rf ~/.android-sdk-macosx/

2、下载 android-sdk-macosx

下载地址:developer.android.com

3、将下载的文件解压并移动到 ~/.android-sdk-macosx

$ cd ~/Downloads(你的下载目录)/ 

$ unzip android-sdk*.zip 

$ mv android-sdk-macosx/ ~/.android-sdk-macosx

4、运行 SDK Manager

$ sh ~/.android-sdk-macosx/tools/android

5、根据你的需要选择,(我只需要Android SDK Platform-tools)[可选步骤]
6、选好后 Install
7、环境变量设置

$ echo 'export PATH=$PATH:~/.android-sdk-macosx/platform-tools/' >> ~/.bash_profile

8、更新配置文件

$ source ~/.bash_profile

9、测试是否正常安装

$ adb devices

参考链接


在 MAC OS X 安装 ADB (Android调试桥)

MVC/MVP/MVVM/MVPVM区别

分析主要是通过它的控制链、控制流向,View 的变化如何反馈到Model,以及Model的变化如何作用到View上。

MVC

View 持有了Controller,把事件传递给Controller,Controller 由此去触发Model层的事件,Model更新完数据(网络或者本地数据)之后触发View的更新事件

MVP

咋看一下MVP只是MVC的变更版,把C层替换成了P层,实则不是这样的,最根本的是添加了Presenter层。

MVP其实是MVC的封装和演化,Controller被拆分,只用它处理View的点击事件,数据绑定,等处理,而View被拆分,更加专注于视图的更新,只做跟视图相关的操作,而Presenter被独立出来,用于沟通View和Model之间的联系,Model不能直接作用于View 的更新,只能通过Presenter来通知View进行视图的刷新,比如showLoading(),showEmpty(),showToast()等等,这样View就完全被独立出来了,只是被动接受Presenter的命令,这样避免了View 有过多的逻辑处理,更加简单。Presenter持有了Model。Model 只用于处理跟数据获取相关的逻辑。

MVVM

又称状态机制,View和ViewModel 是进行绑定的,改变ViewModel 就会直接作用到View视图上,而View 会把事件传递给ViewModel,ViewModel去对Model进行操作并接受更新。

MVPVM

可以看到MVPVM 其实就是MVP的变种,加入了MVVM事件特性,增加了ViewModel,功能分类:
View:只做视图更新操作
Model: 只做数据处理,网络数据 、本地数据
Presenter: 只做业务逻辑处理,View或者Model 事件分发
ViewModel: 绑定View 和 Model,添加数据变更监视器

Android官方给出的例子参考 todo-mvvm-databindingtodo-mvp

参考链接


MVC、MVP、MVVM、MVPVM区别