Delphi XE5开发Android程序使用自定义字体文件.

万事大吉,只欠根据字体文件(.ttf文件)切换阅读字体,通常Android系统只带三种以下字体.一般用Java/Eclipse开发的话比较简单,typeface的createFromAsset,createFromFile之类的很容易使用. 但是由于FireMonkey是跨平台的类库,必然不能和平台帮得太紧,所以提供了抽象的封装. 但是也许Delphi XE5是Android平台的第一个版本,有些地方难免有疏漏,FireMonkey的封装没有提供更换字体的功能.

但是我要实现的电子书阅读器换字体几乎是必须要实现的功能,所以只能给FireMonkey动动小手术了.

FireMonkey的字体加载是由抽象类TFontGlyphManager来实现的,在各个具体平台又有不同的实现,TWinFontGlyphManager,TIOSFontGlyphManager,TMacFontGlyphManager,TAndroidFontGlyphManager.

我们这里只针对Android不能加载字体文件换字体进行手术.

把TAndroidFontGlyphManager的实现单元FMX.FontGlyphs.Android拷贝到我们自己要使用更换字的的工程的目录中.修改TAndroidFontGlyphManager.LoadResource方法,当应用某字体的时候先判断我们指定的目录中是否存在同名的.ttf文件.有的话优先使用我们的字体文件.

做了两处改动.一处是uses添加了System.IOUtils单元.一处是TAndroidFontGlyphManager.LoadResource. 在这里做这样的小手术好处是我们的程序不收任何影响.例如: Text1.Font.Family:=’微软雅黑’; Text2.Font.Family:=’楷体’; 那么只要在我们指定的目录中存在”楷体.ttf”和”微软雅黑.ttf”那么这两个控件的字体就会分别应用对应的字体文件.

希望XE6版本中Android能投提供一种让我们动态加载字体的办法.不过也许我这个不是一个大众需求,毕竟大多数Android软件不需要太多的字体文件,在系统两三款字体下也活得好好的.

下面贴出来我修改过的文件.

{ ******************************************************* }

{ }

{ Delphi FireMonkey Platform }

{ Copyright(c) 2013 Embarcadero Technologies, Inc. }

{ }

{ ******************************************************* }

 

unit FMX.FontGlyphs.Android;

 

interface

 

uses

  System.Types, System.Classes, System.SysUtils, System.UITypes,

  System.UIConsts, System.Generics.Collections,

  FMX.Types, FMX.Surfaces, FMX.FontGlyphs, FMX.PixelFormats,

  Androidapi.JNI.JavaTypes, Androidapi.JNI.GraphicsContentViewText,

  Androidapi.JNIBridge;

 

{$SCOPEDENUMS ON}

 

type

 

  TAndroidFontGlyphManager = class(TFontGlyphManager)

  private

    FPaint: JPaint;

    // Current metrics

    FSpacing: Single;

    FTop: Single;

    FTopInt: Integer;

    FAscent: Single;

    FDescent: Single;

    FBottom: Single;

    FBottomInt: Integer;

    FLeading: Single;

    FLeadingInt: Integer;

  protected

    procedure LoadResource; override;

    procedure FreeResource; override;

    function DoGetGlyph(const Char: UCS4Char;

      const Settings: TFontGlyphSettings): TFontGlyph; override;

  public

    constructor Create;

    destructor Destroy; override;

  end;

 

implementation

 

uses

  System.Math, System.Character,

  Androidapi.Bitmap,

  //引入System.IOUtils是为了能够获取Android的各种系统目录

  System.IOUtils,

  //

  FMX.Graphics;

 

{ TAndroidFontGlyphManager }

 

constructor TAndroidFontGlyphManager.Create;

begin

  inherited Create;

  FPaint := TJPaint.Create;

end;

 

destructor TAndroidFontGlyphManager.Destroy;

begin

  FPaint := nil;

  inherited;

end;

 

procedure TAndroidFontGlyphManager.LoadResource;

const

  BoldAndItalic = [TFontStyle.fsBold, TFontStyle.fsItalic];

var

  TypefaceFlag: Integer;

  Typeface: JTypeface;

  FamilyName: JString;

  Metrics: JPaint_FontMetrics;

  MetricsInt: JPaint_FontMetricsInt;

  FontFile: string;

begin

  FPaint.setAntiAlias(True);

  FPaint.setTextSize(CurrentSettings.Size * CurrentSettings.Scale);

  FPaint.setARGB(255, 255, 255, 255);

  FPaint.setUnderlineText(TFontStyle.fsUnderline in CurrentSettings.Style);

  FPaint.setStrikeThruText(TFontStyle.fsStrikeOut in CurrentSettings.Style);

  if TOSVersion.Check(4, 0) then

    FPaint.setHinting(TJPaint.JavaClass.HINTING_ON);

  // Font

  try

    FamilyName := StringToJString(CurrentSettings.Family);

    if (BoldAndItalic * CurrentSettings.Style) = BoldAndItalic then

      TypefaceFlag := TJTypeface.JavaClass.BOLD_ITALIC

    else if TFontStyle.fsBold in CurrentSettings.Style then

      TypefaceFlag := TJTypeface.JavaClass.BOLD

    else if TFontStyle.fsItalic in CurrentSettings.Style then

      TypefaceFlag := TJTypeface.JavaClass.ITALIC

    else

      TypefaceFlag := TJTypeface.JavaClass.NORMAL;

 

    { Fix Begin 修改开始.如果在下载目录中存在跟字体同名的.ttf文件,那么优先使用ttf文件.

      我是放在SD卡的下载目录中.大家可以按需要任意改这个位置.

      甚至也可以放在Asset目录中,这样可以打包在APK中.

    }

    FontFile := TPath.GetSharedDownloadsPath + PathDelim +

      CurrentSettings.Family + '.ttf';

    if FileExists(FontFile) then

      Typeface := TJTypeface.JavaClass.createFromFile(StringToJString(FontFile))

    else

      Typeface := TJTypeface.JavaClass.Create(FamilyName, TypefaceFlag);

    { Fix End 修改结束 }

    FPaint.setTypeface(Typeface);

    try

      Metrics := FPaint.getFontMetrics;

      MetricsInt := FPaint.getFontMetricsInt;

      //

      FSpacing := FPaint.getFontMetrics(Metrics);

      FTop := Metrics.top;

      FTopInt := MetricsInt.top;

      FAscent := Metrics.ascent;

      FDescent := Metrics.descent;

      FBottom := Metrics.bottom;

      FBottomInt := MetricsInt.bottom;

      FLeading := Metrics.leading;

      FLeadingInt := MetricsInt.leading;

 

      // SysDebug(FloatToStr(CurrentSettings.Size) + ':' + FloatToStr(CurrentSettings.Scale));

      // Log.d(Format('Top=(%d %f) Bottom=(%d %f) Leading=(%d %f) FAscent=(%d %f)', [FTopInt, FTop, FBottomInt, FBottom, FLeadingInt, FLeading, MetricsInt.ascent, FAscent]));

    finally

      Metrics := nil;

      MetricsInt := nil;

    end;

  finally

    FamilyName := nil;

    Typeface := nil;

  end;

end;

 

procedure TAndroidFontGlyphManager.FreeResource;

begin

  if Assigned(FPaint) then

    FPaint.reset;

end;

 

function TAndroidFontGlyphManager.DoGetGlyph(const Char: UCS4Char;

  const Settings: TFontGlyphSettings): TFontGlyph;

var

  Text: JString;

  Bitmap: JBitmap;

  Canvas: JCanvas;

  GlyphRect: TRect;

  C, I, J, Width, Height: Integer;

  Advance: Single;

  Bounds: JRect;

  GlyphStyle: TFontGlyphStyles;

  PixelBuffer: Pointer;

  Data: PIntegerArray;

  Path: JPath;

  PathMeasure: JPathMeasure;

  PathLength: Single;

  Coords: TJavaArray<Single>;

  StartPoint, LastPoint, Point: TPointF;

  NewContour, HasStartPoint: Boolean;

begin

  try

    Text := StringToJString(System.Char.ConvertFromUtf32(Char));

    Advance := FPaint.measureText(Text);

    // SysDebug(Format('%s %f', [System.Char.ConvertFromUtf32(Char), Advance]));

    Height := Abs(FTopInt) + Abs(FBottomInt) + 2;

    Width := Ceil(Abs(Advance)) + 2;

    try

      Bitmap := TJBitmap.JavaClass.createBitmap(Width, Height,

        TJBitmap_Config.JavaClass.ARGB_8888);

      try

        Bounds := TJRect.Create;

        FPaint.getTextBounds(Text, 0, Text.length, Bounds);

        // Log.d(Format('Bounds=(%d %d %d %d) %d %d ', [Bounds.left, Bounds.top, Bounds.right, Bounds.bottom, Bounds.width, Bounds.height]));

        try

          Canvas := TJCanvas.JavaClass.init(Bitmap);

          Canvas.drawText(Text, 0, -Trunc(FAscent), FPaint);

        finally

          Canvas := nil;

        end;

 

        GlyphStyle := [];

        if ((FAscent = 0) and (FDescent = 0)) or not HasGlyph(Char) then

          GlyphStyle := [TFontGlyphStyle.NoGlyph];

        if TFontGlyphSetting.gsPath in Settings then

          GlyphStyle := GlyphStyle + [TFontGlyphStyle.HasPath];

 

        Result := TFontGlyph.Create(TPoint.Create(Bounds.left,

          Abs(FTopInt - Bounds.top)), Advance, Abs(FTopInt) + Abs(FBottomInt) +

          Abs(FLeadingInt), GlyphStyle);

 

        if (TFontGlyphSetting.gsBitmap in Settings) and

          (HasGlyph(Char) or ((FAscent <> 0) or (FDescent <> 0))) and

          (AndroidBitmap_lockPixels(TJNIResolver.GetJNIEnv,

          (Bitmap as ILocalObject).GetObjectID, @PixelBuffer) = 0) then

        begin

          Data := PIntegerArray(PixelBuffer);

          GlyphRect.left := Bounds.left;

          GlyphRect.Right := Bounds.Right;

          GlyphRect.top := Abs(Trunc(FAscent) - Bounds.top);

          GlyphRect.bottom := Abs(Trunc(FAscent) - Bounds.bottom);

          // Log.d(Format('GlyphRect=(%d %d %d %d) %d %d', [GlyphRect.Left, GlyphRect.Top, GlyphRect.Right, GlyphRect.Bottom, GlyphRect.Width, GlyphRect.Height]));

 

          if (GlyphRect.Width > 0) or (GlyphRect.Height > 0) then

          begin

            Result.Bitmap.SetSize(GlyphRect.Width + 1, GlyphRect.Height + 1,

              TPixelFormat.pfA8R8G8B8);

            if TFontGlyphSetting.gsPremultipliedAlpha in Settings then

            begin

              for I := GlyphRect.top to GlyphRect.bottom do

                Move(Data[I * Width + Max(GlyphRect.left, 0)],

                  Result.Bitmap.GetPixelAddr(0, I - GlyphRect.top)^,

                  Result.Bitmap.Pitch);

            end

            else

              for I := GlyphRect.top to GlyphRect.bottom - 1 do

                for J := GlyphRect.left to GlyphRect.Right - 1 do

                begin

                  C := Data[I * Width + J];

                  if C <> 0 then

                  begin

                    C := ((C shr 16) and $FF + (C shr 8) and

                      $FF + (C and $FF)) div 3;

                    Result.Bitmap.Pixels[J - GlyphRect.left, I - GlyphRect.top]

                      := MakeColor($FF, $FF, $FF, C);

                  end

                end;

          end;

          AndroidBitmap_unlockPixels(TJNIResolver.GetJNIEnv,

            (Bitmap as ILocalObject).GetObjectID);

        end;

        // Path

        if TFontGlyphSetting.gsPath in Settings then

          try

            Path := TJPath.Create;

            FPaint.getTextPath(Text, 0, Text.length, Result.Origin.X,

              Result.Origin.Y, Path);

            PathMeasure := TJPathMeasure.Create;

            PathMeasure.setPath(Path, False);

            Coords := TJavaArray<Single>.Create(2);

            if PathMeasure.getLength > 0 then

              repeat

                PathLength := PathMeasure.getLength;

                NewContour := True;

                HasStartPoint := False;

                I := 0;

                while I < PathLength do

                begin

                  if PathMeasure.getPosTan(I, Coords, nil) then

                  begin

                    Point := PointF(Coords[0], Coords[1]);

                    if NewContour then

                    begin

                      Result.Path.MoveTo(Point);

                      NewContour := False;

                      HasStartPoint := False;

                    end

                    else if Point <> LastPoint then

                    begin

                      if HasStartPoint and (LastPoint <> StartPoint) then

                        if not SameValue

                          (((Point.Y - StartPoint.Y) / (Point.X - StartPoint.X)

                          ), ((Point.Y - LastPoint.Y) / (Point.X - LastPoint.X)

                          ), Epsilon) then

                        begin

                          Result.Path.LineTo(Point);

                          HasStartPoint := False;

                        end

                        else

                      else

                        Result.Path.LineTo(Point);

                    end;

                    LastPoint := Point;

                    if not HasStartPoint then

                    begin

                      StartPoint := Point;

                      HasStartPoint := True;

                    end;

                  end;

                  Inc(I);

                end;

                if Result.Path.Count > 0 then

                  Result.Path.ClosePath;

              until not PathMeasure.nextContour;

            Point := Result.Path.GetBounds.TopLeft;

            Result.Path.Translate(-Point.X + Result.Origin.X,

              -Point.Y + Result.Origin.Y);

          finally

            FreeAndNil(Coords);

            Path := nil;

            PathMeasure := nil;

          end;

      finally

        Bounds := nil;

      end;

    finally

      Bitmap.recycle;

      Bitmap := nil;

    end;

  finally

    Text := nil;

  end;

end;

 

end.