手记

Android UI进阶之旅4--Material Design之消息提示

前言

Android中的消息提示主要有一下几种:

  1. Toast: 用户无法交互。

  2. Dialog:用户可以交互,但是体验会打折扣,会阻断用户的连贯性操作。

  3. Snackbar既可以做到轻量级的用户提醒效果,又可以有交互的功能(必须是一种非必须的操作)。Snackbar的提出实际上是界于Toast和Dialog的中间产物。

自定义吐司

我们平常通过下面的代码进行弹出一个吐司的:

Toast.makeText(this, "吐司", Toast.LENGTH_SHORT).show();

深入makeText方法瞄一眼:

public static Toast makeText(Context context, @StringRes int resId, @Duration int duration)
                            throws Resources.NotFoundException {
    return makeText(context, context.getResources().getText(resId), duration);
}

另外一个重载:

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
    Toast result = new Toast(context);

    LayoutInflater inflate = (LayoutInflater)
            context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
    TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
    tv.setText(text);

    result.mNextView = v;
    result.mDuration = duration;

    return result;
}

可以清晰地看到Toast的创建过程,因此我们也仿照这样就可以自定义吐司了:

Toast result = new Toast(SnackBarActivity.this);

LayoutInflater inflate = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflate.inflate(R.layout.my_toast, null);
TextView tv = (TextView) view.findViewById(R.id.message);
tv.setText("这是一个自定义吐司");

result.setView(view);
result.setDuration(Toast.LENGTH_SHORT);

result.show();

其中,mNextView以及mDuration是私有的成员,我们可以通过方法去设置。

最后看看show方法:

public void show() {
    if (mNextView == null) {
        throw new RuntimeException("setView must have been called");
    }

    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;

    try {
        service.enqueueToast(pkg, tn, mDuration);
    } catch (RemoteException e) {
        // Empty
    }
}

可以看到Toast的管理是通过INotificationManager来实现的,这是一个与进程间通信有关的类。show方法调用的时候,并不是马上就显示的,而是加入了一个队列里面。

SnackBar的基本使用

下面先给出代码:

//其中View是一个锚点
Snackbar snackbar = Snackbar.make(v, "是否打开XXX模式", Snackbar.LENGTH_SHORT);

//只能设置一个Action
snackbar.setAction("打开", new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.e(TAG, "打开XXX模式");
    }
});
//监听打开与关闭
snackbar.setCallback(new Snackbar.Callback() {
    @Override
    public void onShown(Snackbar snackbar) {
        super.onShown(snackbar);
        Log.e(TAG, "显示");
    }

    @Override
    public void onDismissed(Snackbar snackbar, int event) {
        super.onDismissed(snackbar, event);
        Log.e(TAG, "关闭");
    }
});
snackbar.show();

Snackbar的Duration有三种:

  1. Snackbar.LENGTH_SHORT

  2. Snackbar.LENGTH_LONG

  3. Snackbar.LENGTH_INDEFINITE---无限长

make方法传入的是一个锚点,这里我传入了一个Button对象。然后还可以设置动作以及回调监听。

SnackBar源码分析

同理,我们先分析make方法:

@NonNull
public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
        @Duration int duration) {
    Snackbar snackbar = new Snackbar(findSuitableParent(view));
    snackbar.setText(text);
    snackbar.setDuration(duration);
    return snackbar;
}

@NonNull
public static Snackbar make(@NonNull View view, @StringRes int resId, @Duration int duration) {
    return make(view, view.getResources().getText(resId), duration);
}

传入的view是一个锚点,SnackBar的显示需要依附于parent,就像我们创建View的时候需要传入一个context一样。(注意,这里的SnackBar只是一个简单的类,并不是继承于View的)我们需要的parent就是利用这个view通过findSuitableParent方法进行找到的:

private static ViewGroup findSuitableParent(View view) {
    ViewGroup fallback = null;
    do {
        if (view instanceof CoordinatorLayout) {
            // 如果是CoordinatorLayout直接返回
            return (ViewGroup) view;
        } else if (view instanceof FrameLayout) {
            if (view.getId() == android.R.id.content) {
                // 如归找到Android的根布局,那么直接返回
                return (ViewGroup) view;
            } else {
                // It's not the content view but we'll use it as our fallback
                fallback = (ViewGroup) view;
            }
        }

        if (view != null) {
            // Else, we will loop and crawl up the view hierarchy and try to find a parent
            final ViewParent parent = view.getParent();
            view = parent instanceof View ? (View) parent : null;
        }
    } while (view != null);

    // If we reach here then we didn't find a CoL or a suitable content view so we'll fallback
    return fallback;
}

如上面的代码所示,就是不断通过循环,view.getParent()不断获取parent去找到最合适的parent的。

下面我们继续分析构造方法:

private Snackbar(ViewGroup parent) {
    mTargetParent = parent;
    mContext = parent.getContext();

    ThemeUtils.checkAppCompatTheme(mContext);

    LayoutInflater inflater = LayoutInflater.from(mContext);
    //渲染一个布局进来,但是并没有直接添加到parent里面去,最后Snackbar去通过addView或者通过动画显示出来
    mView = (SnackbarLayout) inflater.inflate(R.layout.design_layout_snackbar, mTargetParent, false);

    mAccessibilityManager = (AccessibilityManager)
            mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
}

构造的时候就会先渲染一个布局然后设置进来,并且添加到parent里面,这个布局我们不能再修改了。其中SnackbarLayout是一个内部的类。

R.layout.design_layout_snackbar中就是引用了这个内部类,这是自定义控件的另外一种写法,通过使用view标签(小写),然后设置class属性:

<view xmlns:android="http://schemas.android.com/apk/res/android"
      class="android.support.design.widget.Snackbar$SnackbarLayout"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_gravity="bottom"
       />

在SnackbarLayout里面实质上会继续渲染一个布局到this(自己)里面来:

public SnackbarLayout(Context context, AttributeSet attrs) {
    //...省略一些代码
    LayoutInflater.from(context).inflate(R.layout.design_layout_snackbar_include, this);
}

同样,我们继续分析show方法:

public void show() {
    SnackbarManager.getInstance().show(mDuration, mManagerCallback);
}

这里实际上是调用了SnackbarManager的show方法:

public void show(int duration, Callback callback) {
    synchronized (mLock) {
        if (isCurrentSnackbarLocked(callback)) {
            // 如果是同一个,那么就更新时间参数
            mCurrentSnackbar.duration = duration;

            // If this is the Snackbar currently being shown, call re-schedule it's
            // timeout
            mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
            scheduleTimeoutLocked(mCurrentSnackbar);
            return;
        } else if (isNextSnackbarLocked(callback)) {
            // 如果是下一个
            mNextSnackbar.duration = duration;
        } else {
            // 如果都不是,那么就创建一个记录。SnackbarRecord的作用相当于ActivityRecord
            mNextSnackbar = new SnackbarRecord(duration, callback);
        }

        if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,
                Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) {
            // If we currently have a Snackbar, try and cancel it and wait in line
            return;
        } else {
            // Clear out the current snackbar
            mCurrentSnackbar = null;
            // 显示
            showNextSnackbarLocked();
        }
    }
}

这里进行了一次synchronized,就防止了同时显示多个的情况。这种队列(类似于消息机制)有效地防止了UI的ANR。

SnackbarRecord的定义如下,其中就包含了当前SnackBar的一些信息:

private static class SnackbarRecord {
    final WeakReference<Callback> callback;//弱引用防止内存泄漏
    int duration;

    SnackbarRecord(int duration, Callback callback) {
        this.callback = new WeakReference<>(callback);
        this.duration = duration;
    }

    boolean isSnackbar(Callback callback) {
        return callback != null && this.callback.get() == callback;
    }
}

继续分析showNextSnackbarLocked:

private void showNextSnackbarLocked() {
    if (mNextSnackbar != null) {
        mCurrentSnackbar = mNextSnackbar;
        mNextSnackbar = null;

        final Callback callback = mCurrentSnackbar.callback.get();
        if (callback != null) {
            callback.show();
        } else {
            // The callback doesn't exist any more, clear out the Snackbar
            mCurrentSnackbar = null;
        }
    }
}

首先拿到下一个SnackBar对象,然后拿到callback对象,最后回调callback的show方法。

那么,我们的SnackBar是怎么显示出来的呢?还记得SnackbarManager的show方法传入的callback是在SnackBar中定义的(callback接口是Snackbar与SnackbarManager交互的接口):

final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
    @Override
    public void show() {
        sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, Snackbar.this));
    }

    @Override
    public void dismiss(int event) {
        sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, Snackbar.this));
    }
};

最后的显示是通过handler来实现的:

static {
    sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
        @Override
        public boolean handleMessage(Message message) {
            switch (message.what) {
                case MSG_SHOW:
                    ((Snackbar) message.obj).showView();
                    return true;
                case MSG_DISMISS:
                    ((Snackbar) message.obj).hideView(message.arg1);
                    return true;
            }
            return false;
        }
    });
}

下面继续分析showView方法:

final void showView() {
    if (mView.getParent() == null) {
        final ViewGroup.LayoutParams lp = mView.getLayoutParams();

        if (lp instanceof CoordinatorLayout.LayoutParams) {
            //如果parent是CoordinatorLayout的处理
        }

        mTargetParent.addView(mView);
    }

    mView.setOnAttachStateChangeListener(new SnackbarLayout.OnAttachStateChangeListener() {
        @Override
        public void onViewAttachedToWindow(View v) {}

        @Override
        public void onViewDetachedFromWindow(View v) {
            if (isShownOrQueued()) {
                sHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        onViewHidden(Callback.DISMISS_EVENT_MANUAL);
                    }
                });
            }
        }
    });

    //如果parent以及显示出来了,那么直接animateViewIn
    if (ViewCompat.isLaidOut(mView)) {
        if (shouldAnimate()) {
            animateViewIn();
        } else {
            onViewShown();
        }
    } else {
        //否则的话需要设置setOnLayoutChangeListener等待布局显示出来以后才能animateViewIn
        mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View view, int left, int top, int right, int bottom) {
                mView.setOnLayoutChangeListener(null);

                if (shouldAnimate()) {
                    animateViewIn();
                } else {
                    onViewShown();
                }
            }
        });
    }
}

下面继续分析animateViewIn:

void animateViewIn() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
        ViewCompat.setTranslationY(mView, mView.getHeight());
        ViewCompat.animate(mView)
                .translationY(0f)
                .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR)
                .setDuration(ANIMATION_DURATION)
                .setListener(new ViewPropertyAnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(View view) {
                        mView.animateChildrenIn(ANIMATION_DURATION - ANIMATION_FADE_DURATION,
                                ANIMATION_FADE_DURATION);
                    }

                    @Override
                    public void onAnimationEnd(View view) {
                        onViewShown();
                    }
                }).start();
    } else {
        Animation anim = AnimationUtils.loadAnimation(mView.getContext(),
                R.anim.design_snackbar_in);
        anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
        anim.setDuration(ANIMATION_DURATION);
        anim.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationEnd(Animation animation) {
                onViewShown();
            }

            @Override
            public void onAnimationStart(Animation animation) {}

            @Override
            public void onAnimationRepeat(Animation animation) {}
        });
        mView.startAnimation(anim);
    }
}

这里根据不同的SDK版本,利用了不同的动画去实现。

原文链接:http://www.apkbus.com/blog-0-65504.html

0人推荐
随时随地看视频
慕课网APP