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

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

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
















 
 
 













 
 
 
 