HarmonyOS 自定義組件之上拉抽屜
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
簡(jiǎn)介
HarmonyOS 開發(fā)自定義組件目前還不是很豐富,在開發(fā)過程中常常會(huì)有一些特殊效果的組件,這就需要我們額外花一些時(shí)間實(shí)現(xiàn),這里給大家提供了一個(gè)BottomSheet上拉抽屜的組件,同時(shí)通過這個(gè)組件示例講解一下HarmonyOS中的幾個(gè)自定義控件用到的知識(shí),分享一下自己自定義組件的思路。
效果演示

實(shí)現(xiàn)思路
1.布局設(shè)計(jì)
選擇的是相對(duì)布局,蒙層區(qū)來改變內(nèi)容區(qū)隨著抽屜的位置調(diào)節(jié)透明度。
圖1:

2.手勢(shì)判斷
先得出Component在屏幕的上下左右的坐標(biāo),然后手指的坐標(biāo)是否在Component內(nèi)。
- /**
- * (x,y)是否在view的區(qū)域內(nèi)
- *
- * @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.抽屜偏移
- 這里采用的是整個(gè)component對(duì)Touch事件的監(jiān)聽;
- 手指按下的判斷是否在抽屜上,然后記錄當(dāng)前觸摸y坐標(biāo);
- 移動(dòng)是算出偏移量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;
- }
- });
根據(jù)偏移量改變抽屜的位置;
- 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.事件沖突解決
首先發(fā)現(xiàn)不能按安卓的思想去處理:
- HarmonyOS中是沒有事件分發(fā)這概念的,只有事件消費(fèi),ListContainer先拿到事件,然后是抽屜布局;
- 根據(jù)抽屜在完全展開的位置,在ListContainer收到觸摸事件時(shí),把ListContainer事件靜止掉,不讓其消費(fèi);
- 待抽屜完全展開時(shí),解開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;
- }
- });
這里是抽屜容器定位抽屜時(shí),判斷是否打開ListContainer事件。
- private void setDrawerMarginBottom(int offY) {
- int bottom = marginBottom + offY;
- if (bottom > 0) {
- bottom = 0;
- listContainer.setEnabled(true);
- }
- .......
- }
5.背景亮暗變化
- 首先我們XML布局參照上述布局設(shè)計(jì)—圖1;
- 背景亮暗的改變根據(jù)抽屜位置按比例設(shè)置蒙層的透明度;
- float alpha = (0.5f - Math.abs((float) bottom / (float) H)) * 0.5f;
- bgComponent.setAlpha(alpha);
6.回彈效果
運(yùn)用到了數(shù)值動(dòng)畫,在手勢(shì)抬起時(shí),判斷上下臨界點(diǎn)決定動(dòng)畫的上下。
- private void onTouchUp() {
- HiLog.info(logLabel, "onTouchUp");
- createAnimator();
- }
- private void createAnimator() {
- marginBottom = directionalLayout.getMarginBottom();
- HiLog.info(logLabel, "createAnimator marginBottom:" + marginBottom);
- //創(chuàng)建數(shù)值動(dòng)畫對(duì)象
- AnimatorValue animatorValue = new AnimatorValue();
- //動(dòng)畫時(shí)長
- animatorValue.setDuration(300);
- //播放前的延遲時(shí)間
- animatorValue.setDelay(0);
- //循環(huán)次數(shù)
- animatorValue.setLoopedCount(0);
- //動(dòng)畫的播放類型
- animatorValue.setCurveType(Animator.CurveType.ACCELERATE_DECELERATE);
- //設(shè)置動(dòng)畫過程
- 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));
- }
- }
- });
- //開始啟動(dòng)動(dòng)畫
- 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);
- }
總結(jié)
自定義組件步驟及思考方向:
明確父容器和子view的關(guān)系;
如何繪制一般采用以下三個(gè)方向:
- 已有控件組合;
- 采用畫布繪制等;
- 繼承控件擴(kuò)展功能;
若涉及到觸摸事件,需要考慮如何處理事件分發(fā)與消費(fèi);
動(dòng)畫選擇,可根據(jù)需求選擇合適動(dòng)畫(本文采用屬性動(dòng)畫);
計(jì)算問題,復(fù)雜的需要豐富的數(shù)學(xué)知識(shí);
性能問題(過度計(jì)算,重復(fù)繪制,對(duì)象重復(fù)創(chuàng)建)。
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)