Better

Ethan的博客,欢迎访问交流

Ionic ActionSheet模拟实现

对于这个模拟,本人内心其实是拒绝的,但是没办法,公司的UI不安常理出牌,经常整出一些神级交互,同时拥有着我不理解的审美,最关键的问题是,即使在千钧一发之际,也会和我纠结几个像素的大小和颜色点的区别,也没有半点商量的余地,对于此,我内心没有一点波澜(才怪),甚至想吃一顿肯德基。吐槽归吐槽,那能怎么办,问题总得解决。

技术点

  • 功能抽象
  • 元素定位
  • 动态交互
  • 事件回调
  • 细节处理
    • 注册物理返回键
    • 路由改变时消失
    • 基本按钮:取消

功能抽象

抽象成一个指令的话,在项目中用的经常用到,但用过ActionSheet的都知道,他是用过一个服务的方式调用的,而不是在HTML使用指令,那这是怎么做到的呢?观察源码才发现,我们可以搭配指令和$compile服务的方式来使用。核心代码如下:

// $rootScope.$new()的作用就是为$rootScope创建一个新的scope。
var scope = $rootScope.$new(true);
// 通过$compile的方式得到元素
var element = scope.element = $compile('<action-sheet-up ng-class="cssClass" buttons="buttons"></action-sheet-up>')(scope);
// 达到显示的目的
$ionicBody.append(element);
// 达到消失的目的
element.remove();

元素定位

首先我们需要一个大背景置为灰色,给人一个幕布的感觉,使用户的焦点放在你要显示的菜单上,同时菜单需要定位在底部。具体结构如下:

<div class="ActionSheet-dt my-action-sheet-backdrop">
    <div class="my-action-sheet-wrapper">
    </div>
</div>

具体CSS如下:

.ActionSheet-dt.my-action-sheet-backdrop{
  transition: background-color 150ms ease-in-out;
  position: fixed;
  top: 0;
  left: 0;
  right:0;
  bottom: 0;
  z-index: 11;
  background-color: transparent;
}
.ActionSheet-dt.my-action-sheet-backdrop.active{
  background-color: rgba(0, 0, 0, 0.4);
}

.ActionSheet-dt .my-action-sheet-wrapper {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  width: 100%;
  max-width: 500px;
  margin: auto;
  background: #F2F5F8;
  transform: translate3d(0, 100%, 0);
  transition: all cubic-bezier(0.36, 0.66, 0.04, 1) 500ms;
}
/*还有这种操作,这个my-action-sheet-up样式必须在my-action-sheet-wrapper下,否则样式被覆盖,不生效*/
.ActionSheet-dt .my-action-sheet-up{
  transform: translate3d(0, 0, 0);
}

动态交互

如果背景和菜单瞬间出来的话,就显得太突兀,可不可以背景逐渐加深,而菜单从底部慢慢出来呢,消失相反!答案是可以的,ionic源码中为我们提供了两种方式添加动态效果,分别是$animate服务和css3的动画属性。下面分别讲解。

$animate服务,代码如下:

$animate.addClass(element, 'active').then(function () {

})

其实实现渐变效果的还是CSS3动画起的效果:

/*在150ms渐变*/
.ActionSheet-dt.my-action-sheet-backdrop{
  transition: background-color 150ms ease-in-out;
}

CSS3动画主要通过transition和transform,主要掌握translate3d的妙用。具体如下:

/*默认不可见*/
.ActionSheet-dt .my-action-sheet-wrapper {
    transform: translate3d(0, 100%, 0);
      transition: all cubic-bezier(0.36, 0.66, 0.04, 1) 500ms;
}
/*还有这种操作,这个my-action-sheet-up样式必须在my-action-sheet-wrapper下,否则样式被覆盖,不生效*/
.ActionSheet-dt .my-action-sheet-up{
  transform: translate3d(0, 0, 0);
}

事件回调

ionic ActionSheet的事件回调处理比较简单,直接返回被点击按钮的下标。需要开发人员根据下标判断哪个按钮被点击,从而采取处理。

细节处理

注册物理返回键

在上拉菜单出现时,在安卓端,用户点击物理返回键采取的措施应该是上拉菜单收起,而不是返回上一页,因此需要注册物理按键的事件回调,具体代码如下:

// 物理返回按钮注册函数返回值用来取消注册,如果上拉菜单出现,点击返回则关闭上拉菜单,而不是返回页面
scope.$deregisterBackButton = $ionicPlatform.registerBackButtonAction(
  function() {
    $timeout(scope.cancel);
  },
  IONIC_BACK_PRIORITY.actionSheet
);

需要注意的是,在上拉菜单收起时,需要取消注册

路由改变时消失

在路由发生改变时,应该收起菜单,同样的道理,监听路由改变事件即可。

var stateChangeListenDone = scope.cancelOnStateChange ? $rootScope.$on('$stateChangeSuccess', function() { scope.cancel(); }) : noop;

需要注意的是,在上拉菜单收起时,需要取消监听

具体使用

使用是传递配置对象,如果要使用代码收起菜单,直接调用显示菜单的返回值方法即可。

var actionSheetUp = ActionSheetUp.show({
  buttons: [
    {
      text: '确定',
      styleObject: {
        color:'#1fb5ff'
      }
    }
  ],
  titleText: '确定要更新此次签到记录吗?',
  cancelText: '取消',
  cancel: function () {
    // 取消操作
    console.log('close');
  },
  buttonClicked: function (index) {
    console.log(index);
    return true;
  }
});
// 消失
actionSheetUp()

完整代码

js部分

/**
 * Created by Administrator on 2017/9/12.
 */
angular.module('starter.directive')
  .directive('actionSheetUp', ['$document', function ($document) {
    return {
      restrict: 'E',
      replace: true,
      scope: true,
      // 不能使用templateUrl,否则查找不到元素
      template: '<div class="ActionSheet-dt my-action-sheet-backdrop">' +
      '<div class="my-action-sheet-wrapper">' +
      '<div class="ActionSheet-title" ng-if="titleText" ng-bind-html="titleText"></div>' +
      '<div class="ActionSheet-items">' +
      '<div class="ActionSheet-item" ng-click="buttonClicked($index)" ng-class="b.className" ng-style="b.styleObject" ng-repeat="b in buttons" ng-bind-html="b.text"></div>' +
      '<div class="ActionSheet-item" ng-if="cancelText" ng-click="cancel()" ng-bind-html="cancelText"></div>' +
      '</div></div></div>',
      link: function ($scope, $element) {
        // 点击ESC键消失
        var keyUp = function (e) {
          if (e.which == 27) {
            $scope.cancel();
            $scope.$apply();
          }
        };

        // 点击阴影消失
        var backdropClick = function (e) {
          if (e.target == $element[0]) {
            $scope.cancel();
            $scope.$apply();
          }
        };
        // 销毁与事件解绑
        $scope.$on('$destroy', function () {
          $element.remove();
          $document.unbind('keyup', keyUp);
        });

        $document.bind('keyup', keyUp);
        $element.bind('click', backdropClick);
      }
    }
  }]);


angular.module('starter.services')
  .factory('ActionSheetUp', ['$rootScope', '$ionicBody', '$compile', '$animate', '$timeout','IONIC_BACK_PRIORITY','$ionicPlatform',
    function ($rootScope, $ionicBody, $compile, $animate, $timeout,IONIC_BACK_PRIORITY,$ionicPlatform) {

      var extend = angular.extend;
      var noop = angular.noop;

      /*
      * opts:
      * titleText标题文本
      * cancelText取消按钮文本
      * cancel点击取消按钮回调事件
      * buttons:数组,对象主要属性text,className,style
      * buttonClicked点击按钮回调事件,参数为数组下标
      * */
      function actionSheetUp(opts) {
        // $rootScope.$new()的作用就是为$rootScope创建一个新的scope。
        var scope = $rootScope.$new(true);

        extend(scope, {
          cancel: noop,
          buttonClicked: noop,
          $deregisterBackButton: noop,
          buttons: [],
          cancelOnStateChange: true
        }, opts || {});

        // 使支持传入cssClass来自定义样式
        var element = scope.element = $compile('<action-sheet-up ng-class="cssClass" buttons="buttons"></action-sheet-up>')(scope);
        // 得到内容元素,用来实现动画效果
        var sheetEl = angular.element(element[0].querySelector('.my-action-sheet-wrapper'));

        var stateChangeListenDone = scope.cancelOnStateChange ? $rootScope.$on('$stateChangeSuccess', function() { scope.cancel(); }) : noop;

        // action-sheet-open 干嘛的呢

        scope.showSheet = function (done) {
          // 防止出现多个
          if (scope.removed) return;

          $ionicBody.append(element);

          // 背景渐变出现
          $animate.addClass(element, 'active').then(function () {
            if (scope.removed) return;
            (done || noop)();
          })

          // 内容区域慢慢出现
          $timeout(function () {
            if (scope.removed) return;
            sheetEl.addClass('my-action-sheet-up');
          }, 20, false);

        }

        scope.removeSheet = function (done) {
          if (scope.removed) return;
          scope.removed = true;
          // 慢慢消失
          sheetEl.removeClass('my-action-sheet-up');

          scope.$deregisterBackButton();//取消监听物理返回
          stateChangeListenDone();//取消监听

          $animate.removeClass(element, 'active').then(function() {
            scope.$destroy();
            element.remove();
            sheetEl = null;
            (done || noop)(opts &&opts.buttons);
          });
        }

        // 物理返回按钮注册函数返回值用来取消注册,如果上拉菜单出现,点击返回则关闭上拉菜单,而不是返回页面
        scope.$deregisterBackButton = $ionicPlatform.registerBackButtonAction(
          function() {
            $timeout(scope.cancel);
          },
          IONIC_BACK_PRIORITY.actionSheet
        );

        scope.cancel = function () {
          scope.removeSheet(opts && opts.cancel);
        }

        scope.buttonClicked = function(index) {
          // 如果方法返回true,意味着我们在执行完操作后可以直接关闭
          if (opts.buttonClicked(index, opts.buttons[index]) === true) {
            scope.removeSheet();
          }
        };

        // 默认打开功能
        scope.showSheet();

        // 满足调用返回值即可关闭的需求
        return scope.cancel;
      }

      return {
        show: actionSheetUp
      }
    }])
;

CSS部分

.ActionSheet-dt.my-action-sheet-backdrop{
  transition: background-color 150ms ease-in-out;
  position: fixed;
  top: 0;
  left: 0;
  right:0;
  bottom: 0;
  z-index: 11;
  background-color: transparent;
}
.ActionSheet-dt.my-action-sheet-backdrop.active{
  background-color: rgba(0, 0, 0, 0.4);
}

.ActionSheet-dt .my-action-sheet-wrapper {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  width: 100%;
  max-width: 500px;
  margin: auto;
  background: #F2F5F8;
  transform: translate3d(0, 100%, 0);
  transition: all cubic-bezier(0.36, 0.66, 0.04, 1) 500ms;
}
/*还有这种操作,这个my-action-sheet-up样式必须在my-action-sheet-wrapper下,否则样式被覆盖,不生效*/
.ActionSheet-dt .my-action-sheet-up{
  transform: translate3d(0, 0, 0);
}

.ActionSheet-dt .ActionSheet-title{
  padding:8px;
  color: #555;
  text-align: center;
}

.ActionSheet-dt .ActionSheet-items .ActionSheet-item{
  padding:14px;
  color: #222;
  margin-bottom: 10px;
  text-align: center;
  background: #fff;
}

.ActionSheet-dt .ActionSheet-items .ActionSheet-item:last-child{
   margin-bottom: 0;
}


留言