应用内预览适配

来自Flyme开放平台
跳转至: 导航搜索

压力触控开发者指南

1.简介

压力触控是指在使用了压力屏的魅族手机上,应用能够根据当前按压屏幕的力度提供下一级界面的内容预览的功能,这样可以在不直接打开下一级界面的情况下预览下一级界面的内容,类似于iphone 上的3D Touch,具体效果可见下图的演示,或者直接安装文档尾部附件的demo来体验。

4ac961c5c780462f8b6ed78fb6747e66.gif

本文档主要就是介绍第三方如何通过我们提供的压力触控SDK来接入这一功能。

2.开发工具及工程配置

由于Android Studio已经作为目前主流的Android应用开发工具,所以我们提供的SDK也会用来适配Android Studio,目前会提供给开发者PeekAndPop.aar,里面提供了关于压力触控的所有api。
应用可以参考下图的演示来导入这个aar到工程中,先将这个aar拷贝到工程根目录下的libs库中,如下图所示:

b7ee67d6444f4cb1a15bad121b3b3595.png

之后在工程的build.gradle中按照如下图所示配置,注意要同时引入support-v7的库

830bc4d25fa9468fbbbeab066f456331.png

最后需要在proguard-rules.pro中配置以下内容,避免相关方法被混淆。
-keep class com.meizu.common.renderer.**{*;}
-dontwarn com.meizu.common.renderer**

3.压力触控流程及SDK接入

压力触控分两个阶段,预览(Peek)和全屏(Pop),如下图所示:

65f25f7fd7de414aacfa8dd2126a2b24.png

以在应用内点击列表项启动压力触控为例来说明,在刚开始点击列表项时,当手指按压屏幕的力度超过一定阈值后,所在的列表项会有一个浮起的效果,同时整个界面会盖上一层毛玻璃的效果(如下左第一张),继续稍微施加压力后,就会出现新界面的预览效果(Peek)(如下左二,三,四张),在预览的情况下,可以上划出底部的菜单,或者下滑出顶部的更新操作。最后再继续施压的话,就会从预览进入最终的全屏效果(Pop)(如下右第一张)。

71c1292cd4914da3aa8ddd95197d7314.jpg 542258a8d8d840658e9c7366b8eb0b88.jpg 756d3dc483894bb797e675e1dda2bc4c.jpg 9ebdd779066e494798c7ae936fcfe597.jpg bf1d7fa7fde74b8e833c9665c31973e3.jpg

对应用适配来说,只需熟悉PeekAndPopHelper这个类即可,更直观的来说要适配压力触控,其实只要实现这个类里面的一个方法即可。
该类内部有一个内部类PeekAndPopConfig,该内部类主要用来配置一些压力触控相关的参数,大部分情况下都是通过这个类里面相关成员的设置,来获取压力触控需要的一些参数,下面这个表格是目前PeekAndPopConfig中已经提供的一些成员及相关含义:

名称 参数类型 具体含义
mPeekType int 压力触控的类型,具体参数可以是TYPE_VIEW_OR_FRAGMENT 适用于下一级界面是Fragment实现或者通过AddView的方式添加 TYPE_ACTIVITY适用于下一级的界面是Activity
mConfirmBitmap Bitmap 压力触控开始时按下区域的内容,即上面流程中描述的浮起的效果
mConfirmRect Rect 压力触控开始时浮起的区域,这个区域是相对于整个屏幕的,可以通过getLocationOnScreen方法来获取
mConfirmBgResId int 压力触控开始时按下区域的背景颜色,只适用于TYPE_VIEW_OR_FRAGMENT或者TYPE_ACTIVITY,默认可以不用配置
mPeekView WeakReference<View> Peek阶段用于展示预览内容的view,到时预览的内容就是从mPeekView中获取
mPeekRect Rect 用来配合上面的mPeekView,其坐标系是以mPeekView来参考的。假如传入null,则说明预览的内容就是mPeekView中的所有内容,如果传入一个非null的区域,则到时就以mPeekRect来裁剪mPeekView,预览显示裁剪后的内容
mMenuBuilder MenuBuilder 配置预览时上划出来的底部菜单,通常应用可以在res/menu/下通过一个xml来配置,然后在代码中解析
mMenuBackgroundColor int 配置预览时上划出来的菜单的背景,需要一个颜色值
mPullReresh View 配置预览时下拉的“已读/未读”的布局,通常可以直接使用PeekAndPop库中提供的peek_pull_to_mark_layout
mMarkToRead boolean 配合上面的mPullReresh使用,用来设置预览下拉时的初始状态,true表示“已读”
mDisablePop boolean 是否允许进入Pop阶段,通常不用设置
mHasVerticalScrollArrow boolean 是否显示上拉的箭头,默认为true

下面是PeekAndPopHelper类中相关API接口说明

public static void enablePeekAndPop(View source, PeekAndPopConfig config, PeekAndPopListener listener)
启动压力触控的入口函数,基本应用只要实现这个方法,整个压力触控的适配就基本完成了。

参数名称 说明
source 需要响应压力触控的控件,设置的原则是尽量不要太宽泛,比如只需要listview中的子项响应重压,传入listview对象就好了,就不要传入listview的父view甚至整个界面的根节点之类的,总之,如果你只需要某个ViewGroup内的控件响应重压,就只要传入这个ViewGroup即可,尽量不要把无关的view也包裹进来。
config 包含压力触控参数的PeekAndPopConfig对象。
listener 压力触控过程的回调接口。

PeekAndPopListener
压力触控过程中的回调接口。

函数类型 说明
boolean peek(MotionEvent event, PeekAndPopConfig config);压力触控启动,并且预览界面即将出现前调用。
void onPeekMenuItemClick(AdapterView<?> parent, View view, int position, long id);点击预览弹出菜单的时候回调。
void cancel();预览正常退出的时候调用。
boolean pop(View peekView);从预览阶段进入最终全屏的时候调用。
void onPulldownViewChanged();在预览界面,一直下拉到"已读/未读"的状态改变了,此时要放手,让其退出预览,回退到正常情况下时,该回调才能被调用。

public static boolean notifyActivityPeekReady(Activity b, PeekAndPopConfig config)
如果下一级界面是通过Activity实现的,则需要在这个Activity的onCreate里面调用,并传入相关的参数,详见后面的示例。

参数名称 说明
b 预览内容为Activity,该参数传入的是需要预览的Activity对象。
config 包含压力触控参数的PeekAndPopConfig对象。

返回结果:true 表示当前已经启动了预览,false 没有启动预览。

public static void animToNormal()
方便应用通过代码来取消预览的效果。

public static void cancelForActivityPeek()
如果下一级界面是Activity实现的,在预览退出的时候需要调用此方法来finish掉新界面对应的Activity,只适用于mPeekType是TYPE_ACTIVITY的情况。

public static void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)
方便我们打印当前压力触控相关的变量,应用只需在自己的Activtiy中重写系统的dump方法,并在内部调用此方法即可。

由于新界面的预览可以是新的一个Activity,新的Fragment或者直接通过一个View来展示,不同情况下的适配会稍微有些不同,下面就分类说明:


预览内容通过View直接展示

预览内容为新添加的view,首先需要创建一个config,设置config的peekType为TYPE_VIEW_OR_FRAGMENT,调用PeekAndPopHelper.enablePeekAndPop 传入需要响应压力触控的view,config,以及回调函数。在回调里面的peek接口里做好初始化工作。以下示例中有对必须设置项进行标记,请务必设置好!

mRootView = (RelativeLayout) findViewById(R.id.image_group);
PeekAndPopHelper.PeekAndPopConfig config = new PeekAndPopHelper.PeekAndPopConfig();
//即将启动的预览界面是新添加的一个view
config.mPeekType = PeekAndPopHelper.PeekAndPopConfig.TYPE_VIEW_OR_FRAGMENT;
//mRootView即需要响应压力触控的view,PeekAndPop库会监听此view上的压力触控,一旦触发了压力触控就会回调下面listener中的peek方法
PeekAndPopHelper.enablePeekAndPop(mRootView, config, new PeekAndPopHelper.PeekAndPopListener() {
    View newView;
 
    /**
     * 在检测到当前压力值超过阈值时调用,暗示压力触控开始了
     * @param event
     * @param config
     * @return 返回true表示要响应压力触控,否则就不响应压力触控
     */
    @Override
    public boolean peek(MotionEvent event, PeekAndPopHelper.PeekAndPopConfig config) {
        //传入进来的event参考的坐标系即刚才传入的mRootView
        //下面是提供一个辅助类帮助应用找到当前是点击了mRootView的哪个子view,当然这部分逻辑也可以应用根据自己的需求去实现
        View forceTouchView = PeekAndPopUtil.getForceTouchViewFromViewGroup(mRootView, event);
        if (forceTouchView == null) {
            return false;
        }
        int id = forceTouchView.getId();
        if (!isTargetView(id)) {
            return false;
        }
 
        int[] location = new int[2];
        forceTouchView.getLocationOnScreen(location);
        Rect rect = new Rect(location[0], location[1],
                location[0] + forceTouchView.getWidth(), location[1] + forceTouchView.getHeight());
        //此处一定要传入view的全局location,通过上面的getLocationOnScreen()
        config.mConfirmRect = rect;//必须要的
        config.mConfirmBitmap = PeekAndPopUtil.getBitmapFromView(forceTouchView);//必须要的
 
        newView = getLayoutInflater().inflate(R.layout.image_view_layout, null);
        ImageView iv = (ImageView) newView.findViewById(R.id.iv_preview);
        setImageResource(id, iv);
        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
        //newView是预览的内容
        mRootView.addView(newView, params);
        config.mPeekView = new WeakReference<View>(newView);
 
        //告知预览界面的裁剪区域,也即是在最终界面的基础上裁剪一个区域用来做预览的界面,如果不指定mPeekRect则不做任何裁剪
        Rect clipRect = new Rect();
        clipRect.set(0, 0, mRootView.getWidth(), mRootView.getHeight());
        config.mPeekRect = clipRect;//必须要的
 
 
        //预览界面上划时出现的底部菜单,目前是通用的menu配置,应用只需直接配置一个xml即可,里面也可以根据需要配置二级菜单
        MenuBuilder menu = new MenuBuilder(AddViewActivity.this);
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.submenu, menu);
        config.mMenuBuilder = menu;
        return true;
    }
 
    /**
     * 取消预览的时候调用,也即此时会恢复到当前界面的正常情况
     */
    @Override
    public void cancel() {
        if (newView != null) {
            mRootView.removeView(newView);
            newView = null;
        }
    }
    ...
    ...
});

预览内容为Fragment

创建config,配置config的参数,调用PeekAndPopHelper.enablePeekAndPop,实现回调函数,peek里面需要使用add方式添加需要预览的fragment,cancel接口需要使用popback在结束重压的时候恢复fragment的状态。

 PeekAndPopHelper.PeekAndPopConfig config = new PeekAndPopHelper.PeekAndPopConfig();
//即将启动的预览界面是新添加的一个Fragment
config.mPeekType = PeekAndPopHelper.PeekAndPopConfig.TYPE_VIEW_OR_FRAGMENT;
//mListView即需要响应重压的view,PeekAndPop库会监听此view上的压力触控,一旦压力触控启动了就会回调下面listener中的peek方法
PeekAndPopHelper.enablePeekAndPop(mListView, config, new PeekAndPopHelper.PeekAndPopListener() {
     /**
     * 在检测到当前压力值超过阈值时调用,暗示压力触控开始了
     * @param event
     * @param config
     * @return 返回true表示要响应压力触控,否则就不响应压力触控
     */
    @Override
    public boolean peek(MotionEvent event, final PeekAndPopHelper.PeekAndPopConfig config) {
        //Toast.makeText(getActivity(), "startPeekAndPop !!!! ", Toast.LENGTH_SHORT).show();
        //方便应用得知当前是点击了列表的哪一个子项
        View forceTouchView = PeekAndPopUtil.getForceTouchViewFromAbsListView(mListView, event);
        if (forceTouchView != null) {
            WindowUtil.setStatusBarTranslucent(getActivity(),true);
            int[] location = new int[2];
            forceTouchView.getLocationOnScreen(location);
            Rect rect = new Rect(location[0], location[1],
                    location[0] + forceTouchView.getWidth(), location[1] + forceTouchView.getHeight());
            //此处一定要传入view的全局location,通过上面的getLocationOnScreen()
            config.mConfirmRect = rect;//必须要的
            config.mConfirmBitmap = PeekAndPopUtil.getBitmapFromView(forceTouchView);//必须要的
        } else {
            return false;
        }
 
        //预览的界面
        //目前Fragment的添加请使用add的方式,而不要使用replace的方式
        final Fragment fragment = new MsgDetailFragment();
        FragmentTransaction fragmentTransaction = getActivity().getFragmentManager().beginTransaction();
        fragmentTransaction.add(R.id.fragment_container, fragment);
        fragmentTransaction.addToBackStack(null);
        fragmentTransaction.commit();
        View view = getActivity().findViewById(R.id.fragment_container);
 
        //告知预览界面的裁剪区域,也即是在最终界面的基础上裁剪一个区域用来做预览的界面,如果不指定mPeekRect则不做任何裁剪
        Rect clipRect = new Rect();
        clipRect.set(0, 0, view.getWidth(), view.getHeight() - 100);
        config.mPeekRect = clipRect;//必须要的
 
        //预览界面上划时出现的底部菜单,目前是参考通用的menu配置,应用只需直接配置一个xml即可,里面也可以根据需要配置二级菜单
        MenuBuilder menu = new MenuBuilder(getActivity());
        MenuInflater inflater = getActivity().getMenuInflater();
        inflater.inflate(R.menu.submenu, menu);
        config.mMenuBuilder = menu;
        LayoutInflater layoutInflater = getActivity().getLayoutInflater();
        View pullView = layoutInflater.inflate(com.meizu.forcetouch.R.layout.peek_pull_to_mark_layout, null);
        config.mPullReresh = pullView;
        config.mMarkToRead = true;
 
        //此处必须post一下,否则直接调用fragment.getView() 返回的view是null
        view.post(new Runnable() {
            @Override
            public void run() {
                fragment.getView().setVisibility(View.INVISIBLE);
                config.mPeekView = new WeakReference<View>(fragment.getView());//必须要的
            }
        });
        return true;
    }
 
    @Override
    public void onPeekMenuItemClick(AdapterView<?> parent, View view, int position, long id) {
    }
 
    /**
     * 取消预览的时候调用,也即此时会恢复到当前界面的正常情况
     */
    @Override
    public void cancel() {
        //这里应用可以按照自己的要求来实现,例如退出刚才add的fragment等。
        getActivity().getFragmentManager().popBackStack();
    }
    ...
    ...
});

预览内容为Activity

创建config,配置config的参数,然后调用PeekAndPopHelper.enablePeekAndPop,实现回调接口,必须在peek接口里面启动需要预览的Activity。

PeekAndPopHelper.PeekAndPopConfig config = new PeekAndPopHelper.PeekAndPopConfig();
//即将启动的是Activity
config.mPeekType = PeekAndPopHelper.PeekAndPopConfig.TYPE_ACTIVITY;
//mListView即需要响应压力触控的view,PeekAndPop库会监听此view上的压力触控,一旦压力触控启动了就会回调下面listener中的peek方法
PeekAndPopHelper.enablePeekAndPop(mListView, config, new PeekAndPopHelper.PeekAndPopListener() {
     /**
     * 在检测到当前压力值超过阈值时调用,暗示压力触控开始了
     * @param event
     * @param config
     * @return 返回true表示要响应压力触控,否则就不响应压力触控
     */
    @Override
    public boolean peek(MotionEvent event, PeekAndPopHelper.PeekAndPopConfig config) {
 
        View forceTouchView = PeekAndPopUtil.getForceTouchViewFromAbsListView(mListView, event);
 
        if (forceTouchView == null) {
            return false;
        }
 
        int[] location = new int[2];
        forceTouchView.getLocationOnScreen(location);
        Rect rect = new Rect(location[0], location[1],
                location[0] + forceTouchView.getWidth(), location[1] + forceTouchView.getHeight());
        //此处一定要传入view的全局location,通过上面的getLocationOnScreen()
        config.mConfirmRect = rect;//必须要的
        config.mConfirmBitmap = PeekAndPopUtil.getBitmapFromView(forceTouchView);//必须要的
 
        //预览界面上划时出现的底部菜单,目前是参考通用的menu配置,应用只需直接配置一个xml即可,里面也可以根据需要配置二级菜单
        MenuBuilder menu = new MenuBuilder(EmailListActivity.this);
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.submenu, menu);
        config.mMenuBuilder = menu;
 
        LayoutInflater layoutInflater = getLayoutInflater();
        View view = layoutInflater.inflate(com.meizu.forcetouch.R.layout.peek_pull_to_mark_layout, null);
        config.mPullReresh = view;
        config.mMarkToRead = true;
        startActivity(mIntent);
        return true;
    }
 
    /**
     * 取消预览的时候调用,也即此时会恢复到当前界面的正常情况
     */
    @Override
    public void cancel() {
        PeekAndPopHelper.cancelForActivityPeek();//必须调用,通过此方法来finish调预览的activity
    }
    ...
    ...
});`

预览的Activity需要做如下初始化工作,在其onCreate里必须调用:PeekAndPopHelper.notifyActivityPeekReady

protected void onCreate(Bundle savedInstanceState) {
    ...
    //需要进行初始化操作
    PeekAndPopHelper.PeekAndPopConfig config = new PeekAndPopHelper.PeekAndPopConfig();
    peekView = findViewById(R.id.root_view);
    config.mPeekView = new WeakReference<View>(peekView);
    config.mPeekRect = new Rect(0, 231 ,1080, 1500);
    PeekAndPopHelper.notifyActivityPeekReady(this, config);
    ...
}

完整代码请下载附件的demo

注意事项

1.如果应用的下一级界面通过Fragment来实现,请通过add Fragment的方式创建新的界面,而不要使用replace的方式加载新的fragment,并且请务必在新的Fragment所在的根布局中android:clickable="true",否则会导致触摸事件传入当前的Fragment中,从而影响重压的效果。
2.在调用PeekAndPop中的enablePeekAndPop()方法时,传入进来的source,应用不能在其他地方通过调用setOnTouchListener()给其设置OnTouchListener,否则会导致压力触控失效。
3.给PeekAndPopConfig中的mPeekView赋值时,指定的view不能是通过merge标签定义的,否则得到的view是null。
4.如果预览的界面是要播放视频,请一定要用TextureView,而不能用Surfaceview或者VideoView,并且不能直接把TextureView作为mPeekView参数传入,而是需要传入其父view。
5.如何修改预览弹出菜单的文字颜色?menu的xml文件里面对item使用这种方式设置即可:android:titleCondensed="#ff000000"。
6.通常点击预览后弹出的菜单项后,预览会主动退出,但是如果有应用因为特殊需求,要求在点击菜单项后要保持预览,可以在menu的xml文件里面对item指定android:titleCondensed="Default"。
7.为了避免压力触控跟长按起冲突,我们根据系统默认的认为是长按的判断时间为基准,当在这个时间内达到压力阈值后,会启动压力触控的逻辑,而不会再启动长按;当超过长按的时间后,哪怕此时压力值超过了阈值也不会主动启动压力触控。
8.可能有应用需要知道当前的魅族手机是否支持压力触控,我们提供了如下的接口供大家来判断:

public static final String FEATURE_FORCETOUCH = "android.hardware.touchscreen.forcetouch";
 
PackageManager pm = getPackageManager();
boolean hasForcetouchFeature = pm.hasSystemFeature(FEATURE_FORCETOUCH);


9.PeekAndPop库支持最小的Android SDK版本为17。

文件

PeekAndPop-0411.zip

PeekAndPopDemo.zip

导航菜单