1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.car.cluster.view; 17 18 import android.annotation.Nullable; 19 import android.car.cluster.navigation.NavigationState.ImageReference; 20 import android.car.cluster.navigation.NavigationState.Lane; 21 import android.car.cluster.navigation.NavigationState.Lane.LaneDirection; 22 import android.content.Context; 23 import android.graphics.Bitmap; 24 import android.graphics.Canvas; 25 import android.graphics.PorterDuff; 26 import android.graphics.PorterDuffColorFilter; 27 import android.graphics.drawable.Drawable; 28 import android.graphics.drawable.VectorDrawable; 29 import android.os.Handler; 30 import android.util.AttributeSet; 31 import android.util.Log; 32 import android.widget.ImageView; 33 import android.widget.LinearLayout; 34 35 import com.android.car.cluster.osdouble.R; 36 37 import java.util.ArrayList; 38 import java.util.List; 39 40 /** 41 * View component that displays the Lane preview information on the instrument cluster display 42 */ 43 public class LaneView extends LinearLayout { 44 private static final String TAG = "Cluster.LaneView"; 45 46 private Handler mHandler = new Handler(); 47 48 private ArrayList<Lane> mLanes; 49 50 private final int mWidth = (int) getResources().getDimension(R.dimen.lane_width); 51 private final int mHeight = (int) getResources().getDimension(R.dimen.lane_height); 52 private final int mOffset = (int) getResources().getDimension(R.dimen.lane_icon_offset); 53 54 private enum Shift { 55 LEFT, 56 RIGHT, 57 BOTH 58 } 59 LaneView(Context context)60 public LaneView(Context context) { 61 super(context); 62 } 63 LaneView(Context context, AttributeSet attrs)64 public LaneView(Context context, AttributeSet attrs) { 65 super(context, attrs); 66 } 67 LaneView(Context context, AttributeSet attrs, int defStyleAttr)68 public LaneView(Context context, AttributeSet attrs, int defStyleAttr) { 69 super(context, attrs, defStyleAttr); 70 } 71 setLanes(ImageReference imageReference, ImageResolver imageResolver)72 public void setLanes(ImageReference imageReference, ImageResolver imageResolver) { 73 imageResolver 74 .getBitmap(imageReference, 0, getHeight(), 0.5f) 75 .thenAccept(bitmap -> { 76 mHandler.post(() -> { 77 removeAllViews(); 78 ImageView imgView = new ImageView(getContext()); 79 imgView.setImageBitmap(bitmap); 80 imgView.setAdjustViewBounds(true); 81 addView(imgView); 82 }); 83 }) 84 .exceptionally(ex -> { 85 removeAllViews(); 86 if (Log.isLoggable(TAG, Log.DEBUG)) { 87 Log.d(TAG, "Unable to fetch image for lane: " + imageReference); 88 } 89 return null; 90 }); 91 } 92 setLanes(List<Lane> lanes, float alpha)93 public void setLanes(List<Lane> lanes, float alpha) { 94 mLanes = new ArrayList<>(lanes); 95 removeAllViews(); 96 97 // Use drawables for lane directional guidance 98 for (Lane lane : mLanes) { 99 Bitmap bitmap = combineBitmapFromLane(lane); 100 ImageView imgView = new ImageView(getContext()); 101 imgView.setImageBitmap(bitmap); 102 imgView.setAdjustViewBounds(true); 103 imgView.setImageAlpha((int) (alpha * 255)); 104 addView(imgView); 105 } 106 } 107 combineBitmapFromLane(Lane lane)108 private Bitmap combineBitmapFromLane(Lane lane) { 109 if (lane.getLaneDirectionsList().isEmpty()) { 110 return null; 111 } 112 113 Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888); 114 Canvas canvas = new Canvas(bitmap); 115 116 Shift shift = getShift(lane); 117 118 for (LaneDirection laneDir : lane.getLaneDirectionsList()) { 119 if (!laneDir.getIsHighlighted()) { 120 drawToCanvas(laneDir, canvas, false, shift); 121 } 122 } 123 124 for (LaneDirection laneDir : lane.getLaneDirectionsList()) { 125 if (laneDir.getIsHighlighted()) { 126 drawToCanvas(laneDir, canvas, true, shift); 127 } 128 } 129 130 return bitmap; 131 } 132 drawToCanvas(LaneDirection laneDir, Canvas canvas, boolean isHighlighted, Shift shift)133 private void drawToCanvas(LaneDirection laneDir, Canvas canvas, boolean isHighlighted, 134 Shift shift) { 135 int offset = getOffset(laneDir, shift); 136 VectorDrawable icon = (VectorDrawable) getLaneIcon(laneDir); 137 icon.setBounds(offset, 0, mWidth + offset, mHeight); 138 icon.setColorFilter(new PorterDuffColorFilter(isHighlighted 139 ? getContext().getColor(R.color.laneDirectionHighlighted) 140 : getContext().getColor(R.color.laneDirection), 141 PorterDuff.Mode.SRC_ATOP)); 142 icon.draw(canvas); 143 } 144 145 /** 146 * Determines the offset direction to line up overlapping lane directions. 147 */ getShift(Lane lane)148 private Shift getShift(Lane lane) { 149 boolean containsRight = false; 150 boolean containsLeft = false; 151 boolean containsStraight = false; 152 153 for (LaneDirection laneDir : lane.getLaneDirectionsList()) { 154 if (laneDir.getShape().equals(LaneDirection.Shape.NORMAL_RIGHT) 155 || laneDir.getShape().equals(LaneDirection.Shape.SLIGHT_RIGHT) 156 || laneDir.getShape().equals(LaneDirection.Shape.SHARP_RIGHT) 157 || laneDir.getShape().equals(LaneDirection.Shape.U_TURN_RIGHT)) { 158 containsRight = true; 159 } 160 if (laneDir.getShape().equals(LaneDirection.Shape.NORMAL_LEFT) 161 || laneDir.getShape().equals(LaneDirection.Shape.SLIGHT_LEFT) 162 || laneDir.getShape().equals(LaneDirection.Shape.SHARP_LEFT) 163 || laneDir.getShape().equals(LaneDirection.Shape.U_TURN_LEFT)) { 164 containsLeft = true; 165 } 166 if (laneDir.getShape().equals(LaneDirection.Shape.STRAIGHT)) { 167 containsStraight = true; 168 } 169 } 170 171 if (containsLeft && containsRight) { 172 //shift turns outwards 173 return Shift.BOTH; 174 } else if (containsStraight && containsRight) { 175 //shift straight lane dir to the left 176 return Shift.LEFT; 177 } else if (containsStraight && containsLeft) { 178 //shift straight lane dir to the right 179 return Shift.RIGHT; 180 } 181 182 return null; 183 } 184 185 /** 186 * Returns the offset value of the lane direction based on the given shift direction. 187 */ getOffset(LaneDirection laneDir, Shift shift)188 private int getOffset(LaneDirection laneDir, Shift shift) { 189 if (shift == Shift.BOTH) { 190 if (laneDir.getShape().equals(LaneDirection.Shape.NORMAL_LEFT) 191 || laneDir.getShape().equals(LaneDirection.Shape.SLIGHT_LEFT) 192 || laneDir.getShape().equals(LaneDirection.Shape.SHARP_LEFT) 193 || laneDir.getShape().equals(LaneDirection.Shape.U_TURN_LEFT)) { 194 return -mOffset; 195 } 196 if (laneDir.getShape().equals(LaneDirection.Shape.NORMAL_RIGHT) 197 || laneDir.getShape().equals(LaneDirection.Shape.SLIGHT_RIGHT) 198 || laneDir.getShape().equals(LaneDirection.Shape.SHARP_RIGHT) 199 || laneDir.getShape().equals(LaneDirection.Shape.U_TURN_RIGHT)) { 200 return mOffset; 201 } 202 } else if (shift == Shift.LEFT) { 203 if (laneDir.getShape().equals(LaneDirection.Shape.STRAIGHT)) { 204 return -mOffset; 205 } 206 } else if (shift == Shift.RIGHT) { 207 if (laneDir.getShape().equals(LaneDirection.Shape.STRAIGHT)) { 208 return mOffset; 209 } 210 } 211 212 return 0; 213 } 214 getLaneIcon(@ullable LaneDirection laneDir)215 private Drawable getLaneIcon(@Nullable LaneDirection laneDir) { 216 if (laneDir == null) { 217 return null; 218 } 219 switch (laneDir.getShape()) { 220 case UNKNOWN: 221 return null; 222 case STRAIGHT: 223 return mContext.getDrawable(R.drawable.direction_continue); 224 case SLIGHT_LEFT: 225 return mContext.getDrawable(R.drawable.direction_turn_slight_left); 226 case SLIGHT_RIGHT: 227 return mContext.getDrawable(R.drawable.direction_turn_slight_right); 228 case NORMAL_LEFT: 229 return mContext.getDrawable(R.drawable.direction_turn_left); 230 case NORMAL_RIGHT: 231 return mContext.getDrawable(R.drawable.direction_turn_right); 232 case SHARP_LEFT: 233 return mContext.getDrawable(R.drawable.direction_turn_sharp_left); 234 case SHARP_RIGHT: 235 return mContext.getDrawable(R.drawable.direction_turn_sharp_right); 236 case U_TURN_LEFT: 237 return mContext.getDrawable(R.drawable.direction_uturn_left); 238 case U_TURN_RIGHT: 239 return mContext.getDrawable(R.drawable.direction_uturn_right); 240 } 241 return null; 242 } 243 } 244