The operator '*' can't be unconditionally invoked because the receiver can be 'null'. Try adding a null check to the target ('!') - flutter

I used this code for responsiveness in my UI. So what this code basically does is calculate the size of the screen and I use the functions below to put the exact font size according to the design provided to me in Figma or Adobe XD. Using this method, I was able to create pixel-perfect UI.
After upgrading to Flutter 2.0.3, I am getting null safety errors. I was able to solve most of them but I am not able to solve this error.
Please advice.
Complete Code
import 'package:flutter/material.dart';
class SizeConfig {
static MediaQueryData? _mediaQueryData;
static double? screenWidth;
static double? screenHeight;
static double? defaultSize;
static Orientation? orientation;
void init(BuildContext context) {
_mediaQueryData = MediaQuery.of(context);
screenWidth = _mediaQueryData!.size.width;
screenHeight = _mediaQueryData!.size.height;
orientation = _mediaQueryData!.orientation;
if (orientation == Orientation.landscape) {
defaultSize = screenHeight! * 0.024;
} else {
defaultSize = screenWidth! * 0.024;
}
}
}
double getSize(double size) {
var defaultsSize = SizeConfig.defaultSize * size;
return (defaultsSize / 10);
}
// Get the proportionate height as per screen size
double getProportionateScreenHeight(double inputHeight) {
double screenHeight = SizeConfig.screenHeight!;
// 812 is the layout height that designer use
return (inputHeight / 812.0) * screenHeight;
}
// Get the proportionate width as per screen size
double getProportionateScreenWidth(double inputWidth) {
double screenWidth = SizeConfig.screenWidth!;
// 375 is the layout width that Figma provides
return (inputWidth / 375.0) * screenWidth;
}
Error

Because SizeConfig.defaultSize is nullable, you need to make sure that its value should not be null.
You can add some assertion to notify the caller that SizeConfig should be initialized first. Then, you can change it to SizeConfig.defaultSize!.
Sample...
double getSize(double size) {
assert(
SizeConfig.defaultSize != null,
"SizeConfig should be initialized (only once) before calling getSize(...). Refer to SizeConfig.init(...).",
);
var defaultsSize = SizeConfig.defaultSize! * size;
return (defaultsSize / 10);
}

Problem:
You get this error because the object you're invoking * on can be null.
Example:
int? count = 1;
void main() {
print(count * 2); // error: The operator '*' can't be unconditionally invoked ...
}
Solutions:
Use a local variable:
int? count = 1;
void main() {
var i = count;
if (i != null) {
print(i * 2); // Prints 2
}
}
Use bang operator (!)
int? count = 1;
void main() {
print(count! * 2); // Prints 2
}

You have three options:
Use the bang operator:
int? count = 1;
void main() {
// will throw an error if count is null
print(count! * 2);
}
Use the ?? operator:
int? count = 1;
void main() {
// safe
print((count ?? 1) * 2);
}
Use an if - else statement:
int? count = 1;
void main() {
if(count != null) {
print(count! * 2);
} else {
print('count is null');
}
}

Related

Real height in flutter?

I am trying to retrieve the real height in Flutter. I tried different options:
MediaQuery.of(context).size.height * MediaQuery.of(context).devicePixelRatio
or
WidgetsBinding.instance.window.physicalSize.height
I add these 2 lines in a new Flutter app (from scratch, just the new counter app screen that flutter creates)
I also tried to use a global key, and it does not work (meaning by that that I get the same result). I am testing it in a Samsung a10, which, according to wikipedia, has a 720 x 1520 pixels. The width I have no problem in calculating it, but the height, is always giving me 1424.0. Why I am not getting the full height? Is happening me with more phone models.
Please see the documentation for the physicalSize property:
This value does not take into account any on-screen keyboards or other system UI. The padding and viewInsets properties provide information about how much of each side of the view may be obscured by system UI.
try to use this utility class it gives me the right result
class ScreenUtil {
static ScreenUtil instance = new ScreenUtil();
int width;
int height;
bool allowFontScaling;
static MediaQueryData _mediaQueryData;
static double _screenWidth;
static double _screenHeight;
static double _screenHeightNoPadding;
static double _pixelRatio;
static double _statusBarHeight;
static double _bottomBarHeight;
static double _textScaleFactor;
static Orientation _orientation;
ScreenUtil({
this.width = 1080,
this.height = 1920,
this.allowFontScaling = false,
});
static ScreenUtil getInstance() {
return instance;
}
void init(BuildContext context) {
MediaQueryData mediaQuery = MediaQuery.of(context);
_mediaQueryData = mediaQuery;
_pixelRatio = mediaQuery.devicePixelRatio;
_screenWidth = mediaQuery.size.width;
_screenHeight = mediaQuery.size.height;
_statusBarHeight = mediaQuery.padding.top;
_bottomBarHeight = mediaQuery.padding.bottom;
_textScaleFactor = mediaQuery.textScaleFactor;
_orientation = mediaQuery.orientation;
_screenHeightNoPadding =
mediaQuery.size.height - _statusBarHeight - _bottomBarHeight;
}
static MediaQueryData get mediaQueryData => _mediaQueryData;
static double get textScaleFactory => _textScaleFactor;
static double get pixelRatio => _pixelRatio;
static Orientation get orientation => _orientation;
static double get screenWidth => _screenWidth;
static double get screenHeight => _screenHeight;
static double get screenWidthPx => _screenWidth * _pixelRatio;
static double get screenHeightPx => _screenHeight * _pixelRatio;
static double get screenHeightNoPadding => _screenHeightNoPadding;
static double get statusBarHeight => _statusBarHeight * _pixelRatio;
static double get bottomBarHeight => _bottomBarHeight * _pixelRatio;
get scaleWidth => _screenWidth / instance.width;
get scaleHeight => _screenHeight / instance.height;
setWidth(int width) => width * scaleWidth;
setHeight(int height) => height * scaleHeight;
setSp(int fontSize) => allowFontScaling
? setWidth(fontSize)
: setWidth(fontSize) / _textScaleFactor;
}
in your build method first call
ScreenUtil().init(context);
then you can call ScreenUtil.screenHeight

Flutter:flame, ComposedComponent cant be mixed onto PositionComponent

I am making a game in flutter, and i found this link in github https://github.com/g0rdan/Flutter.Bird and i tried to run it in my computer and i encounter this error error: 'ComposedComponent' can't be mixed onto 'PositionComponent' because 'PositionComponent' doesn't implement 'HasGameRef'. (mixin_application_not_implemented_interface at [myfirstgame] lib\game\bird.dart:16)
enum BirdStatus { waiting, flying}
enum BirdFlyingStatus { up, down, none }
class Bird extends PositionComponent with ComposedComponent { == from this line
int _counter = 0;
int _movingUpSteps = 15;
Size _screenSize;
double _heightDiff = 0.0;
double _stepDiff = 0.0;
BirdGround ground;
BirdStatus status = BirdStatus.waiting;
BirdFlyingStatus flyingStatus = BirdFlyingStatus.none;
Bird(Image spriteImage, Size screenSize)
{
_screenSize = screenSize;
List<Sprite> sprites = [
Sprite.fromImage(
spriteImage,
width: SpriteDimensions.birdWidth,
height: SpriteDimensions.birdHeight,
y: SpritesPostions.birdSprite1Y,
x: SpritesPostions.birdSprite1X,
),
Sprite.fromImage(
spriteImage,
width: SpriteDimensions.birdWidth,
height: SpriteDimensions.birdHeight,
y: SpritesPostions.birdSprite2Y,
x: SpritesPostions.birdSprite2X,
),
Sprite.fromImage(
spriteImage,
width: SpriteDimensions.birdWidth,
height: SpriteDimensions.birdHeight,
y: SpritesPostions.birdSprite3Y,
x: SpritesPostions.birdSprite3X,
)
];
var animatedBird = new Animation.spriteList(sprites, stepTime: 0.15);
this.ground = BirdGround(animatedBird);
this..add(ground);
}
void setPosition(double x, double y) {
this.ground.x = x;
this.ground.y = y;
}
void update(double t) {
if (status == BirdStatus.flying) {
_counter++;
if (_counter <= _movingUpSteps) {
flyingStatus = BirdFlyingStatus.up;
this.ground.showAnimation = true;
this.ground.angle -= 0.01;
this.ground.y -= t * 100 * getSpeedRatio(flyingStatus, _counter);
}
else {
flyingStatus = BirdFlyingStatus.down;
this.ground.showAnimation = false;
if (_heightDiff == 0)
_heightDiff = (_screenSize.height - this.ground.y);
if (_stepDiff == 0)
_stepDiff = this.ground.angle.abs() / (_heightDiff / 10);
this.ground.angle += _stepDiff;
this.ground.y += t * 100 * getSpeedRatio(flyingStatus, _counter);
}
this.ground.update(t);
}
}
double getSpeedRatio(BirdFlyingStatus flyingStatus, int counter){
if (flyingStatus == BirdFlyingStatus.up) {
var backwardCounter = _movingUpSteps - counter;
return backwardCounter / 10.0;
}
if (flyingStatus == BirdFlyingStatus.down) {
var diffCounter = counter - _movingUpSteps;
return diffCounter / 10.0;
}
return 0.0;
}
void jump() {
Flame.audio.play('wing.wav');
status = BirdStatus.flying;
_counter = 0;
this.ground.angle = 0;
}
}
class BirdGround extends AnimationComponent {
bool showAnimation = true;
BirdGround(Animation animation)
: super(ComponentDimensions.birdWidth, ComponentDimensions.birdHeight, animation);
#override
void update(double t){
if (showAnimation) {
super.update(t);
}
}
}
This uses a very old Flame version, so I would recommend not building anything on top of it.
But to your problem, it is missing the HasGameRef mixin on your component, so if you write something like this it should work:
class Bird extends PositionComponent with HasGameRef<YourGameClass>, ComposedComponent { ...

Flutter update is giving me this error: The method '*' was called on null

I have a flutter app using the flame library. I'm trying to make an object move in a flutter game. When I run the update function, I get the following error:
The method '*' was called on null.
Receiver: null
Tried calling: *(0.0)
Seems like something isn't initialized and the update function is ran before that something is initialized. When I comment out player.update(t) it works, but the update function doesn't get called. What am I doing wrong ad how can I fix it? Here's my code:
Game Controller Class
class GameController extends Game {
Size screenSize;
Player player;
GameController() {
initialize();
}
void initialize() async {
final initDimetion = await Flame.util.initialDimensions();
resize(initDimetion);
player = Player(this);
}
void render(Canvas c) {
Rect bgRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
Paint bgPaint = Paint()..color = Color(0xFFFAFAFA);
c.drawRect(bgRect, bgPaint);
player.render(c);
}
void update(double t) {
if (player is Player) { // Tried adding this if statement but it didn't work
player.update(t);
}
}
void resize(Size size) {
screenSize = size;
}
}
Player Class
class Player {
final GameController gameController;
Rect playerRect;
double speed;
Player(this.gameController) {
final size = 40.0;
playerRect = Rect.fromLTWH(gameController.screenSize.width / 2 - size / 2,
gameController.screenSize.height / 2 - size / 2, size, size);
}
void render(Canvas c) {
Paint color = Paint()..color = Color(0xFF0000FF);
c.drawRect(playerRect, color);
}
void update(double t) {
double stepDistance = speed * t;
Offset stepToSide = Offset.fromDirection(90, stepDistance);
playerRect = playerRect.shift(stepToSide);
}
}
You never initialize the speed attribute of Player to a value. So speed * t in Player.update causes this error.
Simply initialize the speed attribute in the constructor
Player(this.gameController) {
final size = 40.0;
this.speed = 0;
playerRect = Rect.fromLTWH(gameController.screenSize.width / 2 - size / 2,
gameController.screenSize.height / 2 - size / 2, size, size);
}

Cannot figure out how the bounds of a Region work

I'm currently writing a CAD-like program for logic circuits (it's my first "graphics intensive" program ever). When I place a component on the schematic, let say an AND gate (which is Region class at its root), I want to be able to interact with it (select it, change its properties, etc). So far, so good. I can click on it and everything go well. However, if I click outside of it, the mouse click event still show the component as it source(!).
Digging a bit further, I put some traces in the mouse click handler and found out that getBoundsInLocal() and getBoundsInParent() return bounds that are around 50% larger than it should be. The getLayoutBounds(), getWidth() and getHeight() do return the correct value.
The pane onto which the components are laid out is a simple Pane object, but it uses setScaleX() and setScaleY() to implement zooming capabilities. I did try to disable them, with no luck.
public abstract class SchematicComponent
extends Region {
private Shape graphicShape = null;
public Shape getGraphicShape() {
if( isShapeDirty() ) {
if( graphicShape != null ) {
getChildren().remove( graphicShape );
}
graphicShape = createShape();
markShapeDirty( false );
if( graphicShape != null ) {
getChildren().add( graphicShape );
}
}
return graphicShape;
}
abstract protected Shape createShape();
}
abstract public class CircuitComponent
extends SchematicComponent {
}
abstract public class LogicGate
extends CircuitComponent {
#Override
protected void layoutChildren() {
super.layoutChildren();
Pin outPin;
final double inputLength = getInputPinsMaxLength();
// Layout the component around its center.
// NOTE: I did try to set the center offset to 0 with no luck.
Point2D centerOffset = getCenterPointOffset().multiply( -1 );
Shape gateShape = getGraphicShape();
if( gateShape != null ) {
gateShape.setLayoutX( centerOffset.getX() + inputLength );
gateShape.setLayoutY( centerOffset.getY() );
}
/* Layout the output pins. */
outPin = getOutputPin();
if( outPin != null ) {
outPin.layout();
outPin.setLayoutX( centerOffset.getX() + getWidth() );
outPin.setLayoutY( centerOffset.getY() + getHeight() / 2 );
}
/* Compute the first input pin location and the gap between each
pins */
double pinGap = 2;
double y;
if( getInputPins().size() == 2 ) {
y = centerOffset.getY() + getHeight() / 2 - 2;
pinGap = 4;
}
else {
y = centerOffset.getY() + ( getHeight() / 2 ) - getInputPins().size() + 1;
}
/* Layout the input pins */
for( Pin inPin : getInputPins() ) {
inPin.layout();
inPin.layoutXProperty().set( centerOffset.getX() );
inPin.layoutYProperty().set( y );
y += pinGap;
}
}
}
// The actual object placed on the schematic
public class AndGate
extends LogicGate {
#Override
protected double computePrefWidth( double height ) {
// NOTE: computeMin/MaxWidth methods call this one
double width = getSymbolWidth() + getInputPinsMaxLength();
double length = 0;
width += length;
if( getOutputPin().getLength() > 0 ) {
width += getOutputPin().getLength();
}
return width; // Always 16
}
#Override
protected double computePrefHeight( double width ) {
// NOTE: computeMin/MaxHeight methods call this one
return getSymbolHeight() + getExtraHeight(); // Always 10
}
#Override
protected Shape createShape() {
Path shape;
final double extraHeight = getExtraHeight();
final double inputLength = getInputPinsMaxLength();
final double outputLength = getOutputPin().getLength();
/* Width and Height of the symbol itself (i,e, excluding the
input/output pins */
final double width = getWidth() - inputLength - outputLength;
final double height = getHeight() - extraHeight;
/* Starting point */
double startX = 0;
double startY = extraHeight / 2;
ArrayList<PathElement> elements = new ArrayList<>();
elements.add( new MoveTo( startX, startY ) );
elements.add( new HLineTo( startX + ( width / 2 ) ) );
elements.add( new ArcTo( ( width / 2 ), // X radius
height / 2, // Y radius
180, // Angle 180°
startX + ( width / 2 ), // X position
startY + height, // Y position
false, // large arc
true ) ); // sweep
elements.add( new HLineTo( startX ) );
if( extraHeight > 0 ) {
/* The height of the input pins is larger than the height of
the shape so we need to add extra bar on top and bottom of
the shape.
*/
elements.add( new MoveTo( startX, 0 ) );
elements.add( new VLineTo( extraHeight + height ) );
}
else {
elements.add( new VLineTo( startY ) );
}
shape = new Path( elements );
shape.setStroke( getPenColor() );
shape.setStrokeWidth( getPenSize() );
shape.setStrokeLineJoin( StrokeLineJoin.ROUND );
shape.setStrokeLineCap( StrokeLineCap.ROUND );
shape.setFillRule( FillRule.NON_ZERO );
shape.setFill( getFillColor() );
return shape;
}
} // End: LogiGate
// SchematicView is the ScrollPane container that handles the events
public class SchematicView
extends ScrollPane {
/* Mouse handler inner class */
private class MouseEventHandler
implements EventHandler<MouseEvent> {
#Override
public void handle( MouseEvent event ) {
if( event.getEventType() == MouseEvent.MOUSE_CLICKED ) {
processMouseClicked( event );
}
else { /* ... more stuff ... */ }
}
private void processMouseClicked( MouseEvent event ) {
Object node = event.getSource();
SchematicSheet sheet = getSheet();
Bounds local = ( (Node) node ).getLayoutBounds();
Bounds local1 = ( (Node) node ).getBoundsInLocal();
Bounds parent = ( (Node) node ).getBoundsInParent();
// At this point, here is what I get:
// node.getHeight() = 10 -> Good
// local.getHeight() = 10 -> Good
// local1.getHeight() = 15.6499996... -> Not expected!
// parent.getHeight() = 15.6500015... -> Not expected!
/*... More stuff ... */
}
}
So at this point, I'm running of clues of what is going on. Where do these getBoundsInXXX() values come from? They doesn't match with the parent's scale values either. The same goes with getWidth(): I get 24.825000... instead of 16.
Looking at this, I understand why clicking outside the component works as if I clicked on it. Its bounds are about 50% bigger than what it should be.
I googled the damn thing and search some doc for almost 2 days now and I'm still baffled. I think I understand that getBoundsInXXX() methods do their own computation but could it be off by that much? I don't thing so. My best guess is that it is something inside the createShape() method but I just can't figure what it is.
Anyone has a clue of what is going on?
Many thanks for your help.
P.S.: This is my first post here, so hopefully I did it right ;)
I think I finally found the problem :)
Basically, the Pin custom shape was drawn in the negative part of X axis (wrong calculations, my bad!). My best guess is that somehow, Java notices that I drew outside the standard bounds and then added the extra space used to the bounds, hence, adding 50% to width, which matches the length of the Pin. Drawing it in the positive region seem to have fixed the problem.
I'm not 100% sure if that is the right answer, but it make sense and it is now working has expected.

Start Android Wear Watchface from Activity

I am wondering if it's possible to start the Watchface Service from an activity?
I tried to start the service in the onCreate method of my activity but it does not show the Watchface:
Intent serviceIntent = new Intent(this, CustomWatchFaceService);
startService(serviceIntent);
update
Here is the code for the WatchfaceService
public class CustomWatchFaceService extends CanvasWatchFaceService {
private static final String TAG = "DigitalWatchFaceService";
private static final Typeface BOLD_TYPEFACE =
Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);
private static final Typeface NORMAL_TYPEFACE =
Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL);
private static final long NORMAL_UPDATE_RATE_MS = 500;
private static final long MUTE_UPDATE_RATE_MS = TimeUnit.MINUTES.toMillis(1);
#Override
public Engine onCreateEngine() {
return new Engine();
}
private class Engine extends CanvasWatchFaceService.Engine {
static final String COLON_STRING = ":";
static final int MUTE_ALPHA = 100;
static final int NORMAL_ALPHA = 255;
static final int MSG_UPDATE_TIME = 0;
long mInteractiveUpdateRateMs = NORMAL_UPDATE_RATE_MS;
final Handler mUpdateTimeHandler = new Handler() {
#Override
public void handleMessage(Message message) {
switch (message.what) {
case MSG_UPDATE_TIME:
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "updating time");
}
invalidate();
if (shouldTimerBeRunning()) {
long timeMs = System.currentTimeMillis();
long delayMs =
mInteractiveUpdateRateMs - (timeMs % mInteractiveUpdateRateMs);
mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
}
break;
}
}
};
Paint mBackgroundPaint;
Bitmap mBackgroundBitmap;
Bitmap wifiIconOn;
Bitmap wifiIconOff;
Paint mDatePaint;
Paint mNotificationPaint;
Paint mNotificationMax;
Paint mNotificationHigh;
Paint mHourPaint;
Paint mMinutePaint;
Paint mSecondPaint;
Paint mAmPmPaint;
Paint mColonPaint;
float mColonWidth;
boolean mMute;
Calendar mCalendar;
Date mDate;
SimpleDateFormat mDayOfWeekFormat;
java.text.DateFormat mDateFormat;
boolean mShouldDrawColons;
float mXOffset;
float mYOffset;
float mLineHeight;
int mInteractiveBackgroundColor =
R.color.interactive_bg;
int mInteractiveNotificationMax =
R.color.notification_max;
int mInteractiveNotificationHigh =
R.color.notification_high;
int mInteractiveNotificationColor =
R.color.notification;
int mInteractiveHourDigitsColor =
R.color.interactive_time;
int mInteractiveMinuteDigitsColor =
R.color.interactive_time;
int mInteractiveSecondDigitsColor =
R.color.interactive_time;
boolean mLowBitAmbient;
#Override
public void onCreate(SurfaceHolder holder) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onCreate");
}
super.onCreate(holder);
Locale locale = new Locale("de");
Locale.setDefault(locale);
Configuration config = getResources().getConfiguration();
getBaseContext().getResources().updateConfiguration(config,
getBaseContext().getResources().getDisplayMetrics());
setWatchFaceStyle(new WatchFaceStyle.Builder(CustomWatchFaceService.this)
.setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
.setShowSystemUiTime(false)
.build());
Resources resources = CustomWatchFaceService.this.getResources();
mYOffset = resources.getDimension(R.dimen.digital_y_offset);
mLineHeight = resources.getDimension(R.dimen.digital_line_height);
setInteractiveColors();
// Not sure why the text color needs to be set here again ... it should be set in setDefaultColors()!
mDatePaint.setColor(getColor(R.color.digital_date));
mNotificationPaint.setColor(getColor(R.color.notification));
mNotificationMax.setColor(getColor(R.color.notification_max));
mNotificationHigh.setColor(getColor(R.color.notification_high));
mHourPaint.setColor(getColor(R.color.interactive_time));
mMinutePaint.setColor(getColor(R.color.interactive_time));
mSecondPaint.setColor(getColor(R.color.interactive_time));
mColonPaint.setColor(getColor(R.color.interactive_time));
//Images should be loaded here so they can be called during the Draw Method
wifiIconOn = BitmapFactory.decodeResource(CustomWatchFaceService.this.getResources(), R.drawable.wifi_on_small);
wifiIconOff = BitmapFactory.decodeResource(CustomWatchFaceService.this.getResources(), R.drawable.wifi_off_small);
mBackgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.customcart_logo_240_alpha);
mCalendar = Calendar.getInstance();
mDate = new Date();
initFormats();
}
public void setInteractiveColors() {
mBackgroundPaint = new Paint();
mBackgroundPaint.setColor(getColor(mInteractiveBackgroundColor));
mNotificationPaint = createTextPaint(mInteractiveNotificationColor);
mNotificationMax = createTextPaint(mInteractiveNotificationMax);
mNotificationHigh = createTextPaint(mInteractiveNotificationHigh);
mDatePaint = createTextPaint(R.color.digital_date);
mHourPaint = createTextPaint(mInteractiveHourDigitsColor, BOLD_TYPEFACE);
mMinutePaint = createTextPaint(mInteractiveMinuteDigitsColor);
mSecondPaint = createTextPaint(mInteractiveSecondDigitsColor);
mColonPaint = createTextPaint(R.color.digital_colons);
}
#Override
public void onDestroy() {
mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
super.onDestroy();
}
private Paint createTextPaint(int defaultInteractiveColor) {
return createTextPaint(defaultInteractiveColor, NORMAL_TYPEFACE);
}
private Paint createTextPaint(int defaultInteractiveColor, Typeface typeface) {
Paint paint = new Paint();
paint.setColor(defaultInteractiveColor);
paint.setTypeface(typeface);
paint.setAntiAlias(true);
return paint;
}
#Override
public void onVisibilityChanged(boolean visible) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onVisibilityChanged: " + visible);
}
super.onVisibilityChanged(visible);
updateTimer();
}
private void initFormats() {
mDayOfWeekFormat = new SimpleDateFormat("EEEE", Locale.getDefault());
mDayOfWeekFormat.setCalendar(mCalendar);
mDateFormat = DateFormat.getDateFormat(CustomWatchFaceService.this);
mDateFormat.setCalendar(mCalendar);
}
#Override
public void onApplyWindowInsets(WindowInsets insets) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onApplyWindowInsets: " + (insets.isRound() ? "round" : "square"));
}
super.onApplyWindowInsets(insets);
// Load resources that have alternate values for round watches.
Resources resources = CustomWatchFaceService.this.getResources();
boolean isRound = insets.isRound();
mXOffset = resources.getDimension(isRound
? R.dimen.digital_x_offset_round : R.dimen.digital_x_offset);
float textSize = resources.getDimension(isRound
? R.dimen.digital_text_size_round : R.dimen.digital_text_size);
float notificationTextSize = resources.getDimension(isRound
? R.dimen.notification_text_size : R.dimen.notification_text_size);
mDatePaint.setTextSize(resources.getDimension(R.dimen.digital_date_text_size));
mHourPaint.setTextSize(textSize);
mMinutePaint.setTextSize(textSize);
mSecondPaint.setTextSize(textSize);
mColonPaint.setTextSize(textSize);
mNotificationPaint.setTextSize(notificationTextSize);
mColonWidth = mColonPaint.measureText(COLON_STRING);
}
#Override
public void onPropertiesChanged(Bundle properties) {
super.onPropertiesChanged(properties);
boolean burnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false);
mHourPaint.setTypeface(burnInProtection ? NORMAL_TYPEFACE : BOLD_TYPEFACE);
mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onPropertiesChanged: burn-in protection = " + burnInProtection
+ ", low-bit ambient = " + mLowBitAmbient);
}
}
#Override
public void onTimeTick() {
super.onTimeTick();
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode());
}
invalidate();
}
#Override
public void onAmbientModeChanged(boolean inAmbientMode) {
super.onAmbientModeChanged(inAmbientMode);
if (!isInAmbientMode()) {
mBackgroundPaint = new Paint();
mBackgroundPaint.setColor(getColor(R.color.interactive_bg));
mDatePaint.setColor(getColor(R.color.digital_date));
mHourPaint.setColor(getColor(R.color.interactive_time));
mMinutePaint.setColor(getColor(R.color.interactive_time));
mSecondPaint.setColor(getColor(R.color.interactive_time));
mColonPaint.setColor(getColor(R.color.interactive_time));
}
else {
mBackgroundPaint = new Paint();
mBackgroundPaint.setColor(getColor(R.color.ambient_bg));
mDatePaint.setColor(getColor(R.color.digital_date));
mHourPaint.setColor(getColor(R.color.ambient_time));
mMinutePaint.setColor(getColor(R.color.ambient_time));
mSecondPaint.setColor(getColor(R.color.ambient_time));
mColonPaint.setColor(getColor(R.color.ambient_time));
}
//Log.d("XXX", "onAmbientModeChanged: " + inAmbientMode);
if (mLowBitAmbient) {
boolean antiAlias = !inAmbientMode;
mDatePaint.setAntiAlias(antiAlias);
mHourPaint.setAntiAlias(antiAlias);
mMinutePaint.setAntiAlias(antiAlias);
mSecondPaint.setAntiAlias(antiAlias);
mAmPmPaint.setAntiAlias(antiAlias);
mColonPaint.setAntiAlias(antiAlias);
}
invalidate();
// Whether the timer should be running depends on whether we're in ambient mode (as well
// as whether we're visible), so we may need to start or stop the timer.
updateTimer();
}
#Override
public void onInterruptionFilterChanged(int interruptionFilter) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onInterruptionFilterChanged: " + interruptionFilter);
}
super.onInterruptionFilterChanged(interruptionFilter);
boolean inMuteMode = interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE;
// We only need to update once a minute in mute mode.
setInteractiveUpdateRateMs(inMuteMode ? MUTE_UPDATE_RATE_MS : NORMAL_UPDATE_RATE_MS);
if (mMute != inMuteMode) {
mMute = inMuteMode;
int alpha = inMuteMode ? MUTE_ALPHA : NORMAL_ALPHA;
mDatePaint.setAlpha(alpha);
mHourPaint.setAlpha(alpha);
mMinutePaint.setAlpha(alpha);
mColonPaint.setAlpha(alpha);
mAmPmPaint.setAlpha(alpha);
invalidate();
}
}
public void setInteractiveUpdateRateMs(long updateRateMs) {
if (updateRateMs == mInteractiveUpdateRateMs) {
return;
}
mInteractiveUpdateRateMs = updateRateMs;
// Stop and restart the timer so the new update rate takes effect immediately.
if (shouldTimerBeRunning()) {
updateTimer();
}
}
private String formatTwoDigitNumber(int hour) {
return String.format("%02d", hour);
}
#Override
public void onDraw(Canvas canvas, Rect bounds) {
long now = System.currentTimeMillis();
int width = bounds.width();
int height = bounds.height();
mCalendar.setTimeInMillis(now);
mDate.setTime(now);
boolean is24Hour = DateFormat.is24HourFormat(CustomWatchFaceService.this);
// Draw the background.
canvas.drawRect(0, 0, bounds.width(), bounds.height(), mBackgroundPaint);
//Draw the background Image
if (mBackgroundBitmap == null
|| mBackgroundBitmap.getWidth() != width
|| mBackgroundBitmap.getHeight() != height) {
mBackgroundBitmap = Bitmap.createScaledBitmap(mBackgroundBitmap,
width, height, true /* filter */);
}
//Log.d("XXX", "Width: "+ mBackgroundBitmap.getWidth() + "Height: "+mBackgroundBitmap.getHeight() );
if (isInAmbientMode() && (mLowBitAmbient)) {
canvas.drawColor(Color.BLACK);
} else if (isInAmbientMode()) {
canvas.drawColor(Color.BLACK);
} else {
canvas.drawBitmap(mBackgroundBitmap, 0, 0, mBackgroundPaint);
}
// Show colons for the first half of each second so the colons blink on when the time updates.
mShouldDrawColons = (System.currentTimeMillis() % 1000) < 500;
// Draw the hours.
float x = mXOffset;
String hourString;
if (is24Hour) {
hourString = formatTwoDigitNumber(mCalendar.get(Calendar.HOUR_OF_DAY));
} else {
int hour = mCalendar.get(Calendar.HOUR);
if (hour == 0) {
hour = 12;
}
hourString = String.valueOf(hour);
}
canvas.drawText(hourString, x, mYOffset, mHourPaint);
x += mHourPaint.measureText(hourString);
// In ambient and mute modes, always draw the first colon. Otherwise, draw the
// first colon for the first half of each second.
if (isInAmbientMode() || mMute || mShouldDrawColons) {
canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
}
x += mColonWidth;
// Draw the minutes.
String minuteString = formatTwoDigitNumber(mCalendar.get(Calendar.MINUTE));
canvas.drawText(minuteString, x, mYOffset, mMinutePaint);
x += mMinutePaint.measureText(minuteString);
// In unmuted interactive mode, draw a second blinking colon followed by the seconds.
// Otherwise, if we're in 12-hour mode, draw AM/PM
if (!isInAmbientMode() && !mMute) {
if (mShouldDrawColons) {
canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
}
x += mColonWidth;
canvas.drawText(formatTwoDigitNumber(
mCalendar.get(Calendar.SECOND)), x, mYOffset, mSecondPaint);
} else if (!is24Hour) {
x += mColonWidth;
}
// Only render the day of week and date if there is no peek card, so they do not bleed
// into each other in ambient mode.
if (getPeekCardPosition().isEmpty()) {
// Day of week
canvas.drawText(
mDayOfWeekFormat.format(mDate),
mXOffset, mYOffset + mLineHeight, mDatePaint);
// Date
canvas.drawText(
mDateFormat.format(mDate),
mXOffset, mYOffset + mLineHeight * 2, mDatePaint);
}
}
/**
* Starts the {#link #mUpdateTimeHandler} timer if it should be running and isn't currently
* or stops it if it shouldn't be running but currently is.
*/
private void updateTimer() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "updateTimer");
}
mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
if (shouldTimerBeRunning()) {
mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
}
}
/**
* Returns whether the {#link #mUpdateTimeHandler} timer should be running. The timer should
* only run when we're visible and in interactive mode.
*/
private boolean shouldTimerBeRunning() {
return isVisible() && !isInAmbientMode();
}
}
}