深圳幻海软件技术有限公司 欢迎您!

HarmonyOS 自定义组件之上拉抽屉

2023-02-28

想了解更多内容,请访问:51CTO和华为官方合作共建的鸿蒙技术社区https://harmonyos.51cto.com简介HarmonyOS开发自定义组件目前还不是很丰富,在开发过程中常常会有一些特殊效果的组件,这就需要我们额外花一些时间实现,这里给大家提供了一个BottomSheet上拉抽屉的组

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

简介

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

效果演示


实现思路

1.布局设计

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

图1:

 

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; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

3.抽屉偏移

  • 这里采用的是整个component对Touch事件的监听;
  • 手指按下的判断是否在抽屉上,然后记录当前触摸y坐标;
  • 移动是算出偏移量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
    } 
}); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.

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

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); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

4.事件冲突解决

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

  • HarmonyOS中是没有事件分发这概念的,只有事件消费,ListContainer先拿到事件,然后是抽屉布局;
  • 根据抽屉在完全展开的位置,在ListContainer收到触摸事件时,把ListContainer事件静止掉,不让其消费;
  • 待抽屉完全展开时,解开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
    } 
}); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

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

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

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

5.背景亮暗变化

  • 首先我们XML布局参照上述布局设计—图1;
  • 背景亮暗的改变根据抽屉位置按比例设置蒙层的透明度;
float alpha = (0.5f - Math.abs((float) bottom / (float) H)) * 0.5f; 
bgComponent.setAlpha(alpha); 
  • 1.
  • 2.

6.回弹效果

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

private void onTouchUp() { 
    HiLog.info(logLabel, "onTouchUp"); 
    createAnimator(); 

  • 1.
  • 2.
  • 3.
  • 4.
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(); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
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.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

总结

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

明确父容器和子view的关系;

如何绘制一般采用以下三个方向:

  1. 已有控件组合;
  2. 采用画布绘制等;
  3. 继承控件扩展功能;

若涉及到触摸事件,需要考虑如何处理事件分发与消费;

动画选择,可根据需求选择合适动画(本文采用属性动画);

计算问题,复杂的需要丰富的数学知识;

性能问题(过度计算,重复绘制,对象重复创建)。

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com