1 /* 2 * Copyright (c) 2021 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16 package ohos.devtools.views.layout.chartview; 17 18 import com.intellij.ui.components.JBPanel; 19 import ohos.devtools.views.charts.model.ChartDataRange; 20 import ohos.devtools.views.charts.model.ChartStandard; 21 import ohos.devtools.views.common.ColorConstants; 22 23 import java.awt.Dimension; 24 import java.awt.event.MouseAdapter; 25 import java.awt.event.MouseEvent; 26 import java.math.BigDecimal; 27 28 import static ohos.devtools.views.common.LayoutConstants.INITIAL_VALUE; 29 import static ohos.devtools.views.layout.chartview.utils.OperationUtils.divide; 30 import static ohos.devtools.views.layout.chartview.utils.OperationUtils.multiply; 31 32 /** 33 * 自定义滚动条 34 * 35 * @since 2021/11/22 36 */ 37 public class ProfilerScrollbar extends JBPanel { 38 /** 39 * 滚动条的高度 40 */ 41 private static final int BAR_HEIGHT = 8; 42 43 /** 44 * 滚动条的最小宽度 45 */ 46 private static final int MIN_WIDTH = 20; 47 48 /** 49 * 滚动条的父面板 50 */ 51 private final ProfilerChartsView parent; 52 53 /** 54 * 滚动条可移动范围的宽度 55 */ 56 private int moveScopeWidth; 57 58 /** 59 * 拖动时的滚动条控件 60 */ 61 private JBPanel bar; 62 63 /** 64 * 拖动滚动条时,滚动条起点坐标和鼠标点之间的偏移量 65 */ 66 private int offsetPixel = INITIAL_VALUE; 67 68 /** 69 * 构造函数 70 * 71 * @param parent 父面板 72 */ ProfilerScrollbar(ProfilerChartsView parent)73 public ProfilerScrollbar(ProfilerChartsView parent) { 74 this.parent = parent; 75 initBar(); 76 addListener(); 77 } 78 79 /** 80 * 初始化ScrollBar 81 */ initBar()82 private void initBar() { 83 if (parent == null) { 84 return; 85 } 86 this.setVisible(true); 87 this.setLayout(null); 88 // 初始化用于拖动的bar 89 bar = new JBPanel(); 90 // 滚动条颜色 91 bar.setBackground(ColorConstants.SCROLLBAR); 92 // 第一次出现时表明Chart刚刚铺满界面,此时滚动条应和整个Chart一样长,后面随着时间推移,滚动条慢慢缩小 93 moveScopeWidth = parent.getWidth(); 94 bar.setPreferredSize(new Dimension(moveScopeWidth, BAR_HEIGHT)); 95 bar.setBounds(0, 0, bar.getPreferredSize().width, bar.getPreferredSize().height); 96 this.add(bar); 97 this.setPreferredSize(new Dimension(moveScopeWidth, BAR_HEIGHT)); 98 } 99 100 /** 101 * 添加监听器 102 */ addListener()103 private void addListener() { 104 this.addMouseMotionListener(new MouseAdapter() { 105 @Override 106 public void mouseDragged(MouseEvent mouseEvent) { 107 dragEvent(mouseEvent); 108 } 109 }); 110 this.addMouseListener(new MouseAdapter() { 111 @Override 112 public void mousePressed(MouseEvent mouseEvent) { 113 pressEvent(mouseEvent); 114 } 115 116 @Override 117 public void mouseReleased(MouseEvent mouseEvent) { 118 releaseEvent(); 119 } 120 }); 121 } 122 123 /** 124 * 自定义鼠标按下事件 125 * 126 * @param mouseEvent MouseEvent 127 */ pressEvent(MouseEvent mouseEvent)128 private void pressEvent(MouseEvent mouseEvent) { 129 // 按下时计算滚动条起点坐标和鼠标点之间的偏移量 130 offsetPixel = mouseEvent.getX() - bar.getX(); 131 } 132 133 /** 134 * 自定义鼠标释放事件 135 */ releaseEvent()136 private void releaseEvent() { 137 // 恢复偏移量为初始值 138 offsetPixel = INITIAL_VALUE; 139 } 140 141 /** 142 * 自定义拖拽事件 143 * 144 * @param mouseEvent MouseEvent 145 */ dragEvent(MouseEvent mouseEvent)146 private void dragEvent(MouseEvent mouseEvent) { 147 int newLocationX = getNewLocationX(mouseEvent); 148 int barWidth = bar.getWidth(); 149 if (moveScopeWidth == (barWidth + newLocationX) && !parent.isStop()) { 150 setRestartRefresh(); 151 return; 152 } 153 // 拖拽时停止刷新Chart 154 if (!parent.isStop()) { 155 setPauseRefresh(); 156 } 157 // 计算拖动后的应显示的时间区域 158 calcTimeRange(newLocationX); 159 // 更新滚动条位置 160 bar.setBounds(newLocationX, 0, bar.getWidth(), BAR_HEIGHT); 161 } 162 163 /** 164 * 设置重启状态 165 */ setRestartRefresh()166 private void setRestartRefresh() { 167 parent.getPublisher().restartRefresh(); 168 parent.setPause(false); 169 } 170 171 /** 172 * 设置暂停状态 173 */ setPauseRefresh()174 private void setPauseRefresh() { 175 parent.getPublisher().pauseRefresh(); 176 parent.setPause(true); 177 } 178 179 /** 180 * 通过拖动距离计算时间范围的变化 181 * 182 * @param newLocationX 滚动条新位置的X坐标 183 */ calcTimeRange(int newLocationX)184 private void calcTimeRange(int newLocationX) { 185 // 空白宽度:组件宽度 - 滚动条宽度 186 int emptyWidth = moveScopeWidth - bar.getWidth(); 187 if (emptyWidth == 0) { 188 return; 189 } 190 // 隐藏掉的时间:lastTime - displayTime 191 ChartStandard standard = parent.getPublisher().getStandard(); 192 int lastTime = (int) (standard.getLastTimestamp() - standard.getFirstTimestamp()); 193 int hideTime = lastTime - standard.getMaxDisplayMillis(); 194 // 计算未占用的地方,1px代表多少隐藏时间,得到一个比例 195 BigDecimal scale = divide(hideTime, emptyWidth); 196 // 拿到滚动条移动了多少px:新位置减去bar当前位置 197 int movePixel = newLocationX - bar.getX(); 198 // 计算出时间应该偏移多少 199 int timeOffset = multiply(scale, movePixel); 200 ChartDataRange oldRange = standard.getDisplayRange(); 201 int newStart = oldRange.getStartTime() + timeOffset; 202 // 如果newStart不能小于0 203 if (newStart < 0) { 204 newStart = 0; 205 } 206 // 如果拖至最左边和最右边,需要特殊处理 207 if (newLocationX == 0) { 208 newStart = 0; 209 } 210 if (newLocationX == moveScopeWidth - bar.getWidth()) { 211 newStart = lastTime - standard.getMaxDisplayMillis(); 212 } 213 int newEnd = newStart + standard.getMaxDisplayMillis(); 214 parent.getPublisher().notifyRefresh(newStart, newEnd); 215 } 216 217 /** 218 * 鼠标移动后,获取新的滚动条X坐标 219 * 220 * @param mouseEvent MouseEvent 221 * @return int 222 */ getNewLocationX(MouseEvent mouseEvent)223 private int getNewLocationX(MouseEvent mouseEvent) { 224 // 拖拽后滚动条的起点X坐标即为鼠标位置-偏移量 225 int newLocationX = mouseEvent.getX() - offsetPixel; 226 // 如果起始坐标从左侧超出了父Panel,需要限制一下newLocationX 227 newLocationX = Math.max(newLocationX, 0); 228 // 同理,结束坐标超出了父Panel的宽度,也要限制newLocationX,不能超出 229 int newLocationEndX = newLocationX + bar.getWidth(); 230 if (newLocationEndX >= parent.getWidth()) { 231 newLocationX = parent.getWidth() - bar.getWidth(); 232 } 233 return newLocationX; 234 } 235 236 /** 237 * 计算并调整滚动条调整后的宽度和位置 238 */ resizeAndReposition()239 public void resizeAndReposition() { 240 ChartStandard standard = parent.getPublisher().getStandard(); 241 int lastTime = (int) (standard.getLastTimestamp() - standard.getFirstTimestamp()); 242 // 最后的时间小于最大展示时间,则不需要滚动条 243 int maxDisplay = parent.getPublisher().getStandard().getMaxDisplayMillis(); 244 if (lastTime <= maxDisplay) { 245 return; 246 } 247 // 每次调整位置和长度时,需要更新moveScopeWidth,因为界面可能缩放 248 moveScopeWidth = parent.getWidth(); 249 // 用户可能把滚动条拖动值中间,然后触发当前方法, 250 // 所以要计算进度条前面的空白长度来确定滚动条位置 251 // 计算进度条前面的空白长度 252 int startTime = standard.getDisplayRange().getStartTime(); 253 BigDecimal leftEmptyRatio = divide(startTime, lastTime); 254 int leftEmptyWidth = multiply(leftEmptyRatio, moveScopeWidth); 255 // 计算进度条的比例:当前展示时间/本次任务最后的时间 256 BigDecimal barRatio = divide(maxDisplay, lastTime); 257 int barNewWidth = multiply(barRatio, moveScopeWidth); 258 // 保证宽度不能超过最小值,否则会滚动条会缩小到看不清 259 if (barNewWidth < MIN_WIDTH) { 260 barNewWidth = MIN_WIDTH; 261 // 保证宽度不能超过最小值后,滚动条的比例和空白的比例已经不一样了,需要重新计算, 262 // 比例和宽度均减去maxDisplay(barNewWidth) 263 leftEmptyRatio = divide(startTime, lastTime - maxDisplay); 264 leftEmptyWidth = multiply(leftEmptyRatio, moveScopeWidth - barNewWidth); 265 } 266 bar.setPreferredSize(new Dimension(barNewWidth, BAR_HEIGHT)); 267 // Chart再刷新时,滚动条一定在最右边,由于上面的运算会丢失精度, 268 // 会导致滚动条在闪烁,这里做个运算保证滚动条一直在右侧 269 if (parent.getPublisher().isRefreshing()) { 270 leftEmptyWidth = moveScopeWidth - barNewWidth; 271 } 272 // 更新进度条位置 273 bar.setBounds(leftEmptyWidth, 0, barNewWidth, BAR_HEIGHT); 274 this.repaint(); 275 this.revalidate(); 276 } 277 } 278