简介
HarmonyOS 开发自定义组件目前还不是很丰富,在开发过程中常常会有一些特殊效果的组件,这就需要我们额外花一些时间实现,这里给大家提供了一个BottomSheet上拉抽屉的组件,同时通过这个组件示例讲解一下HarmonyOS中的几个自定义控件用到的知识,分享一下自己自定义组件的思路。
效果演示
实现思路
1.布局设计
选择的是相对布局,蒙层区来改变内容区随着抽屉的位置调节透明度。
图1:
2.手势判断
先得出Component在屏幕的上下左右的坐标,然后手指的坐标是否在Component内。
-
- 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.抽屉偏移
- 这里采用的是整个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;
- }
- });
根据偏移量改变抽屉的位置;
- 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.事件冲突解决
首先发现不能按安卓的思想去处理:
- 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;
- }
- });
这里是抽屉容器定位抽屉时,判断是否打开ListContainer事件。
- private void setDrawerMarginBottom(int offY) {
- int bottom = marginBottom + offY;
- if (bottom > 0) {
- bottom = 0;
- listContainer.setEnabled(true);
- }
- .......
- }
5.背景亮暗变化
- 首先我们XML布局参照上述布局设计—图1;
- 背景亮暗的改变根据抽屉位置按比例设置蒙层的透明度;
- 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);
- }
总结
自定义组件步骤及思考方向:
明确父容器和子view的关系;
如何绘制一般采用以下三个方向:
- 已有控件组合;
- 采用画布绘制等;
- 继承控件扩展功能;
若涉及到触摸事件,需要考虑如何处理事件分发与消费;
动画选择,可根据需求选择合适动画(本文采用属性动画);
计算问题,复杂的需要丰富的数学知识;
性能问题(过度计算,重复绘制,对象重复创建)。