#星光计划2.0# HarmonyOS 自定义组件之上拉抽屉 原创 精华

中软国际AIoT开发者社区
发布于 2021-12-20 16:24
浏览
6收藏

作者:彭为杰
【本文正在参与51CTO HarmonyOS技术社区创作者激励计划-星光计划2.0】

简介

HarmonyOS 开发自定义组件目前还不是很丰富,在开发过程中常常会有一些特殊效果的组件,这就需要我们额外花一些时间实现,这里给大家提供了一个BottomSheet上拉抽屉的组件,同时通过这个组件示例讲解一下HarmonyOS中的几个自定义控件用到的知识,分享一下自己自定义组件的思路。

效果演示

#星光计划2.0# HarmonyOS 自定义组件之上拉抽屉-鸿蒙开发者社区

实现思路

1.布局设计

选择的是相对布局,蒙层区来改变内容区随着抽屉的位置调节透明度。

图1:#星光计划2.0# HarmonyOS 自定义组件之上拉抽屉-鸿蒙开发者社区#星光计划2.0# HarmonyOS 自定义组件之上拉抽屉-鸿蒙开发者社区

2.手势判断

先得出Component在屏幕的上下左右的坐标,然后手指的坐标是否在Component内。

/**
 * (x,y)是否在view的区域内
 *
 * @param component
 * @param x
 * @param y
 * @return
 */
private boolean isTouchPointInComponent(Component component, float x, float y) {
    int[] locationOnScreen = component.getLocationOnScreen();
    int left = locationOnScreen[0];
    int top = locationOnScreen[1];
    int right = left + component.getEstimatedWidth();
    int bottom = top + component.getEstimatedHeight();
    boolean inY = y >= top && y <= bottom;
    boolean inX = x >= left && x <= right;
    return inY && inX;
}

3.抽屉偏移

1.这里采用的是整个component对Touch事件的监听;

2.手指按下的判断是否在抽屉上,然后记录当前触摸y坐标;

3.移动是算出偏移量offY;

setTouchEventListener(new TouchEventListener() {
    @Override
    public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
        HiLog.info(logLabel, "onTouchEvent action:" + touchEvent.getAction());
        switch (touchEvent.getAction()) {
            case TouchEvent.PRIMARY_POINT_DOWN:
                marginBottom = directionalLayout.getMarginBottom();
                MmiPoint position = touchEvent.getPointerScreenPosition(0);
                if (isTouchPointInComponent(directionalLayout, position.getX(), position.getY())) {
                    dragStartPointY = touchEvent.getPointerPosition(0).getY();
                    return true;
                }
                break;
            case TouchEvent.PRIMARY_POINT_UP:
                onTouchUp();
                break;
            case TouchEvent.POINT_MOVE:
                float y = touchEvent.getPointerPosition(0).getY();
                float offY = dragStartPointY - y;
                setDrawerMarginBottom((int) offY);
                break;
        }
        return false;
    }
});

根据偏移量改变抽屉的位置;

private void setDrawerMarginBottom(int offY) {
    int bottom = marginBottom + offY;
    if (bottom > 0) {
        bottom = 0;
        listContainer.setEnabled(true);
    }

    if (bottom < -H / 2) {
        bottom = -H / 2;
    }
    HiLog.info(logLabel, "setDrawerMarginBottom bottom:" + bottom);

    float alpha = (0.5f - Math.abs((float) bottom / (float) H)) * 0.5f;
    HiLog.info(logLabel, "setDrawerMarginBottom alpha:" + alpha);
    bgComponent.setAlpha(alpha);
    directionalLayout.setMarginBottom(bottom);
}

4.事件冲突解决

首先发现不能按安卓的思想去处理:

​ 1.HarmonyOS中是没有事件分发这概念的,只有事件消费,ListContainer先拿到事件,然后是抽屉布局;

​ 2.根据抽屉在完全展开的位置,在ListContainer收到触摸事件时,把ListContainer事件静止掉,不让其消费;

​ 3.待抽屉完全展开时,解开ListContainer的事件;

listContainer.setTouchEventListener(new TouchEventListener() {
    @Override
    public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
        marginBottom = directionalLayout.getMarginBottom();
        boolean drag_down = listContainer.canScroll(DRAG_DOWN);
        boolean drag_UP = listContainer.canScroll(DRAG_UP);
        if (marginBottom == 0 && drag_down) {
            component.setEnabled(true);
            return true;
        }
        component.setEnabled(false);
        return false;
    }
});

这里是抽屉容器定位抽屉时,判断是否打开ListContainer事件。

private void setDrawerMarginBottom(int offY) {
    int bottom = marginBottom + offY;
    if (bottom > 0) {
        bottom = 0;
        listContainer.setEnabled(true);
    }
    .......
}

5.背景亮暗变化

1.首先我们XML布局参照上述布局设计—图1;

2.背景亮暗的改变根据抽屉位置按比例设置蒙层的透明度;

float alpha = (0.5f - Math.abs((float) bottom / (float) H)) * 0.5f;
bgComponent.setAlpha(alpha);

6.回弹效果

运用到了数值动画,在手势抬起时,判断上下临界点决定动画的上下。

private void onTouchUp() {
    HiLog.info(logLabel, "onTouchUp");
    createAnimator();
}
private void createAnimator() {
    marginBottom = directionalLayout.getMarginBottom();
    HiLog.info(logLabel, "createAnimator marginBottom:" + marginBottom);
    //创建数值动画对象
    AnimatorValue animatorValue = new AnimatorValue();
    //动画时长
    animatorValue.setDuration(300);
    //播放前的延迟时间
    animatorValue.setDelay(0);
    //循环次数
    animatorValue.setLoopedCount(0);
    //动画的播放类型
    animatorValue.setCurveType(Animator.CurveType.ACCELERATE_DECELERATE);
    //设置动画过程
    animatorValue.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
        @Override
        public void onUpdate(AnimatorValue animatorValue, float value) {
            HiLog.info(logLabel, "createAnimator value:" + value);
            if (marginBottom > -H / 4) { // top
                HiLog.info(logLabel, "createAnimator top:" + value);
                setDrawerBottomOrToP((int) (marginBottom - value * marginBottom));
            } else { // bottom
                HiLog.info(logLabel, "createAnimator bottom:" + value);
                int top = H / 2 + marginBottom;
                setDrawerBottomOrToP((int) (marginBottom - value *top));
            }
        }
    });
    //开始启动动画
    animatorValue.start();
}
private void setDrawerBottomOrToP(int bottom) {
    if (bottom > 0) {
        bottom = 0;
        listContainer.setEnabled(true);
    }

    if (bottom < -H / 2) {
        bottom = -H / 2;
    }
 
    float alpha = (0.5f - Math.abs((float) bottom / (float) H)) * 0.5f;

    bgComponent.setAlpha(alpha);
    directionalLayout.setMarginBottom(bottom);
}

总结

自定义组件步骤及思考方向:

  1. 明确父容器和子view的关系;
  2. 如何绘制一般采用以下三个方向:
    1. 已有控件组合;
    2. 采用画布绘制等;
    3. 继承控件扩展功能;
  3. 若涉及到触摸事件,需要考虑如何处理事件分发与消费;
  4. 动画选择,可根据需求选择合适动画(本文采用属性动画);
  5. 计算问题,复杂的需要丰富的数学知识;
  6. 性能问题(过度计算,重复绘制,对象重复创建);

代码地址

TouchEventDemo: 鸿蒙版java 上拉抽屉demo (gitee.com)

更多原创内容请关注:开鸿 HarmonyOS 学院

入门到精通、技巧到案例,系统化分享HarmonyOS开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2021-12-20 16:31:27修改
8
收藏 6
回复
举报
2条回复
按时间正序
/
按时间倒序
红叶亦知秋
红叶亦知秋

学到了,感谢分享

1
回复
2021-12-21 10:14:11
深开鸿
深开鸿

非常实用的例子

回复
2021-12-22 09:33:28
回复
    相关推荐